Have you ever wanted to write scripts for your linux terminal? Maybe you remember DOS modem programs that let you do this?
This lets you write python scripts to interact with your terminal session. Scripts can watch for certain input and then run automatically, run on a timer, or run when requested by you.
It's also just a simple terminal that's easy to modify if you're tired of bloated, cumbersome terminals with slow menus.
You can write your own menus, shortcuts and modify other aspects of the terminal's behavior from python. If you want custom behavior, a lot of it is implemented in python so it's easy to tweak.
I want to have my terminal cache my ssh passwords so I don't have to retype them over and over. I hit a key (^Q) when I'm at an ssh prompt and the script could handle it for me.
I'd also like to have the brightness change with the time of day. That'll be easy to do with a script as well.
I've added a list of ideas (Script ideas) below but I'm sure I'm just scratching the surface with this.
You'll want a unix/X11 system with a lot of libraries and headers to build this.
I should make a list for raspbian (TODO). For a start, you'll want python development, X11, Xext, Xft (freetype2) and fontconfig. If you have a problem compiling, shoot me a message.
make
If you just compiled it and just want to try it out:
./xapterm -nobc user
If you want to install it:
mkdir ~/.config/xapterm
cp *.py ~/.config/xapterm
./xapterm -- -h
./xapterm
Usage
xapterm [-nobc] [-nopy] [-stderr] [-h] [scriptname] [python args...]
-nobc disables python's __pycache__ directory and compilation caching.
-nopy disables python support and saves some memory. Some functions will still work, using the alternate cscript.c code.
-stderr preserves the default stderr function and sends python output to the caller.
The default behavior is to trap python's output and show it in the terminal window so the user can see it. Otherwise the output might never reach the user.
This prints some basic command line help.
By default, xapterm will look for user.py in ~/.config/xapterm and /usr/share/xapterm.
An alternate script can be specified here.
Optional arguments can be provided here to be read by the python script. The default script supports a number of arguments.
These arguments are read by the default user.py script. They can be overwritten and modified by the user in ~/.config/xapterm/user.py.
Running xapterm -- -h will ask user.py for help instead of main.c.
This will run fastbegin() which bypasses a lot of startup code and uses explicit settings.
This will run dfastbegin() which is like -fast.
This will attempt to go fullscreen. X11 servers are picky about this so it might not work perfectly everywhere.
Start in a dark color palette.
Start in a light color palette.
By default the cursor blinks. This doesn't.
By default, bash is used. This uses lighter-weight dash instead.
Sensible answers for X are ansi and linux. Other values will work too.
For me, monospace, freemono, liberationmono, notomono, droidsansmono100 and OpenDyslexicMono all seem to work for X.
Different fonts fit differently. You can try slightly larger or smaller numbers to get a fit you like more. I've used Monospace with size 17 and been happy with it. I've also liked OpenDyslexicMono with size 15.
You can specify how tall you want the cursor. Note there are some escape sequences that will change it.
I like red for X. I find that works but you can change the color here.
Here are a few ideas of what can be done to extend the functionality with scripts. Back in the day I had scripts to play games for me (Tradewars!).
Included is passwords.py, a primitive password manager for ssh. This could be improved greatly.
It wouldn't be hard to calculate the appropriate brightness for the time of day and switch automatically from light mode to dark more. The colors can be modified from python so this isn't hard.
Incoming text can be watched from python. Common misspellings could be highlighted by a script.
Text that looks like URLs can be identified from scripts. They could be automatically copied to the clipboard from a script.
Most of the customization is done in user.py.
def OnInitBegin(*args)
OnInitBegin is the first procedure called. It can interact with the config module. This can not interact with the vte module as nothing has been created yet. You can use OnInitEnd for vte interaction.
OnInitEnd is the second procedure called, after data structures have been created. At this point, the python code can start interacting with the vte and texttap modules.
There are a number of callbacks here:
This is called when the user suspends the session, for example with ctrl-s.
This is called with the user requests to resume the session, for example with ctrl-q.
This is called when the session is paused and the user presses a control key combination.
This allows the script to get input from the user.
This is called when the session is paused and the user presses a normal key.
This allows the script to get input from the user.
etype: 1 => button press, 2 => pointer movement, 3 => click, 4 => button release mods (bitmask): &1 => shift is down, &2 => control, &4 => alt, &8 => numlock, &16 => appmode, &32 => scrollback mode, &64 => mouse is grabbed button: 1 => left, 2 => middle, 3=> right, ...
This is called when the mouse is used but the use wasn't caught by the main code. There will be more events when the session is paused.
This is called when the BEL (0x07) character is received. Older terminals used to beep. You could have python play a sound but the current code flashes the screen. The flash can be customized here.
t: The time requested This is called when a user-specified timer expires. This allows the code to request a callback in the future.
msg: text This is called when an in-line message is received, e.g. a DCS like ESC+P+...+ESC+\ will send the P+... to OnMessage().
When a special key is released, see OnKeySym
key: of the form 0xff51, which is left cursor mods (bitmask): &1 => shift is down, &2 => control, &4 => alt, &8 => numlock, &16 => appmode, &32 => scrollback mode, &64 => mouse is grabbed
This lets user.py send the control sequences for special characters. This allows full customization without recompiling. For example, if 0xff51 is pressed (with no modifiers), OnKeySym() sends ESC+[+D, an escape sequence for left. If we're in "app mode", we can see that in the mods variable and send ESC+[+OD instead.
The user code is notified when the window changes size. This allows the user to choose between a differently sized grid or differently sized fonts, or a combination.
There are three python modules for interacting with the terminal: config, vte, and texttap.
config lets python read and set basic configuration variables such as font size.
vte lets python interact with the terminal.
texttap lets python read characters or be notified when specified strings are received. This is an enormous security concern so it is separate from vte. The only way (without hacking python) for user.py to access texttap is by receiving it in OnInitEnd and passing it to code that uses it.
See below for a list of the modules.
config.cmdline holds the command line
config.darkmode holds the dark mode colors
config.lightmode holds the light mode colors
config.exportterm holds the TERM value
config.typeface holds the font typeface
config.windims holds the window dimensions
config.celldims holds the size of a single character's cell size
config.columns holds the number of columns
config.rows holds the number of rows
config.cursorheight holds the height of the cursor
config.cursoryoff holds the y-offset for the cursor
config.font0shift holds the x-offset for drawing fonts
config.font0line holds the y-offset for drawing fonts
config.fontulline holds the y-offset for underlining fonts
config.fontullines holds the thickness of underlines
config.charcache holds the number of drawn characters to cache so they don't have to be drawn each time
config.depth holds the X11 color depth
config.rgb_cursor holds the (r,g,b) tuple for the cursor color
config.isfullscreen holds the boolean value of whether fullscreen was requested
config.isdarkmode holds the boolean value if we're in dark mode (false => light mode)
config.isblinkcursor holds the boolean value if cursor is blinking
config.isnostart holds the boolean value of true if we're not going to start the terminal (e.g. just show help)
config.screendims holds the dimensions of the x11 screen in pixels
config.mm_screendims holds the dimensions of the x11 screen in millimeters
Set the current configuration. Changes to config variables won't be used until this is called.
This returns (ismatch,ascent,descent,width) as the information found about fontname.
You can try config.queryfont("monospace-17") as an example.
This is safe to ignore. It always returns true currently.
Clears the screen
vte.clearlines() Clear the current line
vte.clearlines(row) Clear the line at row
vte.clearlines(row,count) Clear lines starting at row and count lines from there
vte.copy(x) copy x to the (PRIMARY) clipboard
vte.copy(x,clipboard) copy x to the (clipboard) clipboard
vte.cursorheight() reset cursor height
vte.cursorheight(h) set cursor height to h pixels
vte.drawtoggle() toggles drawing state
vte.drawtoggle(1) enabled drawing
vte.drawtoggle(0) disables drawing
This is useful if you want the terminal to quickly process incoming text without slowing down to draw to the screen. You can disable drawing and then re-enable it a few seconds later.
This draws string to the screen at the current cursor position.
vte.fillpadding(color)
vte.fillpadding(color,ms)
Draws color index in the window padding and waits ms milliseconds afterward.
There's often a small gap between the edge of the text screen and the x11 window. This happens because the window isn't usually an integral size of the font size. We can draw in this gap for visual bells and similar effects.
vte.fillrect(x,y,width,height)
vte.fillrect(x,y,width,height,color)
vte.fillrect(x,y,width,height,color,ms)
This fills the rectangle at x,y with width and height with an optional color index (or current color) and optional sleep of ms milliseconds.
This returns a line from the screen at row, col and count characters. If the span exceeds the screen, this will return an error code.
This returns the position of the last character drawn by the terminal as a (row,col) pair.
vte.grabpointer(1) This grabs the pointer
vte.grabpointer(), vte.grabpointer(0) This releases the pointer
If the pointer is grabbed, more mouse events are sent to the python script. When it isn't grabbed, a lot of events are either handled or not sent.
This allows the script to get user input from the mouse. Note that the mods variable in python callbacks will have the grabbed bit set.
This returns true if the session is currently paused.
This returns a count of milliseconds. It grows monotonically and lets the script measure elapsed time.
vte.moveto() This moves the cursor to the top left.
vte.moveto(row) This moves the cursor to row
vte.moveto(row,col) This moves the cursor to row and col
vte.movewindow() This moves the window to the top left.
vte.movewindow(x) This moves the window to (x,0)
vte.movewindow(x,y) This moves the window to (x,y)
vte.paste() This pastes text from the (PRIMARY) clipboard.
vte.paste(clipboard) This pastes text from the clipboard clipboard.
This pauses the terminal. See vte.unpause().
This restores the drawn screen from a backup. See vte.savetext().
This forces a redraw of the screen (x,y,width,height)
I'm not sure what this is useful for but it's here.
This saves the drawn screen to a backup. See vte.restoretext().
This lets you overdraw the screen and then undo your changes.
This scrolls back n lines.
vte.select(row_start,col_start,row_stop,col_stop) This copies a portion of the screen to the (PRIMARY) clipboard.
vte.select(row_start,col_start,row_stop,col_stop,clipboard) As above but to clipboard clipboard.
Sends chars as if they were typed.
Sets an alarm for seconds seconds. OnAlarm will be called in user.py.
vte.setcolors() This sets the foreground and background colors to defaults.
vte.setcolors(fore) This sets the foreground color to fore color index.
vte.setcolors(fore,back) This sets the foreground and background colors.
This enables the cursor.
vte.setpointer() Sets the mouse pointer to default
vte.setpointer(index) Sets the mouse pointer to X11 number index.
You can easily write a script to cycle through the possible index values. I find 60 is a nice value.
Sets the window title to text.
vte.setunderline(0) Disables
vte.setunderline(1) Enables underlining in the current style
This sends text to the terminal's stderr output.
This sends text to the terminal's stdout output.
This returns the seconds since the epoch.
See vte.pause(), this unpauses the session.
This hides the cursor. See vte.setcursor().
These are useful when interacting with the user.
vte.visualbell() Uses color 0 and 150 ms.
vte.visualbell(color) Uses color color and 150 ms
vte.visualbell(color,ms) Uses color color and ms milliseconds
This flashes the screen. It's obnoxious. You're better off writing your own with the functions available.
Without a parameter, it uses 0 percent.
This calls XBell() in the X11 server but on modern systems, this doesn't do anything.
If you actually want to make sound, you might be better off playing a wave file (or similar) via python.
Set a callback for a specified input.
object.OnLook(id) will be called when match is found.
If match is empty, everything will match.
If object is destroyed, the tap will be deleted.
This removes the tap created by addlook().
This is the main python script. All callbacks go through here.
This is a small manager for alarms. The terminal only supports one alarm at a time, so this code sequences multiple alarms and does the work to support multiple alarms.
This has a Menu class as well as MainMenu, BrightnessMenu and FontMenu. They can all be edited by the user.
This has some helper functions for drawing and debugging.
This is a work-in-progress password manager. It can be easily disabled in user.py if it's not wanted.