Friday, June 26, 2020

Draw Boxes, etc, on Debian GNU/Linux Terminal Window With Go Lang & tcell

     A cleaner version of this document is here.
Draw Boxes, etc, on Debian GNU/Linux
Terminal Window With
Go Lang
&
tcell

Have a functioning Debian system.

Install go

# apt install go

Make sure go works:

Create a workspace directory and cd into it; something like:

$ mkdir ~/PROGRAMMING/GO/HELLOWORLD
$ cd ~/PROGRAMMING/GO/HELLOWORLD

Create a test “helloworld.go” file with the following:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hi")
}

Compile it:

$ go build helloworld.go

Be aware that if you copy and paste from this document into a text editor, the quote marks might paste as “smartquotes”, causing your program to generate errors during compilation. In such a case, just manually delete and retype the quote marks.

Run it, to test:

$ ./helloworld

Do a basic app to draw a character on the “graphical” screen

Create a new workspace for our box-drawing program: and cd into it:

     $ mkdir ~/PROGRAMMING/GO/BOXES
    $ cd ~/PROGRAMMING/GO/BOXES

Create a new program file, “boxes.go”, and put the following code into it:

package main
/*
 This code draws a single character (or 'rune', or 'glyph') at a single
 location on a terminal window in Debian GNU/Linux.
*/

import (
     "fmt"
"os"
"time"
     "github.com/gdamore/tcell"
) // end of import

func main() {
// Create a "canvas" (a window, frame) on which to draw
// ‘s’ is the name of the screen/canvas on which we’ll draw
// ‘e’ is some error information the NewScreen() method insists on generating
// (NewScreen() returns both values, so we have to deal with both)
s, e := tcell.NewScreen()

// Deal with the ‘e’ value by checking for some error situations; exit if need be
// (Requires "os" package in "import" section at top.)
if e != nil {
        fmt.Fprintf(os.Stderr, "%v\n", e)
        os.Exit(1)
}
if e = s.Init(); e != nil {
         fmt.Fprintf(os.Stderr, "%v\n", e)
         os.Exit(1)
     }

// Set the 's' canvas’s default bg & fg colors from tcell’s list of colors
s.SetStyle(tcell.StyleDefault.
    Foreground(tcell.ColorWhite).
    Background(tcell.ColorBlack))

// Clear the "canvas"
s.Clear()

// Create a “glyph” using the “rune” (character) we want to draw
glyph := rune('W')

// Create and set the coordinates for where we'll draw our character ('glyph', 'rune')
x := 10
y := 20

// Create and set variables for foreground and background colors
fg := tcell.ColorYellow
bg := tcell.ColorBlue

// We need to send an entire “clump” of tcell info to the 
// SetCell() command below, so we’ll create a copy of the
// default tcell “clump” named ‘st’, and then modify the
// bg & fg colors to what we set above, for just this copy,
// which will be what we draw the cell with.
st := tcell.StyleDefault
st = st.Background(bg)
st = st.Foreground(fg)

// Place the cell at the desired location, with our copy of the tcell “clump”,
// and the glyph we want to draw.  All the cells of the ‘s’ screen/canvas
// except this one will get the default settings, such as the black background.
s.SetCell(x, y, st, glyph)

// Nothing has actually been drawn on screen yet; so unveil the "canvas"
s.Show()

// Pause X seconds before resetting "graphics" screen to normal
// time.Second is the unit (a second); time.Duration is how many units
// (Requires “time” package in the “import” section at top.)
secondsToSleep := time.Duration(3) * time.Second
time.Sleep(secondsToSleep)

//Reset screen to normal
s.Fini()
} // end of main()

Compile it:

$ go build boxes.go

If you get an error like ‘cannot find package "github.com/gdamore/tcell"’,

    $ go get github.com/gdamore/tcell

Run it, to test:

$ ./boxes

You should see the terminal window turn black, and then a yellow “W” printed on a blue background at 10 characters in from the left of the terminal window and 20 lines down from the top of the terminal window.

Feel free to tinker with the settings in this program and recompile/re-run until you feel comfortable with what the program is doing.

