The star-making machine is an interesting visualization of a pipeline, but maybe it would be better to use more of the available screen real-estate.
For something a bit different, try running some text through raspipe.py
:
shuf /usr/share/dict/words | head -500 | ./raspipe.py
With raspipe.py running, try clicking anywhere in the window and the display should switch to something a little more abstract:
You can click again to switch back to the text display.
The script itself reads a lot like machine_stars.py
, but there are a few key differences. Open it up with Nano and let's have a look at some of the important ones (if you'd rather view it in a browser, you can find the code on GitHub):
nano raspipe.py
The first thing you'll probably notice is that there're a couple of extra imports:
#!/usr/bin/env python import getopt import re import sys import pygame
The re
module, which you might remember from stars.py
, will let us match on regular expressions. We use this to let the user optionally specify a pattern to highlight. More about that when we get to the penguin.
The getopt
module will let us easily parse some command-line options.
Next, instead of just writing a series of procedural statements in order, I wrote RasPipe as a class. An outline of its structure looks something like this:
class RasPipe: # Some properties are here. def __init__(self, infile): """Create a RasPipe object for a given input file.""" def setfont(self, size): """Set a display font of a given size.""" def wait_for_click(self): """Wait for the user to click on the screen (or quit).""" def toggle_stars(self): """Toggle whether we should display stars.""" def scale_display(self): """Set the current font size and delay based on amount of input.""" def penguin_think(self, thought): """Have a penguin appear and think a thought.""" def run(self): """Process standard input."""
Python, like most modern languages, provides support for a technique known as Object Oriented Programming, often abbreviated OOP or OO.
OO can be a very deep topic, but at heart it isn't as complicated as it's often made out to be. The short version is that it offers a way to bundle together state (variables, often called properties when they're part of an object) with behavior (functions, often called methods when they're part of an object).
The RasPipe class describes an object. You can think of an object as a sort of box around a collection of variables (properties) and functions (methods). Here's a very simple Python class and some code which gets an object (an instance of this class) and calls one of its methods:
#!/usr/bin/env python class HelloSayer: name = "" def __init__(self, name): self.name = name def speak(self): print("Hello " + self.name) s = HelloSayer("Ralph") s.speak()
def foo(param1, param2:
is how you define a function in Python. The first parameter to object methods in Python is always the object instance itself. This is how objects refer to their own properties and call their own methods.
__init__()
is a special method that gets called when we create an object instance with something like s = HelloSayer("Ralph")
.
RasPipe works just the same, with more properties and methods.
Most of what's in raspipe.py
is a class definition, but if you look at the bottom you'll see a few lines that turn it into a standalone utility program and let it take some useful options:
# Handle running this file as a standalone script. if __name__ == '__main__': rp = RasPipe(sys.stdin) opts, args = getopt.getopt(sys.argv[1:], 'sx:y:') for opt, arg in opts: if opt == '-x': rp.size[0] = (int(arg)) if opt == '-y': rp.size[1] = (int(arg)) if opt == '-r': rp.grep_for = arg if opt == '-s': rp.toggle_stars() rp.run()
The if __name__ == '__main__':
bit is a little magic that lets us check whether the current file is being run as the main Python program. (This way, other programs can include and reuse the RasPipe class for their own purposes - we'll get to that in Part 2.)
With rp = RasPipe(sys.stdin)
we create an instance of RasPipe with self.infile
set to sys.stdin
. (To see everything that happens here, have a look at the __init__()
defined at the top of the class.)
Next, we use the getopt module to see if the user passed in any command line arguments. This lets us do the following:
-s |
Start with stars enabled instead of scrolling text. |
-x 640 |
Start with a screen 640 pixels wide. |
-y 480 |
Star with a screen 480 pixels tall. |
-r '.*foo.*' |
Have a penguin appear onscreen thinking any line that matches the regular expression |
We could easily add other options here. As an exercise, you might try adding a feature to change a property like bgcolor
from the command line.
Finally, we call rp.run()
, which is the method that does most of the heavy lifting.
Suppose you have a lot of output streaming through raspipe.py
, and want to highlight lines that match a particular pattern? Normally, you'd use a tool like grep
to accomplish this.
Fortunately, Python supports its own variety of regular expressions, so it's pretty easy to give our program its own simple version of grep-like features. For example, if I wanted to check the output of dmesg
for things about ethernet interfaces, I could do:
dmesg | ./raspipe.py -r '.*eth0.*'
And see someting like the following:
This is done inside run()
by checking each line to render against a regular expression:
for render_line in to_render: # Show a penguin thinking if we have a regular expression to # search for and found a match: if self.grep_for is not None: if re.match(self.grep_for, render_line): matched_line = render_line
And later calling a special method to draw the penguin and its thought:
if matched_line is not None: self.penguin_think(matched_line)
That function looks like this:
def penguin_think(self, thought): """Have a penguin appear and think a thought.""" r = self.penguin.get_rect(left=50, bottom=self.height) self.screen.fill(self.fgcolor, [0, 10, self.width, r.top]) self.screen.blit(self.penguin, r) thought_surface = self.font.render(thought, True, self.bgcolor) thought_r = thought_surface.get_rect(left=5, top=30) self.screen.blit(thought_surface, thought_r) pygame.display.update()
Pygame offers a way to process all sorts of user input.
In this case, we only really deal with one interaction: When the user clicks on the screen, we toggle the display between a scrolling-text mode and a mode that displays little star images. To see how this is done, have a look at the first few lines of run()
:
def run(self): """Process standard input.""" line = self.infile.readline() while line: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() if event.type == pygame.MOUSEBUTTONDOWN: self.toggle_stars()
Just like in machine_stars.py
, we're looping over the contents of standard input. Here, however, we also check the Pygame event loop and make a decision if there's been a mouse click (or if the window has been closed, which will give us a pygame.QUIT
).
You may have noticed this bit in __init()__
:
# A little bit of sound. pygame.mixer.init() self.click = pygame.mixer.Sound('./tick.wav')
...and later on in run()
:
if self.tick % self.display_lines == 0: self.click.play()
This just plays a clicking noise every so often. Pygame's mixer can be used to do all sorts of things with individual sound files, and pygame.mixer.music allows for a custom soundtrack.
Text editor powered by tinymce.