...making Linux just a little more fun! |
By Stephen Bint |
When I began using Linux I noticed that most Linux text editors are rubbish, having little or no mouse support, no shift-selection and no menus or file open dialogs. So I thought I could make a contribution, by writing an editor which had all the features we have come to associate with DOS editors, for the Linux console. Why shouldn't the better OS have editors which are at least as good?
So I searched for a library that would give me a colour text-mode interface to both platforms and I found Slang and curses. Neither were satisfactory. In order to provide an interface to a huge list of platforms, many features possible at the Linux console were disabled. Also, they were so big there was no prospect of customising them for my own needs and bundling the modified versions with my own sources. Out of frustration I set out to write my own.
I set myself the target of producing an interface with the widest possible range of ctrl- and alt- key combinations, a function to report the state of shift, ctrl and alt keys, full mouse reporting (including movement) and direct access to a screen array of EGA-style character-colour pairs. I hoped to produce something small and simple enough for programmers to bundle with their own source, so they could modify it if they wish and distribute with confidence.
Programming for the mouse was relatively easy. Under DOS, I just used int86 and Ralf Brown's Interrupt List. Under Linux, I struggled for a while and eventually mastered the gpm mouse driver, which has pretty good docs and demo programs.
Finding out how to output colour text to the Linux screen was more of a struggle. I was saved by an article at Linux Gazette called "So You Like Color!!!". I was shocked at what it said.
Unlike DOS, under which characters and colours are written directly to video memory as byte pairs, the Linux screen is refreshed by using fwrite to write to stdout! Instead of a colour being written with each character, the output colour must be changed whenever a character is a different colour to the one written before it. Changing the output colour involves writing an 11-byte string to stdout.
Consequently, screen refresh is very slow under Linux. I did what I could to speed it up. I keep a duplicate screen buffer which is updated as the screen is refreshed. Comparing this to the screen buffer, I refresh only parts of the screen which have changed. Even so, screen refresh takes twenty times as long under Linux as it does under DOS.
It is possible to access video memory in recent versions of Linux, by opening /dev/vcsa as a file. (see man vcs for details) There are two reasons not to do this. One is that only programs run by the superuser are allowed to do it. The second is that only the US ASCII character set is supported. At least with fwrite, the local character set is respected, which is important because Linux is an international thing, from its friendly welcome screen to its big, warm heart.
I found out how to show, hide and position the text cursor by examining the Slang sources and by using an excellent program bundled with Slang, called untic. Untic reads the terminfo database and translates it into human-readable form. (The terminfo database contains the command strings to write to stdout to perform control operations on any terminal.)
There was one little niggle. Under Linux, box-drawing characters are not part of the default character set. ASCII values which produce boxes under DOS produce funny foreign letters under Linux unless you send a string to stdout to switch to the alt character set. Switching to that charset permanently was not an option. I wanted the library to be international like Linux, supporting international character sets, so what to do?
I decided to use the high bit of the colour byte as a box bit. Programmers wishing to draw boxes would have to set the box bit in the colour for any characters they wish to be shown as box characters. This meant that blinking text would not be available, because the high bit is otherwise used for that, but I was happy. I never liked blinking text anyway.
Interpreting keyboard events on either platform is a giant screaming nightmare on stilts. Under DOS, the BIOS scancodes are so illogically allocated, they might as well be random numbers. Under Linux, the terminal has to be specially prepared and then, the function keys generate strings of bytes which need to be converted to scancodes through a lookup table.
It was almost inconceivable to convert Linux key events to DOS, or vice versa. I decided instead, to produce a pure key function, which would report a key value which is unaffected by control or alt, but will be shifted if shift is pressed. Programmers wishing to use a ctrl- or alt-key combination for a hot key could examine the keyboard status word seperately.
You might hope that a two-byte BIOS scancode would use the high byte as a key ID which never varies and the low byte for an ASCII value which depends on whether shift, control or alt are pressed. Unfortunately, because of a need to maintain compatability with the old XT keyboard, the high byte varies as much much as the low byte. What is worse, different keys react differently to control and alt. To avoid a time-consuming switch block, I produced a tangle of "if" tests to sieve the identities out of ctrl'd and alt'd scancodes.
Then I found that holding down shift reverses the sense of the numlock under DOS, but not Linux. I had to complicate my key purifier still further to undo that stupidity, so numlock means numbers, no matter what. So DOS was conquered and I faced the horror of the Linux keyboard.
In its default state, the Linux keyboard is far from suitable for an interactive program. The fgetc() function does not return until return is pressed, then it returns a whole string at once, so moving the cursor with arrows can't work. It echoes characters to the screen and ctrl-z, ctrl-q and ctrl-s all generate interrupts. It's a nightmare.
I had hoped I could avoid using fgetc() and slip the keyboard into raw mode (pure scancodes), but the gpm mouse driver offered me no choice. It provides a single function to read events from both keyboard and mouse, and the keyboard part uses fgetc(stdin). There is a mouse-only polling function, but I couldn't make it work.
I am glad of that now, because I have realised since that fgetc() receives high-level keycodes which are likely to be the same on foreign keyboards, where the layout and probably the scancodes would be different. I resigned myself to translating strings of bytes into scancodes as a necessity and it turned out to be easier than dealing with BIOS scancodes under DOS had been.
I found out how to set up the terminal by examining the Slang sources. You use a function called tcsetattr() to set flags and values in a terminal control structure. So I fixed the keyboard to return characters immediately without echo and to treat ctrl-z, ctrl-q and ctrl-s as ordinary keys.
I still had no kbhit() function, nor any way to read the shift state (whether ctrl, alt or shift are pressed). Google turned up an article at Linux Gazette called "Taming the Linux Keyboard", which gave me both those functions, full source code.
Still one bugbear remained. It may seem trivial to you but it was everything to me. It seemed insurmountable and I don't mind admitting, it nearly broke me.
You know how on DOS editors you can select text by holding down shift while using cursor-movement keys, including page up and page down? Well under Linux, shift-PageUp and shift-PageDown are reserved for a pointless function called scrollback. That means applications receive nothing from fgetc() when shift-PageUp/Down are pressed. The kernel spirits these keys away and your program never sees them.
But that is not the worst of it by a long chalk. After weeks of brain-busting work I found out at the final furlong, that if a user tries to select text with shift-PageUp, half my lovely colour text screen disappears - scrolled back!
There was no way I could release my library now. I felt like I had read a thousand-page novel and found the last page missing. I went round and round in circles of man pages and info files and searched the net to no avail. Then I noticed that the shift_state() function I got from that article I mentioned earlier, used a function called ioctl() to work its magic.
I used "apropos ioctl" to search the man pages and found one called "console_ioctls". There I discovered that ioctl() is the Linux equivalent of a DOS interrupt call. The same page gave a full list of low-level system calls and a warning from a kernel programmer, never to use these because they are not guaranteed and are subject to change in future versions of the kernel.
But we all know we can ignore kernel programmers when they say things like that. They are just denying responsibility, like when Scotty tells Captain Kirk it's going to take twice as long as it really will.
In the list I found one to change the functions associated with keys - including PageUp and PageDown. It involved filling a struct with three integers, to indicate which table, which key and which command to assign. The problem was, there were no docs telling me what these numbers should be, to disable scrollback for shift-PageUp.
Further research turned up the kbd package, which contains great docs and a bunch of utilities for changing the key mapping. You can dump the current mapping to stdout by running dumpkeys. Here is an excerpt from my dumpkeys output. Notice that it only gives me one of the three numbers I need - the keycode.
keycode 103 = Up alt keycode 103 = KeyboardSignal keycode 104 = Prior shift keycode 104 = Scroll_Backward keycode 105 = Left alt keycode 105 = Decr_Console keycode 106 = Right alt keycode 106 = Incr_Console keycode 107 = Select keycode 108 = Down keycode 109 = Next shift keycode 109 = Scroll_Forward keycode 110 = Insert
If you redirect the output into a text file, you can edit it and pass it to loadkeys to alter the mapping. Experiments revealed that you can delete most of the file - only leaving the keys you want to change. So I reduced it to two lines:
shift keycode 104 = Scroll_Backward shift keycode 109 = Scroll_Forward
and changed the current functions to the ones for those keys without shift pressed:
shift keycode 104 = Prior shift keycode 109 = Next
I called the file kmap and ran "loadkeys kmap". Then I tried my test program and found that scrollback had been disabled - exactly the result I was looking for. I knew now that it was possible. A peek at the source for loadkeys revealed that it used the ioctl I had found, to change the key functions, but I still did not know what numbers to use.
I had no choice but to use cunning. I found out that loadkeys has a -m option, to produce a source file, which contains tables of 256 values. I ran "loadkeys -m kmap" and found it produced one table with 254 null values and two non-null. Counting elements I found that the non-null elements were numbered 104 and 109 - the key codes in my kmap file. The values in the table had to be the values of the "Prior" and "Next" commands.
I also saw that this table had a number. I tried changing "shift" to "control" in one of the lines in kmap and got two tables, one for shift and one for control. In both cases the shift table was table number 1. Along with the actual values in the table, I had my three numbers.
To disable scrollback and scroll forward and make shift-PageUp/Down into ordinary keys, you must save the existing values, then change them and install an exit routine to restore them to normal function afterwards.
If you want to disable any key, such as the console switching keys for example, you will need to mess about like I did with "loadkeys -m" to find the numbers you are looking for.
This function changes a key's action and saves the old one in an integer you pass in by reference (written for gcc):
(text version of all listings)
#include <sys/ioctl.h> #include <linux/kd.h> #include <linux/keyboard.h> #include <stdio.h> int set_kb_entry( unsigned short table, unsigned short keycode, unsigned short value, unsigned short *oldvalue ) { struct kbentry ke; ke.kb_table = table; ke.kb_index = keycode; /* Get old value, return error if table or keycode are duff */ if( ioctl( fileno(stdin), KDGKBENT, &ke ) ) return -1; /* Unless oldvalue ptr is NULL, save old value to restore later */ if( oldvalue ) *oldvalue = ke.kb_value; /* The new action for this key */ ke.kb_value = value; /* Do the business, return error if value is duff */ if( ioctl( fileno(stdin), KDSKBENT, &ke ) ) return -1; return 0; }
To use the above function to disable scrollback and restore it on exit:
#include <stdlib.h> /* Old key action values will be stored in these */ unsigned short scroll_forward = 0; unsigned short scroll_backward = 0; /* The magic numbers gleaned from dumpkeys and loadkeys -m */ #define SHIFT_TABLE 1 #define PAGE_UP_KEYCODE 104 #define PAGE_DOWN_KEYCODE 109 #define PAGE_UP_ACTION 0x0118 /* Prior */ #define PAGE_DOWN_ACTION 0x0119 /* Next */ /* Restore default funcs for shift-PageUp and shift-PageDown */ static void restore_scrollback() { if( scroll_backward ) set_kb_entry( SHIFT_TABLE, PAGE_UP_KEYCODE, scroll_backward, 0 ); if( scroll_forward ) set_kb_entry( SHIFT_TABLE, PAGE_DOWN_KEYCODE, scroll_forward, 0 ); } /* Liberate shift-PageUp and shift-PageDown for normal use */ int disable_scrollback() { if( set_kb_entry( SHIFT_TABLE, PAGE_UP_KEYCODE, PAGE_UP_ACTION, &scroll_backward ) ) return -1; if( set_kb_entry( SHIFT_TABLE, PAGE_DOWN_KEYCODE, PAGE_DOWN_ACTION, &scroll_forward ) ) return -1; atexit( restore_scrollback ); return 0; }
So I emerged from the dark underworld of the Linux console, prizes in hand, triumphant. I have made it possible for programmers to write console apps which behave exactly the same under DOS and Linux and (I think) secured my place in legend.
And you know what? I never did write that text editor. I can't because I am homeless and I was lucky to get access to a computer long enough to do this little thing. Perhaps that is where you come in.
Linux is a virgin territory, about to be colonized by the people of India and Africa. They can't afford flash computers that can run X, so they need console apps. Now even those of you who don't have Linux installed can help them.
Linux needs pioneers to carve out the infrastructure before the first big wave of settlers can move in. Those settlers will need configuration dialogs for common apps like Apache and for common filters like grep. They will need a good text editor, with a right-click cut-copy-paste menu.
Programmers who mean to produce these tools will need a widget library and especially, a file Open/Save dialog. They would benefit from a well-written string array class with cut-copy-paste functions, provided separately to be used in various, competing text editors.
The perfect editor wouldn't have many features, but would have a simple facility for adding functions to its menus. It would be set up so that any fool could write a C++ function which takes a pointer to an editor as an argument and add that function to the editor's menu, just by adding a single line to main(). Programmers could swap C++ editor functions with eachother and we would be on course to the ultimate editor.
Will you be a pioneer? If no-one bothers, I fear that Linux may fall and we may all end up the helpless playthings of the evil Darth Gates. So I am hoping you will pick up my fallen standard. You may be our last, our only hope. Good luck.
May the Source be with you.
Slang, by John E. Davis. Slang is easy to rob because it is well-written. I learned how to init the keyboard and got most of the command strings for the screen from the Slang sources. I got other command strings by using the untic program that comes with it. But the best thing about Slang is what enables Midnight Commander to run in a telnet window. Anyone who has ever had to fix a web server remotely will know, it's a beautiful thing.
So You Like Color !!! By Pradeep Padala (LG #65). This article got me started on the Linux console screen.
Taming The Linux Keyboard By Petar Marinov (LG #76). My shift_status() and key_awaits() functions are modified versions of shift_state() and kbhit() given away with this article.
Ralf Brown, Patron Saint of DOS programmers
Stephen is a homeless Englishman who lives in a tent in the woods. He eats out
of bins and smokes cigarette butts he finds on the road. Though he once worked
for a short time as a C programmer, he prefers to describe himself as a "keen
amateur".