Move the Actual Drawing Part into a function

Modify your program so it looks like the one below. Note that the bulk of the program listing has been replaced here with an ellipses (“...”), but you won’t do that in your program listing. I’m only doing so here to make this document less cumbersome. Note also that functions do not have to be below the main() function; they can be above if that’s your preference.

package main
/*
 This code draws a single character (or 'rune', or 'glyph') at a single
 location on a terminal window in Debian GNU/Linux.
*/

import (
...
) // end of import

func main() {
...
} // end of main()

func drawChar(s tcell.Screen, x, y int, fg, bg tcell.Color, glyph rune) int {
    // We need to send an entire “clump” of tcell info to the
    // SetCell() command below, so we’ll create a copy of the
    // default tcell “clump” named ‘st’, and then modify the
    // bg & fg colors to what we set above, for just this copy,
    // which will be what we draw the cell with.
    st := tcell.StyleDefault
    st = st.Background(bg)
    st = st.Foreground(fg)

// Place the cell at the desired location, with our copy of the tcell “clump”,
// and the glyph we want to draw. All the cells of the 's' screen/canvas
// except this one will get the default settings, such as the black background.
s.SetCell(x, y, st, glyph)
return 0
} // end of drawChar()

You’ll notice that the four lines (one line of code, and three lines of comment) were copied from the main() func into the drawChar() func.
Now, of those four lines in main(), leave the three comment lines for now, and replace just the one line of:

s.SetCell(x, y, st, glyph)

with the single line of:

drawChar(s tcell.Screen,, x, y, fg, bg, glyph)

Note also that a new line of “return 0” was added to the bottom of the drawChar() function. I think it’s a good practice to provide a return value from functions. The last “int” of the top line of the function specifies that the return value will be an integer; since we’re assuming everything will work without error, we’re returning a return value of zero, which is usually interpreted to mean “no errors occurred while running this function”. We’re not actually doing anything with this return value, but we could do something like:
   
retVal := drawChar(s tcell.Screen, x, y, fg, bg, glyph)
    If retVatl != 0 (print some error information to the screen or to a log file, etc)

If you compile and run this new code, you should see the same result on the screen.

It may seem like you’ve added some unneeded complexity to your program (which I’ll explain about a bit in a bit), but you’ve just made it easier to draw new characters on the screen. For example, just before the existing lines that say:

// Nothing has actually been drawn on screen yet; so unveil the "canvas"
s.Show()

add these lines:

x = 30
y = 10
fg = tcell.ColorRed
bg = tcell.ColorGreen
glyph = rune(‘?’)
drawChar(s, x, y, fg, bg, glyph)

Now compile and run your program. In addition to the “W” you saw earlier, you should now see a red “Y” on a green background 30 columns in and 10 rows down.

Now for Some Explanation

The function drawChar() expects some information from the main program (aka, the main() function). These pieces of information are sent to the function by the line that calls the function:

drawChar(s, x, y, fg, bg, glyph)

And the function’s top line defines what it expects as incoming information to the function:

