Commits

J.A. Roberts Tunney committed 7ed6516

cleaned up a lot of code and started writing documentation

Comments (0)

Files changed (15)

+Copyright (c) 2010 J.A. Roberts Tunney
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 Here's how Fabulous compares to other similar libraries:
 
-- fabulous_: Licensed BSD.  Focuses on delivering useful features in
+- fabulous_: Licensed MIT.  Focuses on delivering useful features in
   the simplest, most user-friendly way possible (without a repulsive
   name.)  Written in pure-python but will attempt to auto-magically
   compile/link a speedup library.  ~1,000 lines of code.
+/**
+ * Optimized Code For Quantizing Colors to xterm256
+ *
+ * These functions are equivalent to the ones found in xterm256.py but
+ * orders of a magnitude faster and should compile quickly (fractions
+ * of a second) on most systems with very little risk of
+ * complications.
+ *
+ * Color quantization is very complex.  This works by treating RGB
+ * values as 3D euclidean space and brute-force searching for the
+ * nearest neighbor.
+ */
 
 typedef struct {
         int r;
         int b;
 } rgb_t;
 
-int CUBE_STEPS[] = {0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF};
-rgb_t BASIC16[] = {{0, 0, 0}, {205, 0, 0}, {0, 205, 0}, {205, 205, 0},
-                   {0, 0, 238}, {205, 0, 205}, {0, 205, 205}, {229, 229, 229},
-                   {127, 127, 127}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0},
-                   {92, 92, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255}};
+int CUBE_STEPS[] = { 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF };
+rgb_t BASIC16[] = { {   0,   0,   0 }, { 205,   0,   0}, {   0, 205,   0 },
+		    { 205, 205,   0 }, {   0,   0, 238}, { 205,   0, 205 },
+		    {   0, 205, 205 }, { 229, 229, 229}, { 127, 127, 127 },
+		    { 255,   0,   0 }, {   0, 255,   0}, { 255, 255,   0 },
+		    {  92,  92, 255 }, { 255,   0, 255}, {   0, 255, 255 },
+		    { 255, 255, 255 } };
 rgb_t COLOR_TABLE[256];
 
 
         return res;
 }
 
-int rgb_to_xterm_i(int xcolor)
+/**
+ * This function provides a quick and dirty way to serialize an rgb_t
+ * struct to an int which can be decoded by our Python code using
+ * ctypes.
+ */
+int xterm_to_rgb_i(int xcolor)
 {
         rgb_t res = xterm_to_rgb(xcolor);
         return (res.r << 16) | (res.g << 8) | (res.b << 0);
 }
 
-inline int sqr(int x)
-{
-        return x * x;
-}
+#define sqr(x) ((x) * (x))
 
