Scripting the GUI with 'hey'
Haiku’s GUI is in principle entirely scriptable. You can change a window’s position and size and manipulate pretty much every widget in it.
The tool to do this is hey
. It sends BMessages to an application, thus emulating what happens if the user clicks on a menu, checkbox, or other widgets.
The seminal work on this application scripting is the BeOS Application Scripting chapter of the BeOS Bible by Chris Herborth. You should study that first and, if you’re like me, will keep coming back to it every time you think about solving something via hey
.
This blog isn’t an introduction to scripting with hey, but more like a collection of useful hey-lines. It’s intended to bring our GCI students up to speed, who have chosen the task to create hey-scripts to automate taking screenshots for the user guide. Please comment, if you have more tips!
As example application, we have a look at the Time preferences.
Moving and resizing a window
You can get a window’s size and position with:
hey Time get Frame of Window 0
Reply BMessage(B_REPLY):
"result" (B_RECT_TYPE) : BRect(163.0, 53.0, 649.0, 420.0)
The coordinates of the BRect are in the screen coordinates (left/top/right/bottom).
To set a new size/position:
hey Tracker set Frame of Window 0 to "BRect(1329.0, 65.0, 1905.0, 968.0)"
Reply BMessage(B_REPLY):
"error" (B_INT32_TYPE) : 0 (0x00000000)
Don’t be fooled, the “error” output of “0” is really a sign of success. You can avoid all junk output and limit it to the important essentials by adding the -o
parameter. Makes for much easier scripting, too.
If you have a smaller resolution, you may want to adjust the screen coordinates accordingly.
As you can see, by forcing these absolute coordinates, the window layout isn’t in any way restricted and can result in widgets being placed widely spaced in that new window frame.
Changing tabs
It’s not as straight forward as expected… To change to the “Clock” tab:
hey -o Time set Selection of View 0 of View 0 of Window 0 to 3
You can explore the hierarchy of an app’s GUI by counting the views in the window, getting their “InternalName” and looking at the output of the various “getsuites”:
hey Time count View of Window 0
Reply BMessage(B_REPLY):
"result" (B_INT32_TYPE) : 3 (0x00000003)
hey Time get InternalName of View 0 of Window 0
Reply BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "baseView"
"error" (B_INT32_TYPE) : 0 (0x00000000)
hey Time get InternalName of View 0 of View 0 of Window 0
Reply BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "tabView"
"error" (B_INT32_TYPE) : 0 (0x00000000)
hey Time getsuites of View 0 View 0 of Window 0
Reply BMessage(B_REPLY):
"suites" (B_STRING_TYPE) : "suite/vnd.Be-tab-view"
"suites" (B_STRING_TYPE) : "suite/vnd.Be-view"
"suites" (B_STRING_TYPE) : "suite/vnd.Be-handler"
"messages" (B_PROPERTY_INFO_TYPE) :
property commands specifiers types
---------------------------------------------------------------------------------------------------
Selection B_GET_PROPERTY B_SET_PROPERTY DIRECT LONG
Usage:
"messages" (B_PROPERTY_INFO_TYPE) :
property commands specifiers types
---------------------------------------------------------------------------------------------------
Frame B_GET_PROPERTY B_SET_PROPERTY DIRECT RECT
Usage: The view's frame rectangle.
Hidden B_GET_PROPERTY B_SET_PROPERTY DIRECT BOOL
Usage: Whether or not the view is hidden.
Shelf DIRECT
Usage: Directs the scripting message to the shelf.
View B_COUNT_PROPERTIES DIRECT LONG
Usage: Returns the number of child views.
View INDEX REV.INDEX NAME
Usage: Directs the scripting message to the specified view.
"messages" (B_PROPERTY_INFO_TYPE) :
property commands specifiers types
---------------------------------------------------------------------------------------------------
Suites B_GET_PROPERTY DIRECT (suites CSTR)(messages SCTD)
Usage:
Messenger B_GET_PROPERTY DIRECT MSNG
Usage:
InternalName B_GET_PROPERTY DIRECT CSTR
Usage:
"error" (B_INT32_TYPE) : 0 (0x00000000)
It also often helps to look into the app’s source code, of course. Find the place where the window layout is created and see if you can follow the view hierarchy.
Any visible control widget is usually some view contained in another view, which is contained in another view, which may be contained in another view, … which in the end is contained in the window. Like: A button view sits in a view, which sits in a container view, which sits in a tab view, which sits in a base view, which sits in a window. Turtles, all the way down…
However! You shouldn’t use such a View-of-View-of-concatenation in a script if you care about the script keep working if the app’s GUI changes. Use the “InternalName” instead. So this:
hey -o Time set Selection of View 0 of View 0 of Window 0 to 3
becomes:
hey -o Time set Selection of View "tabView" of Window 0 to 3
Checking a checkmark
We’re on the “Clock” tab of the Time preferences:
hey Time get InternalName of View 3 of View 0 of View 0 of View 0 of Window 0
Reply BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Clock"
"error" (B_INT32_TYPE) : 0 (0x00000000)
Find the “Show clock in Deskbar” checkbox by checking the ‘InternalName’ of the views inside the “Clock” view. The checkbox itself is in another view “show clock box”:
hey Time get InternalName of View 0 of View "Clock" of Window 0
Reply BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "show clock box"
"error" (B_INT32_TYPE) : 0 (0x00000000)
We can get the label of the checkbox as it appears in the GUI (and by using “set” instead of “get” we could even change that):
hey Time get Label of View 0 of View "show clock box" of Window 0
Reply BMessage(B_REPLY):
"result" (B_STRING_TYPE) : "Show clock in Deskbar"
Now, to simply remove the checkmark:
hey -o Time set Value of View 0 of View "show clock box" of Window 0 to 0
But as you see, that only un-draws the checkmark - it doesn’t actually run the code as if the user clicked the checkbox! Sometimes that might be enough if you only want to take a screenshot of the window. But in this case, actually unselecting the checkbox should also disable the checkboxes below. And of course hide the clock in the Deskbar.
We have to actually send the BMessage that will trigger all that. For this, we again need to look through the code. The BMessages for a view arrive at its MessageReceived() function, in our case in ClockView.cpp. We see the BMessage’s ‘what’ field’s matching constant ‘kShowHideTime’. Now we just need to find the actual value of that constant and do so in TimeMessages.h: ‘ShTm’ OpenGrok is very useful for these things. Now everything becomes even much easier:
hey -o Time 'ShTm'
That’s generally how you deal with all BControls that should trigger some action, like clicking a button.
Filling out a text field
The ‘Network time’ tab has a text field at the top:
hey -o Time set Value of View 0 of View 2 of View 0 of View 0 of View 0 of Window 0 to "Example text"
Or after a bit of nosing around with “get InternalName”:
hey -o Time set Value of View 0 of View "Network time" of Window 0 to "Example text"
humdinger's blog
- Registering your nickname for IRC
- Manipulating window look & behaviour with 'hey'
- Google Code-in 2019 finished
- Report of BeGeistert 031 in Hamburg
- Scripting the GUI with 'hey'
- Building packages with haikuporter
- Contributing via pull requests
- A short report of BeGeistert 029
- BeGeistert 028 impressions
- Videos of BeGeistert 026 are up