func drawChar(s tcell.Screen, x, y int, fg, bg tcell.Color, glyph rune) int {

The number and types of arguments (or parameters) sent to the function must match the number and types of arguments (parameters) expected by the function.

In this case, I used the same names for the outgoing information as for the incoming information, but it’s important to know that (in this case, at least) the incoming information is only a copy of the outgoing information; the variables have the same names, and a copy of the same values, but they are completely different variables in the function than in the main program. Just as Paul Simon the musician and Paul the Apostle from the Bible have the same name “Paul”, they are completely different people, and the “fg” variable in the main program and which is being sent to the function is a completely different variable than the “fg” variable used in the function, even though ti has the same name and an identical copy of the variable’s value.

If we wanted to do so, we could have used different names in the function, like this:

func drawChar(screenOnWhichToDraw tcell.Screen, x_coordinate, y_coordinate int, foreground_color, background_color tcell.Color, charToDisplay rune) int {

The types of the arguments specify what type of information is in the variable.

For example, the ‘s’ (or ‘screenOnWhichToDraw’) variable holds a “clump” of data that is predefined in the tcell package, named “Screen” (thus, it’s referenced as “tcell.Screen”).

The ‘x’ and ‘y’ variables are integers (referenced as “int”), which means they’re just normal “counting numbers”, not fractions or weird imaginary numbers or having decimal points, etc, just -2 or 6 or 248, etc.

The ‘fg’ and ‘bg’ variable are also a pre-defined type from the tcell package, named “Color” (referenced as “tcell.Color”).

And the “glyph” variable is a type named “rune”. A “rune” is a special case of 32-bit integer used with Unicode characters. Just think of them as any character you might find on a computer keyboard, even in far-off places that use weird symbols instead of English alphabetic characters. For example, the letter “W” is a rune; so is ‘w’, and so are ‘╔’ and ‘&’, although “under the hood” the computer really sees them as 32-bit integers. If you remember your ASCII studies from your high-school Computer Literacy Class, ASCII chars are runes.

So the main program sets up a “screen” with a default white lettering on a black background, and sends a copy of that to the drawChar() function as ‘s’ (or ‘screenOnWhichToDraw’). It sends a copy of the X and Y coordinates, and a copy of the foreground and background colors to use for drawing the one cell that will change in the function, and a copy of the rune that we’ll draw in that one cell.

When the function gets all these copies, it creates a new copy of the “clump” of information containing all the style defaults provided by the tcell package, named “st”, and then modifies two of those styles, the foreground and background colors, and then sends that “clump” of information, along with the x and y coordinates and the glyph rune, to the screen’s “SetCell” function, which sets one cell on the screen according to all that information.

Now, Let’s Draw a Box

Let’s create a brand-new function. Leave the old drawChar() function in place; even if we decided to remove the calls for that function from the main program, and never use that function again, it doesn’t hurt us to have it in our program listing file just sitting there doing nothing.

After the last line of the drawChar() function, create this code:

...
} // end of drawChar()


func drawBox(s tcell.Screen, leftEdge, topEdge, width, height int, fg, bg tcell.Color) int {
    // Calculate bottom-right corner of box
    rightEdge := leftEdge + width
    bottomEdge := topEdge + height
    glyph := ' '        // Create a rune variable

    glyph = ‘╔’
    drawChar(s, leftEdge, topEdge, fg, bg, glyph)

glyph = ‘╗’
    drawChar(s, rightEdge, topEdge, fg, bg, glyph)

glyph = ‘╚’
    drawChar(s, leftEdge, bottomEdge, fg, bg, glyph)

glyph = ‘╝’
    drawChar(s, rightEdge, bottomEdge, fg, bg, glyph)

    s.Show()

    return 0
}

Depending on your operating system, there are different ways of entering in these characters. On my Debian GNU/Linux box, I enter such a rune by pressing Ctrl-Shift-U, which will create an underlined ‘u’, then typing in the Unicode number for the character I want. I find the easiest way to find these codes is to web-search for “Unicode character set box-drawing” and using the first Wikipedia hit I get.

To create ‘╚’, I press Ctrl-Shift-U, then 255a, then ENTER.

Next, create the drawbox() calling line (and it’s comment, “Draw a box”, and a blank line above that, leaving the rest of the contextual lines around it alone):

    ...
glyph = rune('?')
drawChar(s, x, y, fg, bg, glyph)

// Draw a box
drawBox(s, x, y, 20, 4, tcell.ColorWhite, tcell.ColorBlack)

// Nothing has actually been drawn on screen yet; so unveil the "canvas"
s.Show()
    ...

When you compile and run this, you should have the four corners of a box.

Note that since we left the starting x & w the same as when we drew the ‘?’, the top-left rune of the box overwrote that ‘?’.

Also notice that I used “magic numbers” in this line; six months from now, when you go to look at this code, you won’t remember what the “20” and “4” represent, or what the colors color. It’s a bad habit to use “magic numbers”, so let’s replace those with more meaningful identifiers:

    ...
glyph = rune('?')
drawChar(s, x, y, fg, bg, glyph)
boxWidth := 20
boxHeight := 4
boxForeGroundColor := tcell.ColorWhite
boxBackGroundColor := tcell.ColorBlack
drawBox(s, x, y, boxWidth, boxHeight, boxForeGroundColor, boxBackGroundColor)

// Nothing has actually been drawn on screen yet; so unveil the "canvas"
s.Show()
   

Now we need to finish drawing the perimeter lines of the box.

If we try to draw the perimeter line after we’ve drawn the corners, we’ll have to make exceptions for the corners, and that can get messy. So instead, let’s draw the entire box, then re-draw the corners with the correct runes. To start, let’s draw the first and last lines with ‘═’ (Ctrl-Shift-U, 2550, Enter), from leftEdge to rightEdge, then the left and right edges, and then finally the four corners, like so:

func drawBox(s tcell.Screen, leftEdge, topEdge, width, height int, fg, bg tcell.Color) int {
    // Calculate bottom-right corner of box
    rightEdge := leftEdge + width
    bottomEdge := topEdge + height
    glyph := ' '        // Create a rune variable

    // Draw the top and bottom lines of box
    glyph = '═'
    for x := leftEdge; x <= rightEdge; x++ {
        drawChar(s, x, topEdge, fg, bg, glyph)
        drawChar(s, x, bottomEdge, fg, bg, glyph)
    }
    
    // Draw the sides of box
    glyph = '║'
    for y := topEdge + 1; y <= bottomEdge; y++ {
        drawChar(s, leftEdge, y, fg, bg, glyph)   
        drawChar(s, rightEdge, y, fg, bg, glyph)   
    }
    
    // Draw the four corners of box
    glyph = '╔'
    drawChar(s, leftEdge, topEdge, fg, bg, glyph)
    glyph = '╗'
    drawChar(s, rightEdge, topEdge, fg, bg, glyph)
    glyph = '╚'
    drawChar(s, leftEdge, bottomEdge, fg, bg, glyph)
    glyph = '╝'
    drawChar(s, rightEdge, bottomEdge, fg, bg, glyph)

    // Display the screen
    s.Show()
    
    return 0
}

Now you can draw more boxes just by adding more drawBox() lines in main(), like so:

...
// Draw a box
boxWidth := 20
boxHeight := 4
boxForeGroundColor := tcell.ColorWhite
boxBackGroundColor := tcell.ColorBlack
drawBox(s, x, y, boxWidth, boxHeight, boxForeGroundColor, boxBackGroundColor)

// Draw box #2
x = 40
y = 8
boxWidth = 10
boxHeight = 6
boxForeGroundColor = tcell.ColorYellow
boxBackGroundColor = tcell.ColorBlack
drawBox(s, x, y, boxWidth, boxHeight, boxForeGroundColor, boxBackGroundColor)

// Draw box #3
x = 3
y = 2
boxWidth = 5
boxHeight = 14
boxForeGroundColor = tcell.ColorRed
boxBackGroundColor = tcell.ColorYellow
drawBox(s, x, y, boxWidth, boxHeight, boxForeGroundColor, boxBackGroundColor)

We can even add a fill option. Add a boolean (true/false) variable, “fill”, to the drawBox() header line, like this:

func drawBox(s tcell.Screen, leftEdge, topEdge, width, height int, fg, bg tcell.Color, fill bool) int {

and change the function itself to this:

    ...
glyph := ' '        // Create a rune variable
    
    // If fill, fill in the entire box
    if fill {
    for y := topEdge; y <= bottomEdge; y++ {
                for x := leftEdge; x <= rightEdge; x++ {
                        drawChar(s, x, y, fg, bg, glyph)
                }
            }
    }

    // Draw the top and bottom lines of box
     ...

Then add a fill variable to the calling lines, like so:

fill := true    // or ‘false’
drawBox(s, x, y, boxWidth, boxHeight, boxForeGroundColor, boxBackGroundColor, fill)

(Remember the colon only on the first, declaration, of a new variable; otherwise leave the colon off.)