+/**
+ * Quantize RGB values to an xterm 256-color ID
+ */
 int rgb_to_xterm(int r, int g, int b)
 {
         int best_match = 0;
         int smallest_distance = 1000000000;
         int c, d;
         for (c = 16; c < 256; c++) {
-                /* euclidean distance */
                 d = sqr(COLOR_TABLE[c].r - r) +
                     sqr(COLOR_TABLE[c].g - g) +
                     sqr(COLOR_TABLE[c].b - b);
 int init()
 {
         int c;
-        for (c = 0; c < 256; c++)
-                COLOR_TABLE[c] = xterm_to_rgb(c);
+        for (c = 0; c < 256; c++) {
+		COLOR_TABLE[c] = xterm_to_rgb(c);
+	}
         return 0;
 }
 # -*- coding: utf-8 -*-
+"""
+    fabulous.color
+    ~~~~~~~~~~~~~~
+
+    I implement support for standard 16-color color terminals.
+
+"""
 
 import sys
 import functools
     r"""A colorized string-like object that gives correct length
 
     If anyone knows a way to be able to make this behave like a string
-    object without creating a bug minefield let me know.
+    object without creating a bug minefield let me know::
 
-    >>> str(red("hello"))
-    '\x1b[31mhello\x1b[39m'
-    >>> len(red("hello"))
-    5
-    >>> len(str(red("hello")))
-    15
-    >>> str(bold(red("hello")))
-    '\x1b[1m\x1b[31mhello\x1b[39m\x1b[22m'
-    >>> len(bold(red("hello")))
-    5
-    >>> len(bold("hello ", red("world")))
-    11
+        >>> str(red("hello"))
+        '\x1b[31mhello\x1b[39m'
+        >>> len(red("hello"))
+        5
+        >>> len(str(red("hello")))
+        15
+        >>> str(bold(red("hello")))
+        '\x1b[1m\x1b[31mhello\x1b[39m\x1b[22m'
+        >>> len(bold(red("hello")))
+        5
+        >>> len(bold("hello ", red("world")))
+        11
+
     """
     sep = ""
     fmt = "%s"
 
 
 def section(title, bar=OVERLINE, strm=sys.stdout):
+    """Helper function for testing demo routines
+    """
     width = utils.term.width
     print >>strm, bold(title.center(width)).as_utf8
     print >>strm, bold((bar * width)[:width]).as_utf8
+"""
+    fabulous.debug
+    ~~~~~~~~~~~~~~
+
+"""
 
 import sys
 import itertools
 
 
 class DebugImage(image.Image):
+    """Visualize Optimization Techniques Used By :class:`Image`
+    """
+
     def reduce(self, colors):
         need_reset = False
         line = ''
                 line += '<' + (self.pad * len(list(items)))[1:]
 
 
-if __name__ == '__main__':
+def main(args):
+    """I provide a command-line interface for this module
+    """
     for imgpath in sys.argv[1:]:
         for line in DebugImage(imgpath):
             print line
+
+
+if __name__ == '__main__':
+    main(sys.argv)

fabulous/experimental/canvas.py

+
+from __future__ import with_statement
+
+import time
+import curses
+
+
+class Canvas(object):
+    def __init__(self, encoding='UTF-8'):
+        self.encoding = encoding
+
+    def __enter__(self):
+        self.win = curses.initscr()
+        curses.start_color()
+        curses.init_color(200, 1000, 300, 0)
+        curses.init_pair(1, 200, curses.COLOR_WHITE)
+        return self
+
+    def __exit__(self, type_, value, traceback):
+        curses.endwin()
+
+    def __setitem__(self, xy, val):
+        self.win.attron(curses.color_pair(1))
+        (x, y) = xy
+        self.win.addch(x, y, val)
+
+
+if __name__ == '__main__':
+    import locale
+    locale.setlocale(locale.LC_ALL, '')
+    encoding = locale.getpreferredencoding()
+    with Canvas(encoding=encoding) as canvas:
+        canvas[5, 5] = 'Y'
+        canvas.win.refresh()
+        time.sleep(5.0)

fabulous/experimental/rotating_cube.py

-"""Completely pointless non-curses rotating cube
-
-Uses a faux 2D rendering technique to create a wireframe 3d cube.
-This doesn't use curses, it just prints whole, entire frames.
-"""
-
-from __future__ import with_statement
-
-import sys
-import time
-from math import cos, sin, pi
-
-from fabulous import color, utils
-
-
-class Frame(object):
-    def __enter__(self):
-        self.width = utils.term.width
-        self.height = utils.term.height * 2
-        self.canvas = [[' ' for x in range(self.width)]
-                       for y in range(self.height // 2)]
-        return self
-
-    def __exit__(self, type_, value, traceback):
-        sys.stdout.write(self.render())
-        sys.stdout.flush()
-
-    def __setitem__(self, p, c):
-        (x, y) = p
-        self.canvas[int(y // 2)][int(x)] = c
-
-    def line(self, x0, y0, x1, y1, c='*'):
-        r"""Draws a line
-
-        Who would have thought this would be so complicated?  Thanks
-        again Wikipedia_ <3
-
-        .. _Wikipedia: http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
-        """
-        steep = abs(y1 - y0) > abs(x1 - x0)
-        if steep:
-            (x0, y0) = (y0, x0)
-            (x1, y1) = (y1, x1)
-        if x0 > x1:
-            (x0, x1) = (x1, x0)
-            (y0, y1) = (y1, y0)
-        deltax = x1 - x0
-        deltay = abs(y1 - y0)
-        error = deltax / 2
-        y = y0
-        if y0 < y1:
-            ystep = 1
-        else:
-            ystep = -1
-        for x in range(x0, x1 - 1):
-            if steep:
-                self[y, x] = c
-            else:
-                self[x, y] = c
-            error = error - deltay
-            if error < 0:
-                y = y + ystep
-                error = error + deltax
-
-    def render(self):
-        return "\n".join(["".join(line) for line in self.canvas])
-
-
-def rotating_cube(degree_change=3, frame_rate=10):
-    """
-    1. Create two imaginary ellipses
-    2. Sized to fit in the top third and bottom third of screen
-    3. Create four imaginary points on each ellipse
-    4. Make those points the top and bottom corners of your cube
-    5. Connect the lines and render
-    6. Rotate the points on the ellipses and repeat
-    """
-    degrees = 0
-    while True:
-        t1 = time.time()
-
-        with Frame() as frame:
-            oval_width = frame.width
-            oval_height = frame.height / 3.0
-            cube_height = oval_height * 2
-
-            (p1_x, p1_y) = ellipse_point(degrees, oval_width, oval_height)
-            (p2_x, p2_y) = ellipse_point(degrees + 90, oval_width, oval_height)
-            (p3_x, p3_y) = ellipse_point(degrees + 180, oval_width, oval_height)
-            (p4_x, p4_y) = ellipse_point(degrees + 270, oval_width, oval_height)
-            degrees = (degrees + degree_change) % 360
-
-            # connect square thing at top
-            frame.line(p1_x, p1_y, p2_x, p2_y)
-            frame.line(p2_x, p2_y, p3_x, p3_y)
-            frame.line(p3_x, p3_y, p4_x, p4_y)
-            frame.line(p4_x, p4_y, p1_x, p1_y)
-
-            # connect top to bottom
-            frame.line(p1_x, p1_y, p1_x, p1_y + cube_height)
-            frame.line(p2_x, p2_y, p2_x, p2_y + cube_height)
-            frame.line(p3_x, p3_y, p3_x, p3_y + cube_height)
-            frame.line(p4_x, p4_y, p4_x, p4_y + cube_height)
-
-            # connect square thing at bottom
-            frame.line(p1_x, p1_y + cube_height, p2_x, p2_y + cube_height)
-            frame.line(p2_x, p2_y + cube_height, p3_x, p3_y + cube_height)
-            frame.line(p3_x, p3_y + cube_height, p4_x, p4_y + cube_height)
-            frame.line(p4_x, p4_y + cube_height, p1_x, p1_y + cube_height)
-
-        elapsed = (time.time() - t1)
-        time.sleep(abs(1.0 / frame_rate - elapsed))
-
-
-def ellipse_point(degrees, width, height):
-    """I hate math so much :'(
-    """
-    width -= 1
-    height -= 1
-    radians = degrees * (pi / 180.0)
-    x = width/2.0 * cos(1) * sin(radians) - width/2.0 * sin(1) * cos(radians)
-    y = height/2.0 * sin(1) * sin(radians) + height/2.0 * cos(1) * cos(radians)
-    x = int(x + width/2.0)
-    y = int(y + height/2.0)
-    return (x, y)
-
-
-if __name__ == '__main__':
-    rotating_cube()
+"""
+    fabulous.gotham
+    ~~~~~~~~~~~~~~~
 
+    I implement functions to satisfy your darker side.
+
+"""
+
+import sys
 import random
 
 
 def lorem_gotham():
+    """Cheesy Gothic Poetry Generator
+
+    When you need to generate random verbiage to test your code or
+    design, let's face it... Lorem Ipsum and "the quick brown fox" are
+    old and boring!
+
+    What we need is something with flavor, the kind of thing a
+    depressed teenager with a lot of black makeup would write.
+    """
     them = ['angels', 'mourners', 'shadows', 'storm clouds', 'memories', 'condemned'
             'hand of Heaven', 'stroke of death', 'damned', 'witches', 'corpses']
     them_verb = ['follow', 'hover close', 'approach', 'loom', 'taunt',
             sentence(punc(',', er(w(adj)),'than the usual',w(feeling)), er(w(adj)),'than',w(them),'in',w(place)),
             sentence(punc('!','oh my',w(me_part)),punc('!','the',w(feeling))),
             sentence('no one',s(w(angst)),'why the',w(them),w(them_verb + me_verb)))
+
+
+def main(args):
+    """I provide a command-line interface for this module
+    """
+    print lorem_gotham()
+
+
+if __name__ == '__main__':
+    main(sys.argv)
-"""Print Images to a 256-Color Terminal
+"""
+    fabulous.image
+    ~~~~~~~~~~~~~~
+
 """
 
 import sys
 import itertools
-import textwrap
 
-from grapefruit import Color
+import grapefruit as gf
 
 from fabulous import utils, xterm256
 
 
 class Image(object):
+    """Printing image files to a terminal
+
+    I use :mod:`PIL` to turn your image file into a bitmap, resize it
+    so it'll fit inside your terminal, and implement methods so I can
+    behave like a string or iterable.
+
+    When resizing, I'll assume that a single character on the terminal
+    display is one pixel wide and two pixels tall.  For most fonts
+    this is the best way to preserve the aspect ratio of your image.
+
+    All colors are are quantized by :mod:`fabulous.xterm256` to the
+    256 colors supported by modern terminals.  When quantizing
+    semi-transparant pixels (common in text or PNG files) I'll ask
+    :class:`TerminalInfo` for the background color I should use to
+    solidify the color.  Fully transparent pixels will be rendered as
+    a blank space without color so we don't need to mix in a
+    background color.
+
+    I also put a lot of work into optimizing the output line-by-line
+    so it needs as few ANSI escape sequences as possible.  You can use
+    :class:`DebugImage` to visualize these optimizations.
+    """
+
     pad = ' '
 
     def __init__(self, path, width=None):
         utils.pil_check()
-        from PIL import Image as Pills
-        self.img = Pills.open(path)
+        from PIL import Image as PillsPillsPills
+        self.img = PillsPillsPills.open(path)
         self.resize(width)
 
     def __str__(self):
                 elif len(rgba) == 3 or rgba[3] == 255:
                     yield xterm256.rgb_to_xterm(*rgba[:3])
                 else:
-                    color = Color.NewFromRgb(*[c / 255.0 for c in rgba])
-                    rgba = color.AlphaBlend(utils.term.bgcolor).rgb
-                    yield xterm256.rgb_to_xterm(*[int(c * 255.0) for c in rgba])
+                    color = gf.Color.NewFromRgb(*[c / 255.0 for c in rgba])
+                    rgba = gf.Color.AlphaBlend(color, utils.term.bgcolor).rgb
+                    yield xterm256.rgb_to_xterm(
+                        *[int(c * 255.0) for c in rgba])
             yield "EOL"
 
 
-if __name__ == '__main__':
-    for imgpath in sys.argv[1:]:
+def main(args):
+    """I provide a command-line interface for this module
+    """
+    for imgpath in args[1:]:
         for line in Image(imgpath):
             print line
+
+
+if __name__ == '__main__':
+    main(sys.argv)
+"""
+    fabulous.logs
+    ~~~~~~~~~~~~~
+
+    I provide utilities for making your logs look fabulous.
+
+"""
 
 import sys
 import logging
 
 
 class TransientStreamHandler(logging.StreamHandler):
+    """Standard Python logging Handler for Transient Console Logging
+
+    Logging transiently means that verbose logging messages like DEBUG
+    will only appear on the last line of your terminal for a short
+    period of time and important messages like WARNING will scroll
+    like normal text.
+
+    This allows you to log lots of messages without the important
+    stuff getting drowned out.
+
+    This module integrates with the standard Python logging module.
+    """
+
     def __init__(self, strm=sys.stderr, level=logging.WARNING):
         logging.StreamHandler.__init__(self, strm)
-        self.levelno = level if isinstance(level, int) else logging._levelNames[level]
+        if isinstance(level, int):
+            self.levelno = level
+        else:
+            self.levelno = logging._levelNames[level]
         self.need_cr = False
         self.last = ""
         self.parent = logging.StreamHandler
 
 
 def basicConfig(level=logging.WARNING, transient_level=logging.NOTSET):
+    """Shortcut for setting up transient logging
+
+    I am a replica of ``logging.basicConfig`` which installs a
+    transient logging handler to stderr.
+    """
     fmt = "%(asctime)s [%(levelname)s] [%(name)s:%(lineno)d] %(message)s"
-    logging.root.setLevel(transient_level) # <--- IMPORTANT
+    logging.root.setLevel(transient_level)  # <--- IMPORTANT
     hand = TransientStreamHandler(level=level)
     hand.setFormatter(logging.Formatter(fmt))
     logging.root.addHandler(hand)

fabulous/rotating_cube.py

+"""
+    fabulous.rotating_cube
+    ~~~~~~~~~~~~~~~~~~~~~~
+
+    Completely pointless terminal renderer of rotating cube
+
+    Uses a faux 2D rendering technique to create what appears to be a
+    wireframe 3d cube.
+
+    This doesn't use the curses library, but rather prints entire
+    frames sized to fill the entire terminal display.
+
+"""
+
+from __future__ import with_statement
+
+import sys
+import time
+from math import cos, sin, pi
+
+from fabulous import color, utils
+
+
+class Frame(object):
+    """Canvas object for drawing a frame to be printed
+    """
+
+    def __enter__(self):
+        self.width = utils.term.width
+        self.height = utils.term.height * 2
+        self.canvas = [[' ' for x in range(self.width)]
+                       for y in range(self.height // 2)]
+        return self
+
+    def __exit__(self, type_, value, traceback):
+        sys.stdout.write(self.render())
+        sys.stdout.flush()
+
+    def __setitem__(self, p, c):
+        (x, y) = p
+        self.canvas[int(y // 2)][int(x)] = c
+
+    def line(self, x0, y0, x1, y1, c='*'):
+        r"""Draws a line
+
+        Who would have thought this would be so complicated?  Thanks
+        again Wikipedia_ <3
+
+        .. _Wikipedia: http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
+        """
+        steep = abs(y1 - y0) > abs(x1 - x0)
+        if steep:
+            (x0, y0) = (y0, x0)
+            (x1, y1) = (y1, x1)
+        if x0 > x1:
+            (x0, x1) = (x1, x0)
+            (y0, y1) = (y1, y0)
+        deltax = x1 - x0
+        deltay = abs(y1 - y0)
+        error = deltax / 2
+        y = y0
+        if y0 < y1:
+            ystep = 1
+        else:
+            ystep = -1
+        for x in range(x0, x1 - 1):
+            if steep:
+                self[y, x] = c
+            else:
+                self[x, y] = c
+            error = error - deltay
+            if error < 0:
+                y = y + ystep
+                error = error + deltax
+
+    def render(self):
+        return "\n".join(["".join(line) for line in self.canvas])
+
+
+def rotating_cube(degree_change=3, frame_rate=10):
+    """Rotating cube program
+
+    How it works:
+
+      1. Create two imaginary ellipses
+      2. Sized to fit in the top third and bottom third of screen
+      3. Create four imaginary points on each ellipse
+      4. Make those points the top and bottom corners of your cube
+      5. Connect the lines and render
+      6. Rotate the points on the ellipses and repeat
+
+    """
+    degrees = 0
+    while True:
+        t1 = time.time()
+
+        with Frame() as frame:
+            oval_width = frame.width
+            oval_height = frame.height / 3.0
+            cube_height = oval_height * 2
+
+            (p1_x, p1_y) = ellipse_point(degrees, oval_width, oval_height)
+            (p2_x, p2_y) = ellipse_point(degrees + 90, oval_width, oval_height)
+            (p3_x, p3_y) = ellipse_point(degrees + 180, oval_width, oval_height)
+            (p4_x, p4_y) = ellipse_point(degrees + 270, oval_width, oval_height)
+            degrees = (degrees + degree_change) % 360
+
+            # connect square thing at top
+            frame.line(p1_x, p1_y, p2_x, p2_y)
+            frame.line(p2_x, p2_y, p3_x, p3_y)
+            frame.line(p3_x, p3_y, p4_x, p4_y)
+            frame.line(p4_x, p4_y, p1_x, p1_y)
+
+            # connect top to bottom
+            frame.line(p1_x, p1_y, p1_x, p1_y + cube_height)
+            frame.line(p2_x, p2_y, p2_x, p2_y + cube_height)
+            frame.line(p3_x, p3_y, p3_x, p3_y + cube_height)
+            frame.line(p4_x, p4_y, p4_x, p4_y + cube_height)
+
+            # connect square thing at bottom
+            frame.line(p1_x, p1_y + cube_height, p2_x, p2_y + cube_height)
+            frame.line(p2_x, p2_y + cube_height, p3_x, p3_y + cube_height)
+            frame.line(p3_x, p3_y + cube_height, p4_x, p4_y + cube_height)
+            frame.line(p4_x, p4_y + cube_height, p1_x, p1_y + cube_height)
+
+        elapsed = (time.time() - t1)
+        time.sleep(abs(1.0 / frame_rate - elapsed))
+
+
+def ellipse_point(degrees, width, height):
+    """I hate math so much :'(
+    """
+    width -= 1
+    height -= 1
+    radians = degrees * (pi / 180.0)
+    x = width/2.0 * cos(1) * sin(radians) - width/2.0 * sin(1) * cos(radians)
+    y = height/2.0 * sin(1) * sin(radians) + height/2.0 * cos(1) * cos(radians)
+    x = int(x + width/2.0)
+    y = int(y + height/2.0)
+    return (x, y)
+
+
+if __name__ == '__main__':
+    rotating_cube()
+"""
+    fabulous.text
+    ~~~~~~~~~~~~~
+
+    I let you print TrueType text to your terminal.  The easiest way
+    to get started with me is by running::
+
+        jart@compy:~$ python -m fabulous.text --help
+
+    To make things simple, Fabulous comes with my favorite serif,
+    non-serif, and monospace fonts:
+
+    - IndUni-H-Bold: Open Source Helvetica Bold clone (sans-serif)
+
+      This is the real deal and not some cheap ripoff like Verdana.
+      IndUni-H-Bold is the default because not only does it look
+      great, but also renders *perfectly*.  and is also used for the
+      Fabulous logo.  Commonly found on stret signs.
+
+      This font is licensed under the GPL.  If you're developing
+      proprietary software you might want to ask its author or a
+      lawyer if Fabulous' use of IndUni-H would be considered a "GPL
+      Barrier."
+
+    - cmr10: Computer Modern (serif)
+
+      Donald Knuth wrote 23,000 lines for the sole purpose of
+      bestowing this jewel upon the world.  This font is commonly seen
+      in scholarly papers.
+
+    - DejaVuSansMono: DejaVu Sans Mono (formerly Bitstream Vera Sans Mono)
+
+      At point size 8, this is my favorite programming/terminal font.
+
+    For other fonts, I'll try my best to figure out where your font
+    files are stored.  If I have trouble finding your font, try using
+    an absolute path *with* the extension.  You could also try putting
+    the font in your ``~/.fonts`` folder and running ``fc-cache -fv
+    ~/.fonts``.
+
+"""
 
 import os
+import sys
 
 import grapefruit
 
-from fabulous import utils, image, color
-
-
-DEFAULT_FONT = os.path.join(os.path.dirname(__file__), 'fonts', 'IndUni-H-Bold.otf')
+from fabulous import utils, image
 
 
 class Text(image.Image):
-    def __init__(self, text, fsize=20, color="#0099ff", font=DEFAULT_FONT,
-                 shadow=False, scew=None):
+    """Renders TrueType Text to Terminal
+
+    I'm a sub-class of :class:`fabulous.image.Image`.  My job is
+    limited to simply getting things ready.  I do this by:
+
+    - Turning your text into an RGB-Alpha bitmap image using
+      :mod:`PIL`
+
+    - Applying way cool effects (if you choose to enable them)
+
+    For example::
+
+        >>> assert Text("Fabulous", shadow=True, skew=5)
+
+        >>> txt = Text("lorem ipsum", font="IndUni-H-Bold")
+        >>> len(str(txt)) > 0
+        True
+        >>> txt = Text("lorem ipsum", font="cmr10")
+        >>> len(str(txt)) > 0
+        True
+        >>> txt = Text("lorem ipsum", font="DejaVuSansMono")
+        >>> len(str(txt)) > 0
+        True
+
+    :param text: The text you want to display as a string.
+
+    :param fsize: The font size in points.  This obviously end up
+                  looking much larger because in fabulous a single
+                  character is treated as one horizontal pixel and two
+                  vertical pixels.
+
+    :param color: The color (specified as you would in HTML/CSS) of
+                  your text.  For example Red could be specified as:
+                   ``red``, ``#00F`` or ``#0000FF``.
+
+    :param shadow: If true, render a simple drop-shadow beneath text.
+                   The Fabulous logo uses this feature.
+
+    :param skew: Skew size in pixels.  This applies an affine
+                 transform to shift the top-most pixels to the right.
+                 The Fabulous logo uses a five pixel skew.
+
+    :param font: The TrueType font you want.  If this is not an
+                 absolute path, Fabulous will search for your font by
+                 globbing the specified name in various directories.
+    """
+
+    def __init__(self, text, fsize=20, color="#0099ff", shadow=False,
+                 skew=None, font='IndUni-H-Bold'):
         utils.pil_check()
         from PIL import Image, ImageFont, ImageDraw
         self.text = text
         self.color = grapefruit.Color.NewFromHtml(color)
-        self.font = ImageFont.truetype(font, fsize)
+        self.font = ImageFont.truetype(resolve_font(font), fsize)
         size = tuple([n + 3 for n in self.font.getsize(self.text)])
         self.img = Image.new("RGBA", size, (0, 0, 0, 0))
         cvs = ImageDraw.Draw(self.img)
         if shadow:
-            cvs.text((2, 2), self.text, font=self.font, fill=(150, 150, 150, 150))
-        cvs.text((1, 1), self.text, font=self.font, fill=self.color.html)
-        if scew:
+            cvs.text((2, 2), self.text,
+                     font=self.font,
+                     fill=(150, 150, 150, 150))
+        cvs.text((1, 1), self.text,
+                 font=self.font,
+                 fill=self.color.html)
+        if skew:
             self.img = self.img.transform(
-                size, Image.AFFINE, (1.0, 0.1 * scew, -1.0 * scew,
+                size, Image.AFFINE, (1.0, 0.1 * skew, -1.0 * skew,
                                      0.0, 1.0, 0.0))
         self.resize(None)
+
+
+class FontNotFound(ValueError):
+    """I get raised when the font-searching hueristics fail
+
+    This class extends the standard :exc:`ValueError` exception so you
+    don't have to import me if you don't want to.
+    """
+
+
+def resolve_font(name):
+    """Sloppy way to turn font names into absolute filenames
+
+    This isn't intended to be a proper font lookup tool but rather a
+    dirty tool to not have to specify the absolute filename every
+    time.
+
+    For example::
+
+        >>> path = resolve_font('IndUni-H-Bold')
+
+        >>> fontdir = os.path.join(os.path.dirname(__file__), 'fonts')
+        >>> indunih_path = os.path.join(fontdir, 'IndUni-H-Bold.ttf')
+        >>> assert path == indunih_path
+
+    This isn't case-sensitive::
+
+        >>> assert resolve_font('induni-h') == indunih_path
+
+    Raises :exc:`FontNotFound` on failure::
+
+        >>> resolve_font('blahahaha')
+        Traceback (most recent call last):
+        ...
+        FontNotFound: Can't find 'blahahaha' :'(  Try adding it to ~/.fonts
+
+    """
+    for fontdir, fontfiles in get_font_files():
+        for fontfile in fontfiles:
+            if name.lower() in fontfile.lower():
+                return os.path.join(fontdir, fontfile)
+    raise FontNotFound("Can't find %r :'(  Try adding it to ~/.fonts")
+
+
+@utils.memoize
+def get_font_files():
+    """Returns a list of all font files we could find
+
+    Returned as a list of dir/files tuples::
+
+        get_font_files() -> [('/some/dir', ['font1.ttf', ...]), ...]
+
+    For example::
+
+        >>> fabfonts = os.path.join(os.path.dirname(__file__), 'fonts')
+        >>> 'IndUni-H-Bold.ttf' in get_font_files()[fabfontdir]
+        True
+        >>> 'DejaVuSansMono.ttf' in get_font_files()[fabfontdir]
+        True
+        >>> 'cmr10.ttf' in get_font_files()[fabfontdir]
+        True
+
+        >>> assert len(get_font_files()) > 0
+        >>> for dirname, filename in get_font_files():
+        ...     assert os.path.exists(os.path.join(dirname, filename))
+        ...
+
+    """
+    dirs = [os.path.join(os.path.dirname(__file__), 'fonts'),
+            os.path.expanduser('~/.fonts')]
+    try:
+        # this is where ubuntu puts fonts
+        dirname = '/usr/share/fonts/truetype'
+        dirs += [os.path.join(dirname, subdir)
+                 for subdir in os.listdir(dirname)]
+    except OSError:
+        pass
+    return [(p, os.listdir(p)) for p in dirs if os.path.isdir(p)]
+
+
+def main(args):
+    """I provide a command-line interface for this module
+    """
+    import optparse
+    parser = optparse.OptionParser(args)
+    parser.add_option(
+        "-S", "--skew", dest="skew", type="int", default=None,
+        help=("Apply skew effect (measured in pixels) to make it look "
+              "extra cool.  For example, Fabulous' logo logo is skewed "
+              "by 5 pixels.  Default: %default"))
+    parser.add_option(
+        "-C", "--color", dest="color", default="#0099ff",
+        help=("Color of your text.  This can be specified as you would "
+              "using HTML/CSS.  Default: %default"))
+    parser.add_option(
+        "-B", "--term-color", dest="term_color", default=None,
+        help=("If you terminal background isn't black, please change "
+              "this value to the proper background so semi-transparent "
+              "pixels will blend properly."))
+    parser.add_option(
+        "-F", "--font", dest="font", default='IndUni-H-Bold',
+        help=("Path to font file you wish to use.  This defaults to a "
+              "free Helvetica-Bold clone which is included with Fabulous.  "
+              "Default value: %default"))
+    parser.add_option(
+        "-Z", "--size", dest="fsize", type="int", default=20,
+        help=("Size of font in points.  Default: %default"))
+    parser.add_option(
+        "-s", "--shadow", dest="shadow", action="store_true", default=False,
+        help=("Size of font in points.  Default: %default"))
+    (options, args) = parser.parse_args()
+
+    if options.term_color:
+        utils.term.bgcolor = options.term_color
+
+    text = " ".join(args)
+    fab_text = Text(text, skew=options.skew, color=options.color,
+                    font=options.font, fsize=options.fsize,
+                    shadow=options.shadow)
+    for line in fab_text:
+        print line
+
+if __name__ == '__main__':
+    main(sys.argv)
+"""
+    fabulous.utils
+    ~~~~~~~~~~~~~~
 
+"""
+
+import os
+import sys
 import fcntl
 import struct
 import termios
+import textwrap
+import functools
 
 import grapefruit
 
 
+def memoize(function):
+    """A very simple memoize decorator to optimize pure-ish functions
+
+    Don't use this unless you've examined the code and see the
+    potential risks.
+    """
+    cache = {}
+    @functools.wraps(function)
+    def _memoize(*args):
+        if args in cache:
+            return cache[args]
+        result = function(*args)
+        cache[args] = result
+        return result
+    return function
+
+
 class TerminalInfo(object):
+    """Quick and easy access to some terminal information
+
+    I'll tell you the terminal width/height and it's background color.
+
+    You don't need to use me directly.  Just access the global
+    :data:`term` instance::
+
+        >>> assert term.width > 0
+        >>> assert term.height > 0
+
+    It's important to know the background color when rendering PNG
+    images with semi-transparency.  Because there's no way to detect
+    this, black will be the default::
+
+        >>> term.bgcolor
+        (0.0, 0.0, 0.0, 1.0)
+        >>> import grapefruit
+        >>> isinstance(term.bgcolor, grapefruit.Color)
+        True
+
+    If you use a white terminal, you'll need to manually change this::
+
+        >>> term.bgcolor = 'white'
+        >>> term.bgcolor
+        (1.0, 1.0, 1.0, 1.0)
+        >>> term.bgcolor = grapefruit.Color.NewFromRgb(0.0, 0.0, 0.0, 1.0)
+        >>> term.bgcolor
+        (0.0, 0.0, 0.0, 1.0)
+
+    """
+
     def __init__(self, bgcolor='black'):
         self.bgcolor = bgcolor
 
     @property
+    def termfd(self):
+        """Returns file descriptor number of terminal
+
+        This will look at all three standard i/o file descriptors and
+        return whichever one is actually a TTY in case you're
+        redirecting i/o through pipes.
+        """
+        for fd in (2, 1, 0):
+            if os.isatty(fd):
+                return fd
+        raise Exception("No TTY could be found")
+
+    @property
     def dimensions(self):
-        call = fcntl.ioctl(0, termios.TIOCGWINSZ, "\000" * 8)
-        height, width = struct.unpack("hhhh", call)[:2]
-        return width, height
+        """Returns terminal dimensions
 
-    width = property(lambda self: self.dimensions[0])
-    height = property(lambda self: self.dimensions[1])
+        Don't save this information for long periods of time because
+        the user might resize their terminal.
+
+        :return: Returns ``(width, height)``.  If there's no terminal
+                 to be found, we'll just return ``(79, 40)``.
+        """
+        try:
+            call = fcntl.ioctl(self.termfd, termios.TIOCGWINSZ, "\000" * 8)
+        except:
+            return (79, 40)
+        else:
+            height, width = struct.unpack("hhhh", call)[:2]
+            return (width, height)
+
+    @property
+    def width(self):
+        """Returns width of terminal in characters
+        """
+        return self.dimensions[0]
+
+    @property
+    def height(self):
+        """Returns height of terminal in lines
+        """
+        return self.dimensions[0]
 
     def _get_bgcolor(self):
         return self._bgcolor
+
     def _set_bgcolor(self, color):
-        self._bgcolor = grapefruit.Color.NewFromHtml(color)
+        if isinstance(color, grapefruit.Color):
+            self._bgcolor = color
+        else:
+            self._bgcolor = grapefruit.Color.NewFromHtml(color)
+
     bgcolor = property(_get_bgcolor, _set_bgcolor)
 
 
 
 
 def pil_check():
-    """Check for PIL with friendly error message
+    """Check for PIL library, printing friendly error if not found
 
-    We check for PIL at runtime because it'd be a far greater evil to
-    put it in the setup_requires list.
+    We need PIL for the :mod:`fabulous.text` and :mod:`fabulous.image`
+    modules to work.  Because PIL can be very tricky to install, it's
+    not listed in the ``setup.py`` requirements list.
+
+    Not everyone is going to have PIL installed so it's best that we
+    offer as much help as possible so they don't have to suffer like I
+    have in the past :'(
     """
     try:
         import PIL
     except ImportError:
         raise ImportError(textwrap.dedent("""
-            I'm sorry, I can't render images without PIL :'(
+            Oh no!  You don't have the evil PIL library!
 
-            Ubuntu Users: sudo apt-get install python-imaging
+            Here's how you get it:
 
-            Windows Users: The PIL people should have something easy to
-              install that you can download from their website.
+            Ubuntu/Debian:
 
-            Everyone Else: This is like the hardest library in the world
-              to manually install.  If your package manager doesn't have
-              it, you can try running ``sudo easy_install pil`` once you
-              get your hands on a C compiler the development headers for
-              ``python``, ``libz``, ``libjpeg``, ``libgif``, ``libpng``,
-              ``libungif4``, ``libfreetype6``, and maybe more >_>
-            """))
+                sudo apt-get install python-imaging
+
+            Mac OS X:
+
+                http://pythonmac.org/packages/py24-fat/index.html
+                http://pythonmac.org/packages/py25-fat/index.html
+
+            Windows:
+
+                http://effbot.org/downloads/PIL-1.1.7.win32-py%(pyversion)s.exe
+
+            Everyone Else:
+
+              This is like the hardest library in the world to
+              manually install.  If your package manager doesn't have
+              it, you can try running ``sudo easy_install pil`` once
+              you get your hands on a C compiler as well as the
+              following libraries (including the development headers)
+              for Python, libz, libjpeg, libgif, libpng, libungif4,
+              libfreetype6, and maybe more >_>
+
+            """ % {'pyversion': "%s.%s" % sys.version_info[:2]}))
-"""Implements Support for 256-color Terminals
+"""
+    fabulous.xterm256
+    ~~~~~~~~~~~~~~~~~
+
+    Implements Support for the 256 colors supported by xterm as well
+    as quantizing 24-bit RGB color to xterm color ids.
+
+    Color quantization is very very slow so when this module is
+    loaded, it'll attempt to automatically compile a speedup module
+    using gcc.  A :mod:`logging` message will be emitted if it fails
+    and we'll fallback on the Python code.
+
 """
 
 import logging
 
 
 def xterm_to_rgb(xcolor):
+    """Convert xterm Color ID to an RGB value
+
+    All 256 values are precalculated and stored in :data:`COLOR_TABLE`
+    """
     assert 0 <= xcolor <= 255
     if xcolor < 16:
         # basic colors
 
 
 COLOR_TABLE = [xterm_to_rgb(i) for i in xrange(256)]
+
+
 def rgb_to_xterm(r, g, b):
-    """
-    OMG there is like no easy way to optimize this!!!  Someone smarter
-    than me should read this:
+    """Quantize RGB values to an xterm 256-color ID
 
-    http://algolist.manual.ru/graphics/quant/qoverview.php
+    This works by envisioning the RGB values for all 256 xterm colors
+    as 3D euclidean space and brute-force searching for the nearest
+    neighbor.
+
+    This is very slow.  If you're very lucky, :func:`compile_speedup`
+    will replace this function automatically with routines in
+    `_xterm256.c`.
     """
     if r < 5 and g < 5 and b < 5:
         return 16
     You need:
 
     - Python >= 2.5 for ctypes library
-    - gcc (sudo apt-get install gcc)
+    - gcc (``sudo apt-get install gcc``)
 
     """
-    import os, ctypes
+    import os
+    import ctypes
     from os.path import join, dirname, getmtime, exists
     library = join(dirname(__file__), '_xterm256.so')
     sauce = join(dirname(__file__), '_xterm256.c')
     xterm256_c = ctypes.cdll.LoadLibrary(library)
     xterm256_c.init()
     def xterm_to_rgb(xcolor):
-        res = xterm256_c.rgb_to_xterm_i(xcolor)
+        res = xterm256_c.xterm_to_rgb_i(xcolor)
         return ((res >> 16) & 0xFF, (res >> 8) & 0xFF, res & 0xFF)
     return (xterm256_c.rgb_to_xterm, xterm_to_rgb)
 
     description          = 'Makes your terminal output totally fabulous',
     download_url         = 'http://bitbucket.org/jart/fabulous/get/tip.tar.gz',
     long_description     = read('README'),
-    license              = 'BSD',
+    license              = 'MIT',
     install_requires     = ['grapefruit'],
     packages             = find_packages(),
     setup_requires       = ["setuptools_hg"],
     # http://pypi.python.org/pypi?:action=list_classifiers
     classifiers=[
         "Development Status :: 2 - Pre-Alpha",
-        "License :: OSI Approved :: BSD License",
+        "License :: OSI Approved :: MIT License",
         "Environment :: Console",
         "Intended Audience :: Developers",
         "Programming Language :: C",