The Making of bs, a Programmable Button Shell for X

by Luiz Henrique de Figueiredo


Abstract. bs is a simple programmable button shell for X that I wrote years ago. Despite being a powerful tool, supporting user-defined dialog layout, bs is actually a very simple and short program. The source code of bs is fully explained in this article. This explanation provides brief introduction to Xt, widgets, callbacks and callback data. I also show how easy it is to use Motif widgets instead of Athena widgets, for which bs was originally developed. The code is freely available.

Introduction

I am a programmer. I like to write tools for programmers. Like many others, I sometimes spend more time writing tools than the programs I'm supposed to write. I wrote bs back in 1992 to help me during my Ph.D. research (but also to avoid having to do the research itself...). I spend a lot of time in long development cycles: edit, compile, run, test, edit again. I also spend a lot of time writing papers and documents: edit, run TeX, preview, edit again. Despite the good development tools available in Unix, particularly make and shells with editable command line history, these development cycles are boring when controlled from the keyboard. I wrote bs to provide a simple graphical user interface for such repetitive cycles of sending commands to Unix shells. It turns out that bs is also useful as a general purpose menu generator. Despite being programmable and supporting user-defined dialog layout, bs is actually a very simple and short program (only 134 lines of C). In this article, I'll fully explain the source code of bs.

Writing a graphical user interface in X using Xlib directly can be done, but is a daunting task. It is much easier to use one of the many existing widget sets, which are usually supported by Xt, the X Toolkit Intrinsics library. Besides needing a tool, I wrote bs also because I wanted to learn how to use Xt and widget sets to write graphical user interfaces in X. At that time, I only had access to Xaw, the Athena widget set, which is distributed with X. I'll explain the original code for bs, written for Xaw. However, as I'll show later, it is easy to change bs to use Motif instead of Xaw.

Defining bs

I wanted a tool that could create simple panels composed of labels and buttons. Labels contain informative text; buttons are associated with commands that are run when the button is pressed by the user. I wanted a programmable tool that would read a panel description from a file written by the user, so that bs could be used for many different tasks.

Layout Model

Besides wanting to program what bs panels would do, I also wanted to control how these panels would look like. I wanted to be able to control the layout of the panel because a long row of buttons and labels looks ugly and requires a lot of mouse movement. I wanted to be able to create horizontal panels and also vertical panels. What I really needed was some way to create a new row, something like a new ``paragraph'', in the panel. In many text processing systems, such as troff and TeX, a paragraph ends with an empty line. I adopted the same idea for bs.

The file read by bs to create the panel is a plain text file in which each line describes either a button or a label. Buttons and labels appear side by side in the panel, from left to right. An empty line in the file signals the start of a new row in the panel. So, descriptions of ``horizontal'' panels have no empty lines, and descriptions of ``vertical'' panels have an empty line after each element.

This simple scheme proved to be powerful enough for creating a large variety of layouts. Space characters are significant in labels and button labels, and can be used for alignment, provided a fixed-width, non-proportional font is used.

Describing Panels

There are four kinds of lines in the file describing a panel:

Empty lines control layout as explained above.

Lines without a TAB represent labels. The text in the line appears verbatim in the panel. Space characters are significant and can be used for alignment.

Lines with a TAB not on the first column represent buttons. The text before the TAB is the button text. The text after the TAB is a command that is run when the button is pressed. The command is run by feeding the text to a Unix shell.

Lines starting with a TAB are called startup lines and are sent to the shell as soon as bs begins. They typically contain variable definitions, but they can contain any command. Because bs sends data to a shell using a single pipe that remains open throughout the execution, variables definitions remain after this initialization and can be used in the command lines associated with buttons. I only added support for this kind of line later, but it proved to be very useful in writing reusable panel descriptions, parametrized by a few lines on the top.

For example, I originally used the panel described below for writing this article. The TABs don't show, but I hope you can guess where they are.


	L="usepath latex209 current -- latex"
	H="latex2html -split 0 -nolatex -no_navigation"
	F=bs
	netscape /u/lhf/w/doc/xa/bs/bs.html &
