bs
, a Programmable Button Shell for X
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.
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.
bs
bs
could be used for many different tasks.
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.
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.
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
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
Figure 2 - Sophisticated layout
bs
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.
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
.
main
Routinemain
routine in bs
is:
int main(int argc, char* argv[]) { shell=popen("/bin/sh","w"); doargs(argc,argv); makemenu(); XtRealizeWidget(toplevel); XtMainLoop(); return 0; }
/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
.
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.
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); }
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); }
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.
bs
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); }
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)
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
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.
bs
:
addbutton
,
which can be avoided by casting f
to XtCallbackProc
:
XtAddCallback(w,XtNcallback,(XtCallbackProc)f,XtNewString(p));
if (t==NULL) /* empty command: label */ { t=strchr(s,'\n'); if (t!=NULL) *t=0; wh=addlabel(s); }
bs
because that is what I write scripts in.
If you prefer other shells,
you may want to change the code to something like
shell=popen(getenv("SHELL"),"w");Another possibility is to use
exec $SHELL
as the first startup line.
This replaces the Bourne shell with your current shell.
Using variants of this idea
allows bs
to send commands to other programs
(but beware that
some programs buffer their input and
do not respond immediately as lines are fed into them).
xmenu
,
xpick
,
and
xprompt
to set variables that are later used in commands,
as in:
choose F=`xmenu *` edit xterm -e vi $F &
bs
inherits
stdin
,
stdout
,
and
stderr
from the shell that starts it.
I usually start a new xterm
to run bs
;
then any error messages that occur appear in this window.
I need to give /dev/null
as input to any
programs that might want to read from stdin
,
such as TeX in the first example.
Finally,
I usually add set -x
to the start-up code in panel descriptions
so that I see what commands are being run.
Just the other day,
I found out that,
instead of a dedicated xterm
,
I can use xless
in ``tail mode'':
bs |& xless -fThis redirects all text written to
stdout
and stderr
to xless
,
which reads and displays it as it is output.
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.