bs
edit	xterm -geometry 80x70+0+0 -title $F -e vi $F.tex &
tex	$L $F.tex </dev/null
preview	xdvi -geometry +0+0 -hushspecials $F.dvi &
html	$H $F.tex; netscape-remote -remote 'Reload()'
print	dvips -f $F.dvi | lpr -h

This description starts with variable definitions that are used in the command lines associated with buttons. It also starts netscape. (Don't worry about the precise commands. They are probably slightly different on your system.) There is a single label, bs; all remaining lines describe buttons. The button edit opens a tall xterm running vi on my TeX file, which is compiled with the tex button and converted to HTML with the html button, which runs latex2html and instructs netscape to reload the file with netscape-remote. (This was what I did back in 1995. I now write HTML directly with vi.) Note that a command line is not restricted to a single command. Note also that some commands are run in the background, to allow other commands to be run. The print button is actually a left-over from a previous script for writing papers in TeX; I continually reuse panel descriptions. bs automatically adds a button labeled "quit" at the end of the panel, which does the obvious thing. Note that this panel is a horizontal panel, because there are no empty lines (see Figure 1). Like a lot of files in Unix, this panel description seems cryptic, but it works like charm and I find this format convenient to write and parse.

image of panel

Figure 1 - The panel used to write this article

Just to give you a flavor of what can be done with bs, a panel for compiling C programs could be:


	X="xterm -geometry 80x70+0+0 -title"
	E="-e vi"
myprog
main	F=main; $X $F.c $E $F.c &
lib	F=lib; $X $F.c $E $F.c &
make	make
run	$F

Note how variable definitions simplify command lines.

The panel below is an example of how more sophisticated layouts can be described (see Figure 2). Although you can't see them, blank spaces have been used for alignment: the label on the ``who'' button is actually "who " (with one trailing space), and the label on the ``ls'' button is actually "ls  " (with two trailing spaces). Also, the file does not end with an empty line, and so the ``quit'' button appears right after the last label.


try some Unix commands

date	date
displays current date and time

who 	who
display who is logged in

ls  	ls
displays contents of current directory

then, when you're tired, just

image of panel

Figure 2 - Sophisticated layout

Writing bs

Enough about the design of bs. I hope the explanation above has convinced you that if you know the command lines needed to perform a task, then it is easy to write a panel description file for bs that creates a simple graphical user interface for this task.

Let's now see how bs is implemented in X using Xt and Xaw.

Initialization

Like I said before, bs uses Xt, the X Toolkit Intrinsics library, to create a graphical user interface in X. The easiest way to initialize an Xt application is by calling XtInitialize. This routine has actually been superseded by XtAppInitialize, but the only documentation I had at the time I wrote bs was an old DEC manual for X11R3. I never changed the code of bs after I wrote it, so I'll explain the ``old'' way. Newer applications, however, should probably use XtAppInitialize instead of XtInitialize. There are XtApp... versions for a few other routines in Xt.

In any case, Xt is initialized in bs with

 toplevel=XtInitialize(argv[0],"bs",NULL,0,&argc,argv);

XtInitialize returns a top level window, that is, a window whose parent is the root window. The labels and buttons created by bs will be placed inside this window. The ``id'' of this window is stored in a global variable toplevel, which is declared as:

static Widget toplevel;
Widget is an opaque Xt data type declared in
#include <X11/Intrinsic.h>
which you need to include in every Xt application.

XtInitialize takes care of all necessary initialization, including parsing and interpreting Xt command line options. This is the reason for passing argc and argv to XtInitialize. By giving Xt access to the command line in this way, every Xt application is automatically configurable using command line options. Thus, for example, without a line of code from me, the user can select the font to be used in both labels and buttons with the -fn option. In bs, XtInitialize also receives the program name stored in argv[0] as the instance name and "bs" as the class name, so that bs can also be configured using resource files.

Let's now see where XtInitialize is actually called in bs.

The main Routine

The complete main routine in bs is:
int main(int argc, char* argv[])
{
 shell=popen("/bin/sh","w");
 doargs(argc,argv);
 makemenu();
 XtRealizeWidget(toplevel);
 XtMainLoop();
 return 0;
}

This routine first opens a pipe to the Bourne shell /bin/sh with popen. All commands are sent down this pipe, which remains open throughout the execution of bs so that commands can set variables that are used later. The FILE descriptor for the pipe is stored in the global variable shell, because it is needed in the code that handle buttons:
static FILE* shell;
Then, main parses the command line with doargs, initializing Xt and selecting a panel description file, and builds the panel by parsing this file with makemenu. Once labels and buttons have been created, the panel is ready to be displayed with XtRealizeWidget(toplevel), and main yields control to Xt by calling XtMainLoop, so that bs can respond to user actions. XtMainLoop never actually returns, but main ends with return 0 to avoid compilation or lint warnings. If you use XtAppInitialize instead of XtInitialize, then you should use XtAppMainLoop instead of XtMainLoop.

Command Line Processing

The actual Xt initialization is done in doargs:
void doargs(int argc, char* argv[])
{
 char* f;
 toplevel=XtInitialize(argv[0],"bs",NULL,0,&argc,argv);
 switch (argc)
 {
  case 1:
   f=".bsrc";
   break;
  case 2:
   f=argv[1];
   break;
  default:
   fprintf(stderr,"usage: bs [menu-file] [X toolkit options]\n");
   exit(1);
 }
 if (freopen(f,"r",stdin)==NULL)
 {
  fprintf(stderr,"bs: cannot open ");
  perror(f);
  exit(1);
 }
}

This routine calls XtInitialize to parse the command line and create toplevel, as explained before. XtInitialize consumes all Xt command line options, and so, at most one argument can remain after XtInitialize: the name of the file containing the panel description. If no arguments are left, then bs uses a file named .bsrc by default, which must reside in the current directory. (I usually create a .bsrc file for each project I work on.) The code then makes sure that stdin points to the panel description file, by using freopen. This redirection is not strictly necessary, but it simplifies the code and avoids creating another global variable. If the redirection fails, then bs aborts gracefully, calling perror to tell the user why it failed. If the redirection succeeds, then doargs retuns to main, where the panel is built by parsing the panel description file with makemenu, which I explain next.

Creating the Panel

The main problem in implementing bs is implementing the layout model. Widget sets usually include widgets to support many different layout models. Such layout widgets are called geometry managers in Xt. The Athena widget set has several geometry managers; it turns out that the Form widget is simple to use and suitable for implementing the layout model in bs. A Form widget displays its children in rows and columns. The children of the Form in bs are just labels or buttons, but Form can manage any kind of widget. What makes Form suitable for bs is that it is possible to define the position of a child widget relatively to existing widgets. This is done by requesting that the child be to the right of an existing widget and below another existing widget. The desired layout for a bs panel is built by using a Form widget and keeping track of the last widget created in a row (stored in the global variable wh), and of a widget in last row (stored in the global variable wv). It is convenient that any widget in the last row works.

Once I had identified that the Form widget was suitable, the actual parsing was simple to write:


void makemenu(void)
{
 char s[BUFSIZ];
 form=XtCreateManagedWidget("form",formWidgetClass,toplevel,NULL,0);
 while (fgets(s,sizeof(s),stdin))
 {
  char* t;
  if (*s=='\n')				/* empty line: new row */
  {
   if (wh!=NULL)			/* handle multiple empty lines */
   {
    wv=wh;
    wh=NULL;
   }
   continue;
  }
  t=strchr(s,'\t');
  if (t==NULL)				/* empty command: label */
   wh=addlabel(s);
  else if (t==s)			/* empty button label: prolog */
   execute(s);
  else					/* button */
  {
   *t++=0;
   wh=addbutton(s,do_it,t);
  }
 }
 addbutton("quit",do_quit,NULL);
}

This routine first creates a Form inside the top level window by calling XtCreateManagedWidget with formWidgetClass. Most user interfaces define a hierarchy of widgets in which every widget has a parent widget. In this case, form is a child of toplevel. This form is given an external name "form" so that bs can be configured using resources.

After creating the form, makemenu reads the panel description file line by line, acting according to the type of the line. Lines without a TAB are labels. The routine addlabel creates a label with the given text. Startup lines (those starting with a TAB) are sent immediately to the shell using execute:


void execute(char* s)
{
 fputs(s,shell);
 fflush(shell);
}

Note that I flush the pipe to make sure that the command is executed by the shell at once. This same routine will be used to execute commands associated with buttons.

Lines with a TAB not on the first column are buttons. Such lines are split into two strings: the button text and the command line. Buttons are created with the routine addbutton, which takes three arguments: the text to appear on button, a C function to be called when the button is pressed, and a string to be used as argument to this C function. The function called when a user-defined button is pressed is do_it. This function simply calls execute to send the command line associated with the button to the shell:


void do_it(Widget w, caddr_t client_data, caddr_t call_data)
{
 execute(client_data);
}

A quit button is automatically created at the end of the panel. The function called when this button is pressed is do_quit, which simply closes the pipe and exits:


void do_quit(Widget w, caddr_t client_data, caddr_t call_data)
{
 pclose(shell);
 exit(0);
}

These two functions, do_it and do_quit, are not called explicitly anywhere in bs. They are callback functions, called by Xt in response to a user event; in this case, pressing a button in the panel created by bs. Programming a user interface is radically different from other kinds of programming, such as batch text processing or accounting. A user interface program is event-driven: it is controlled by user generated events; it is not sequentially controlled like batch programs. A user interface program typically creates and displays the interface, registers callback functions to be called in response to user events, and then yields control to the supporting library, which manages the interaction with the user, calling the appropriate callback functions.

In Xt, a callback function like do_it receives three parameters: the widget w on which the event occurred and pointers to client data and call data. Call data is whatever additional information is needed for specifying the event. Other widgets use call data, but not the buttons in bs. Client data is data that goes hand-in-hand with the callback function. In bs, client data is used in addbutton to store the command line associated with a button:


Widget addbutton(char* label, Callback* f, char* p)
{
 Widget w=XtVaCreateManagedWidget(label,commandWidgetClass,form,
	XtNfromHoriz,	(XtArgVal) wh,
	XtNfromVert,	(XtArgVal) wv,
	NULL);
 XtAddCallback(w,XtNcallback,f,XtNewString(p));
 return w;
}

This function first creates a button inside the Form widget created in makemenu by calling XtVaCreateManagedWidget with commandWidgetClass (buttons widgets are called Command in Xaw). XtVaCreateManagedWidget is a variant of XtCreateManagedWidget that allows additional information to be passed in line. In this case, this information is precisely what is needed to implement the layout, as mentioned above.

After creating the button, addbutton calls XtAddCallback to register a function to be called when the user presses the button. The function XtAddCallback registers a C function as a callback function to a given widget; client data for the callback function can be registered at the same time. bs registers the functions do_it and do_quit as callbacks to buttons created with addbutton. Note that the command line is duplicated with XtNewString, because it is stored in a local variable in makemenu. No command line is associated with the ``quit'' button, so makemenu calls addbutton with NULL in the third argument. It is convenient that XtNewString does the right thing in this case.

For convenience, the type Callback is defined in bs as:

typedef void Callback(Widget w, caddr_t client_data, caddr_t call_data);
Here, caddr_t is a opaque data type representing a pointer. In later revisions of Xt, the type caddr_t has been replaced by XtPointer.

The function addlabel is similar to addbutton but is simpler, because there are no callbacks associated with labels. Again, it calls XtVaCreateManagedWidget, but now with labelWidgetClass, to create the label inside the Form:


Widget addlabel(char* label)
{
 Widget w=XtVaCreateManagedWidget(label,labelWidgetClass,form,
	XtNfromHoriz,	(XtArgVal) wh,
	XtNfromVert,	(XtArgVal) wv,
	XtNborderWidth,	(XtArgVal) 0,
	NULL);
 return w;
}

Note how the layout is specified in both routines using the resources XtNfromHoriz and XtNfromVert. These resources are understood by the Form widget that is given as parent to the button or label being created. Note also that wh stores the last widget created in a row and that wv stores the last widget created in the previous row. As explained before, this information is sufficient to realize the layout model of bs by using relative positioning in a Form widget.

Complete Source Code for bs

All functions in bs have been shown. The code is completed by adding include files, function prototypes, and global variables declarations. So here it is, as I originally wrote it in 1992; only 134 lines of C:
/*
* bs.c
* simple button shell for X11
* Luiz Henrique de Figueiredo (lhf@visgraf.impa.br)
* 05 Nov 92
*/

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Xaw/Command.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Label.h>

typedef void Callback(Widget w, caddr_t client_data, caddr_t call_data);

void	doargs		(int argc, char* argv[]);
void	makemenu	(void);
void	execute		(char* s);
Widget	addlabel	(char* label);
Widget	addbutton	(char* label, Callback* f, char* p);
void	do_it		(Widget w, caddr_t client_data, caddr_t call_data);
void	do_quit		(Widget w, caddr_t client_data, caddr_t call_data);

static	Widget		toplevel;
static	Widget		form;
static	Widget		wh=NULL;
static	Widget		wv=NULL;
static	FILE*		shell;

int main(int argc, char* argv[])
{
 shell=popen("/bin/sh","w");
 doargs(argc,argv);
 makemenu();
 XtRealizeWidget(toplevel);
 XtMainLoop();
 return 0;
} 

void doargs(int argc, char* argv[])
{
 char* f;
 toplevel=XtInitialize(argv[0],"bs",NULL,0,&argc,argv);
 switch (argc)
 {
  case 1:
   f=".bsrc";
   break;
  case 2:
   f=argv[1];
   break;
  default:
   fprintf(stderr,"usage: bs [menu-file] [X toolkit options]\n");
   exit(1);
 }
 if (freopen(f,"r",stdin)==NULL)
 {
  fprintf(stderr,"bs: cannot open ");
  perror(f);
  exit(1);
 }
}

void makemenu(void)
{
 char s[BUFSIZ];
 form=XtCreateManagedWidget("form",formWidgetClass,toplevel,NULL,0);
 while (fgets(s,sizeof(s),stdin))
 {
  char* t;
  if (*s=='\n')				/* empty line: new row */
  {
   if (wh!=NULL)			/* handle multiple empty lines */
   {
    wv=wh;
    wh=NULL;
   }
   continue;
  }
  t=strchr(s,'\t');
  if (t==NULL)				/* empty command: label */
   wh=addlabel(s);
  else if (t==s)			/* empty button label: prolog */
   execute(s);
  else					/* button */
  {
   *t++=0;
   wh=addbutton(s,do_it,t);
  }
 }
 addbutton("quit",do_quit,NULL);
}

void execute(char* s)
{
 fputs(s,shell);
 fflush(shell);
}

Widget addlabel(char* label)
{
 Widget w=XtVaCreateManagedWidget(label,labelWidgetClass,form,
	XtNfromHoriz,	(XtArgVal) wh,
	XtNfromVert,	(XtArgVal) wv,
	XtNborderWidth,	(XtArgVal) 0,
	NULL);
 return w;
}

Widget addbutton(char* label, Callback* f, char* p)
{
 Widget w=XtVaCreateManagedWidget(label,commandWidgetClass,form,
	XtNfromHoriz,	(XtArgVal) wh,
	XtNfromVert,	(XtArgVal) wv,
	NULL);
 XtAddCallback(w,XtNcallback,f,XtNewString(p));
 return w;
}

void do_it(Widget w, caddr_t client_data, caddr_t call_data)
{
 execute(client_data);
}

void do_quit(Widget w, caddr_t client_data, caddr_t call_data)
{
 pclose(shell);
 exit(0);
}

Compiling

The simplest way to make an X program is with imake. First, you write an Imakefile such as the one below. (Note that bs needs an ANSI C compiler; gcc is available in most systems.) Then, run xmkmf to convert this Imakefile to a Makefile. Finally, run make to make bs.
             CC = gcc
        DEPLIBS = XawClientDepLibs
LOCAL_LIBRARIES = XawClientLibs

SimpleProgramTarget(bs)

If you don't want to use imake, a simple Makefile for bs is:


# bs needs an ANSI compiler
CC=gcc
CFLAGS=-O2

# some systems might need -lm.  others might not need -lXext.
LIBS=-lXaw -lXmu -lXt -lXext -lX11

bs:	bs.c
	$(CC) $(CFLAGS) -o bs bs.c $(LIBS)

Resources

bs does not define any new resources, but any resource understood by the Athena widgets used in bs (Command, Form, Label) can be specified, either on the command line using the -xrm option, or in a resource file, such as .Xdefaults. For example, if you want the text in the ``quit'' button to be red, you can use the resource
bs*quit*foreground: red

I like to use a non-proportional font for alignment. I also like the background of the panel in grey and the buttons to have white background. I place all bs panels at the same spot on the screen. And I don't want mwm to add resize handles to bs panels. These preferences are realized with the following resources:


Mwm*bs.clientDecoration: border menu
bs*geometry: -2+112
bs*background: gray80
bs*Command*background: white
bs*font: fixed

The panels in Figures 1 and 2 were created with these resources in effect. Of course, the appearance of panels is a matter of taste. The nice thing about using Xt to manage resources is that I do not need to write support for user preferences.

Using Motif

Motif is very popular nowadays. It is simple to change bs to use Motif widgets instead of Athena widgets, because Motif has a Form widget, with similar semantics, and also labels and buttons (although buttons are called pushbuttons). Most things in Xaw exist in Motif, perhaps under a different name.

The include files for using Motif are:


#include <Xm/Xm.h>
#include <Xm/Form.h>
#include <Xm/Label.h>
#include <Xm/PushB.h>

The file <Xm/Xm.h> includes the necessary Xt files, such as <X11/Intrinsic.h>.

The Form widget has a different name in Motif, but is created exactly as in Xaw:

 form=XtCreateManagedWidget("form",xmFormWidgetClass,toplevel,NULL,0);

The same is true for labels and buttons. The only real difference is that it takes a few more lines to implement the layout model because, instead of horizontal and vertical relative positioning, Motif allows left, right, top and bottom relative positioning:


Widget addlabel(char* label)
{
 Widget w=XtVaCreateManagedWidget(label,xmLabelWidgetClass,form,
	XmNleftAttachment,	(XtArgVal) XmATTACH_WIDGET,
	XmNleftWidget,		(XtArgVal) wh,
	XmNtopAttachment,	(XtArgVal) XmATTACH_WIDGET,
	XmNtopWidget,		(XtArgVal) wv,
	NULL);
 return w;
}

Widget addbutton(char* label, Callback* f, char* p)
{
 Widget w=XtVaCreateManagedWidget(label,xmPushButtonWidgetClass,form,
	XmNleftAttachment,	(XtArgVal) XmATTACH_WIDGET,
	XmNleftWidget,		(XtArgVal) wh,
	XmNtopAttachment,	(XtArgVal) XmATTACH_WIDGET,
	XmNtopWidget,		(XtArgVal) wv,
	NULL);
 XtAddCallback(w,XmNactivateCallback,f,XtNewString(p));
 return w;
}

Except for having to change the Makefile to use -lXm instead of -lXaw, that is all there is to it! The Motif version is just as small as the original Xaw version.

Afterthoughts

This article is slightly modified version of a feature article originally written in 1995 for The X Advisor (now defunct). Here are a few extra comments on the code and on the use of bs:

Conclusion

Writing bs was an instructive experience for me. The most difficult part was going through the large manuals, trying to figure out what widgets to use, what functions to call and with what arguments.

I now have a useful tool and also know how to use Xt and widget sets. I hope this article helps you learn them too.

A bs package containing source code, man page, and examples, is available at sites in Brazil and Canada, and also at the official X repository and its mirrors in contrib/desktop_managers/.


Luiz Henrique de Figueiredo is a Visiting Researcher at IMPA, on leave from LNCC, and also a consultant at TeCGraf, the Computer Graphics Technology Group of PUC-Rio. He is one of the designers of the Lua language.


Last update: Mon May 10 16:55:01 EST 1999 by lhf.