Terminal Tricks

A deep dive into the ways we can get fancy output out of our terminals.

11

MAR
2015

Curses Windows, Pads, and Panels

In the previous article, I showed how you can accomplish some terminal input and output operations using the (n)curses(w) library. What I showed for output were mostly low-level writing routines though. Included in curses are some higher level abstractions that can be used to manage your program's output. This time, I want to show some of those tools.

Windows

The primary abstraction used in curses is the concept of a window. We actually already used them last time, but we stuck with stdscr (standard screen) which is just a window that fills the entire terminal. That's more for using curses without thinking much about windows.

When you want them though, you can section off the terminal screen into rectangular chunks. Each chunk is a window that you can print output inside of. Here's the most basic example that I could dream up:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  window = FFI::NCurses.newwin(5, 15, 4, 2)  # make a new window
  FFI::NCurses.waddstr(window, "Hello world!")

  FFI::NCurses.wrefresh(stdscr)  # still need this
  FFI::NCurses.wrefresh(window)

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

The main new bit in this code is the call to newwin(). As you can probably guess, this creates a new window. The window can contain the number of lines and characters specified in this call, 5 and 15 respectively in the code above. The other two numbers specify where this window is to be located on the screen: 4 lines down and 2 characters indented. (Remember, curses favors putting row before column.)

The two calls to wrefresh() may surprise you. curses still creates the stdscr and you can kind of think of it as backdrop of our output. We still need to draw it to clear the contents behind our window. Then our window can be drawn on top of that. Honestly though, the way I've done it here isn't ideal.

I used wrefresh() because we talked about it last time, but it doesn't make much sense, once multiple windows are involved. The reason is that curses maintains a kind of staged drawing pipeline. First, an in-memory virtual screen is refreshed with any new contents. Then the actual screen is updated with the contents of the virtual screen. wrefresh() confusingly triggers both of these steps. So the code above refreshes, updates, then refreshes and updates again right away. It would make more sense to just refresh the contents of both windows into the virtual screen, then update the real screen one time. To do that, we switch these two lines:

# ...

  FFI::NCurses.wrefresh(stdscr)  # still need this
  FFI::NCurses.wrefresh(window)

# ...

to the more efficient:

# ...

  FFI::NCurses.wnoutrefresh(stdscr)  # still need this
  FFI::NCurses.wnoutrefresh(window)
  FFI::NCurses.doupdate

# ...

I'm guessing the added nout stands for no output, but the purpose of wnoutrefresh() is to refresh without an automatic update. We do that for both windows, then we call for the update with the surprisingly well named doupdate(). You'll want to separate refresh and update operations when using multiple curses windows.

Now if you run that code, you'll probably see the not-too-impressive output:





  Hello world!

It doesn't look like much yet, does it? You can kind of see that my window is displaced from the top left corner due to the extra whitespace, but you can't really see the majority of the window itself because the parts that don't have content are invisible. Let's see if we can fix that;

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  window = FFI::NCurses.newwin(5, 15, 4, 2)
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)  # draw a box
  FFI::NCurses.waddstr(window, "Hello world!")

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.wnoutrefresh(window)
  FFI::NCurses.doupdate

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

The sadly cryptic call to wborder() asks curses to draw a box around our window. That gives us the possibly surprising output of:





  Hello world!--+
  |             |
  |             |
  |             |
  +-------------+

Your border may not look exactly like mine. More on why that is a little later. I'm also going to delay a discussion of the eight magic 0's. We're having enough trouble trying to get the border in the right place for now and those arguments won't help.

The good news is that we can now see our window. The bad news is that the border was drawn inside our content space and then we wrote over it. Oops. Knowing that you can write over the border could be potentially useful for titling windows, but I was hoping to see some content inside a box.

It's possibly interesting to note that reversing these two operations:

# ...

  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)  # draw a box
  FFI::NCurses.waddstr(window, "Hello world!")

# ...

to:

# ...

  FFI::NCurses.waddstr(window, "Hello world!")
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)  # draw a box

# ...

still fails to produce the desired results. Instead we see:





  +-------------+
  |             |
  |             |
  |             |
  +-------------+

In this case, our content was added, then drawn over by the box. Not helpful, right?

The two operations that I actually wanted were these:

# ...

  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)
  FFI::NCurses.mvwaddstr(window, 1, 1, "Hello world!")  # move then add content

# ...

I've changed the content to be drawn a little down and in from the top of the window to put it inside the border. That gives the result I've been trying to achieve:





  +-------------+
  |Hello world! |
  |             |
  |             |
  +-------------+

Seeing these border complications made me wonder what would happen if you overran the boundaries of a window. The answer is not what I expected, so we better talk about how that works:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  window = FFI::NCurses.newwin(4, 6, 0, 0)
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)
  FFI::NCurses.mvwaddstr(window, 1, 1, "123456\n7890\nabcd\n")  # overflow

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.wnoutrefresh(window)
  FFI::NCurses.doupdate

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

This code produces, well, a mess:

+----+
|12345
6
7890

Again we see that we can overwrite a border here. We also see that excess content on a line is hard wrapped to the next line. However, excess lines are discarded.

I wouldn't describe at least two of those operations as desirable. Probably the easiest workaround, in this case, is to swap the order of operations and let the border win:

# ...

  FFI::NCurses.mvwaddstr(window, 1, 1, "123456\n7890\nabcd\n")
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)

# ...

This gets things looking more normal, but it's still probably not desirable:

+----+
|1234|
|    |
+----+

We now see nothing on the second line because the content that was there, a lone wrapped 6, was overwritten by the border and the following newline pushed everything else down a line. This put our second line of content under the bottom border and pushed the rest into oblivion.

Honestly though, this issue is worse than it seems. Remember this?

+----+
|12345
6
7890

We know that the 5 ate one piece of the right side border, but what happened to the other one? Let's try a simpler experiment:

# ...

  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)
  FFI::NCurses.mvwaddstr(window, 1, 1, "1\n2")

# ...

This yields:

+----+
|1
2    |
+----+

Newlines break borders. Not ideal. Again we could draw the box last, but there are just two many issues here to easily get around. My advice: strip newlines out of the content you write into curses windows (use move operations instead).

We'll come back to better ways of protecting your borders from being overwritten, but first let's think a little more on those characters that were thrown into oblivion.

Pads

Many applications are going to have more content than will fit in a curses window. Using a plain window, you'll need to manage that content in some data structure and regularly redraw the currently visible portion into the window. That's one option.

Another option is to use a pad instead of a window. In curses, a pad is a window that can hold more content than it shows. You then just tell curses which portion of the content to show on the screen. I thought that sounded pretty awesome when I first heard about it, but it's not without tradeoffs. Let's walk through an example and I'll try to point out the good and not-so-good parts:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  pad = FFI::NCurses.newpad(4, 4)  # instead of newwin()
  #
  # Create some data like:
  #
  #   00AA
  #   11BB
  #   22CC
  #   33DD
  #
  4.times do |n|
    FFI::NCurses.mvwaddstr( pad, n, 0, "#{n}" * 2 +
                                       ("A".codepoints.first + n).chr * 2 )
  end

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.pnoutrefresh(pad, 0, 0, 0, 0, 1, 1)  # instead of wnoutrefresh()
  FFI::NCurses.doupdate

  FFI::NCurses.getch

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.pnoutrefresh(pad, 1, 1, 0, 0, 1, 1)
  FFI::NCurses.doupdate

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

The comments point out the main differences here. First, pads are created with newpad(), and unlike newwin(), that does not take screen location arguments. You only pass the sizes for the content area.

Once created a pad is mostly a window. You can generally use the same functions that you use on windows, as I have with mvwaddstr() in the code above.

The exception is that the *refresh() functions do not work on pads. Instead you'll need to call prefresh() or pnoutrefresh() (p for pad). These functions require some extra arguments. The first two, after the pad, are the coordinates of the top left corner in your content that you wish to display. The next two are the coordinates you would have normally passed to newwin() for the top left corner to use on the screen. The last two give the lower right corner for the screen. The lower right corner of the content is inferred using the size of the box on screen.

Let's see how this works in action. If I run that code, it will show me the upper left corner of the content before I push a button:

00
11

When I push the button, it skips past the first getch() to the second pnoutrefresh(). Those coordinates shift the view into the middle of the content:

1B
2C

The ability to move around some content like that is awesome, if you ask me, but the means for doing it, via the *refresh() calls, feels strange to me. I don't think I would often have a desire to shift the visible window around on the screen, which seems to be the case these coordinates are optimized for. Instead, I think I would prefer to keep passing the visible corners into newpad(), as I do newwin(), and then just update which portion of the content is currently shown. I would also prefer to set the content coordinates outside of the refresh mechanism and have them stick through redraws until I change them again. The reason for that gets us to the first major drawback of pads.

We've talked about how our code triggers the various stages of the redraw pipeline. Sometimes though, curses itself triggers redraws. This can happen because of terminal scrolling or the need to echo out some input. However, because all of the coordinates decisions of pads are tied to the refresh stage, curses won't know the current numbers to use. As such pads cannot be automatically updated.

The other reason I'm not super sold on pads is that they still don't make it very easy to update the content they hold. You have to set a fixed size on the content portion, then rewrite that content as needed using the same tools you do for windows. That means it's easy to add lines onto the end, assuming you still have the room, but adding a line at the beginning would mean rewriting all of the content in the pad. That doesn't seem superior to using windows.

As you can see, it's all tradeoffs. If I needed to scroll some static content or move the visible window around the screen for some reason (fog of war in a roguelike game?), I might reach for a pad. Otherwise, I'm pretty sure the content will need to be in a separate data structure anyway and I can probably use a window just as easily.

Borders

Remember all of that hand waving I did about borders in my early examples? Well, it's time to make good on those promised explanations.

Here's a trivial border example to refresh your memory:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  window = FFI::NCurses.newwin(4, 6, 0, 0)
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.wnoutrefresh(window)
  FFI::NCurses.doupdate

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

In one terminal, that draws this box for me:

+----+
|    |
|    |
+----+

In a different terminal, presumably with better encoding defaults set, I get:

┌────┐
│    │
│    │
└────┘

The differences occur because the eight 0's tell curses that we'll accept the default character for that position. It seems the chosen default can vary depending on the environment.

Each 0 represents a different character in the border. Probably the easiest way to see which character each argument represents is just to have curses show you. Change this line:

# ...

  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)

# ...

to this:

# ...

  FFI::NCurses.wborder(window, *(1..8).map { |n| n.to_s.codepoints.first })

# ...

You should see this cheatsheet:

533336
1    2
1    2
744448

Using these locations, you can make whatever fancy border you would like. For example, this code:

# ...

  FFI::NCurses.wborder(
    window,
    ">".codepoints.first,
    "<".codepoints.first,
    "v".codepoints.first,
    "^".codepoints.first,
    "\\".codepoints.first,
    "/".codepoints.first,
    "/".codepoints.first,
    "\\".codepoints.first
  )

# ...

gives us a Tie-fighter looking window:

\vvvv/
>    <
>    <
/^^^^\

The real thing you need to catch from these examples is that characters are specified as Integers, not Strings. The examples I've shown so far are just for ASCII characters. You can generally use wide characters (read: Unicode) instead, with uglier code:

# ...

  chars = Hash[ %i[ULCORNER LLCORNER URCORNER LRCORNER HLINE VLINE].map { |name|
    char            = FFI::NCurses::WinStruct::CCharT.new
    char[:chars][0] = FFI::NCurses.const_get("WACS_#{name}")
    [name, char]
  } ]
  FFI::NCurses.wborder_set(
    window,
    chars[:VLINE],
    chars[:VLINE],
    chars[:HLINE],
    chars[:HLINE],
    chars[:ULCORNER],
    chars[:URCORNER],
    chars[:LLCORNER],
    chars[:LRCORNER]
  )

# ...

That code gives me the fancier box, even on the terminal that defaulted to using +, -, and |. The two steps for using Unicode are that you need to construct and fill in FFI::NCurses::WinStruct::CCharT objects for each character and pass those to wborder_set(), instead of wborder().

For this example, I've used a set of constants defined by curses: WACS_VLINE, WACS_HLINE, etc. These are the wide character defaults for box drawing. You could substitute the codepoints for any Unicode characters you need.

Subwindows

OK, so we know it's easy for our content to clobber our borders. I also found myself wanting to answer questions like, "Could I introduce some HTML-like padding between a border and the content?" The solution to both of these problems is subwindows.

A subwindow is just another window. However, it has a parent window, so to speak, and it actually occupies a space inside that parent window. Let's show that in some code:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho

  window = FFI::NCurses.newwin(6, 8, 0, 0)
  FFI::NCurses.wborder(window, 0, 0, 0, 0, 0, 0, 0, 0)
  content = FFI::NCurses.derwin(window, 2, 4, 2, 2)  # create a subwindow
  FFI::NCurses.waddstr(content, "123456\n7890\nabcd\n")

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.wnoutrefresh(window)
  FFI::NCurses.touchwin(window)  # do this before refreshing subwindows
  FFI::NCurses.wnoutrefresh(content)
  FFI::NCurses.doupdate

  FFI::NCurses.getch
ensure
  FFI::NCurses.endwin
end

The comment points out the line that creates the subwindow. On that line, the 2, 4 arguments define the content size, just as they would for a normal window. The following 2, 2 arguments then position it using offsets from the parent window. (There are other ways to create subwindows, but they use absolute coordinates and I find that confusing.)

The other new element here is the added call to touchwin(). I don't think it's actually needed in this case, because the full window was just refreshed on the line above (because it was the first refresh of window), but the documentation does recommend touching before any refresh of a subwindow.

I suppose that means we should discuss the concept of touching. curses aims to be efficient, so it tries to just refresh changed characters in a window. This means it needs to keep track of what has and has not changed. If you overwrite some content, obviously it changed and curses knows that, without your help. However, in scenarios where you have windows laying on top of other windows, a change in the front window could require some refreshing of the back window. For example, any whitespace placed in the front window may allow some content of the back window to bleed through. If those characters aren't touched, curses won't know that it needs to redraw them.

Now, the tool I used above, touchwin(), is crude. It just says, "This window may have changed." curses is forced to refresh all characters in the window for this call. You can be a little more specific and indicate which rows may have changed. This would save curses some work. To do that, we would change this line:

# ...

  FFI::NCurses.touchwin(window)  # do this before refreshing subwindows

# ...

to:

# ...

  FFI::NCurses.touchline(window, 2, 2)  # absolute y, line count

# ...

I have no idea why this function is called touchline() and yet affects many lines. Also, I'm not aware of a way to specify the columns affected. It seems the best you can do is full rows.

Anyway, that code prints content in a window with padding and an intact border:

+------+
|      |
| 1234 |
| 56   |
|      |
+------+

Notice that my content contained nasty newlines, but the window frame is unaffected. This is the power of subwindows, in my opinion. You can use them to separate different kinds of content (including borders) and keep them from interfering with each other.

Could you do this magic trick with normal windows layered on top of each other? Yes. The only differences are that you would need to use absolute coordinates everywhere. (Subwindows are a mixed bag in comparison. I can create them with relative coordinates, but I still need to manage touching using absolute coordinates.) Also the documentation says that subwindows share some memory with their parent windows. I'm not sure how much that helps in cases like the code above where they are never truly drawing on top of each other, but it sounds nice.

Panels

Once you start managing overlapping windows and/or subwindows, keeping track of things like touching and refreshing can get pretty complex. curses itself doesn't offer much relief for that, but there is another library, called panel, that is very commonly installed with curses, and it does address this problem. Luckily for us, ffi-ncurses wraps both libraries.

First, let's look at some problematic code:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho
  FFI::NCurses.keypad(stdscr, true)
  FFI::NCurses.curs_set(0)  # hide the cursor

  moveable = FFI::NCurses.newwin(5, 5, 5, 0)
  FFI::NCurses.wborder(moveable, 0, 0, 0, 0, 0, 0, 0, 0)
  3.times do |y|
    FFI::NCurses.mvwaddstr(moveable, y + 1, 1, "mmm")
  end
  FFI::NCurses.wmove(moveable, 0, 0)

  stationary = FFI::NCurses.newwin(15, 5, 0, 10)
  FFI::NCurses.wborder(stationary, 0, 0, 0, 0, 0, 0, 0, 0)
  13.times do |y|
    FFI::NCurses.mvwaddstr(stationary, y + 1, 1, "sss")
  end

  FFI::NCurses.wnoutrefresh(stdscr)
  FFI::NCurses.wnoutrefresh(moveable)
  FFI::NCurses.wnoutrefresh(stationary)
  FFI::NCurses.doupdate

  loop do
    y     = FFI::NCurses.getbegy(moveable)
    x     = FFI::NCurses.getbegx(moveable)
    lines = FFI::NCurses.getmaxy(moveable)

    case FFI::NCurses.getch
    when "q".codepoints.first
      break
    when FFI::NCurses::KeyDefs::KEY_LEFT
      FFI::NCurses.mvwin(moveable, y, x - 1)
    when FFI::NCurses::KeyDefs::KEY_RIGHT
      FFI::NCurses.mvwin(moveable, y, x + 1)
    end

    FFI::NCurses.wnoutrefresh(stdscr)
    FFI::NCurses.wnoutrefresh(moveable)
    FFI::NCurses.wnoutrefresh(stationary)
    FFI::NCurses.doupdate
  end
ensure
  FFI::NCurses.endwin
end

This code has a moveable window (filled with m's) and a stationary window (filled with s's). The left and right arrow keys can be used to push the moveable window in those directions via the mvwin() function.

The code is lengthy but hopefully not too tricky. After the typical setup, with a first-seen-here mode used to make the cursor invisible, the two chunks of code just create and fill windows. The next chunk does the initial display. Then we go into a loop where we respond to the keys by moving the window in the indicated direction and redrawing.

This code doesn't really work yet. If I run it and nudge the moveable window right for a while, then back to the left, my screen looks like this:

          +---+
          |sss|
          |sss|
          |sss|
          |sss|
+++++---+++++s|
|||||mmm|||||s|
|||||mmm|||||s|
|||||mmm|||||s|
+++++---+++++s|
          |sss|
          |sss|
          |sss|
          |sss|
          +---+

You can see that the old locations aren't being properly cleared. We should understand the reason for this now. We need to touch the affected windows so curses will refresh their content. The fix is to add to this refreshing code inside the loop:

# ...

    FFI::NCurses.wnoutrefresh(stdscr)
    FFI::NCurses.wnoutrefresh(moveable)
    FFI::NCurses.wnoutrefresh(stationary)
    FFI::NCurses.doupdate

# ...

Here are the additions:

# ...

    FFI::NCurses.touchline(stdscr, y, lines)      # update the background
    FFI::NCurses.wnoutrefresh(stdscr)
    FFI::NCurses.wnoutrefresh(moveable)
    FFI::NCurses.touchline(stationary, y, lines)  # update the other window
    FFI::NCurses.wnoutrefresh(stationary)
    FFI::NCurses.doupdate

# ...

This gives us smooth movement:

          +---+
          |sss|
          |sss|
          |sss|
          |sss|
       +--|sss|
       |mm|sss|
       |mm|sss|
       |mm|sss|
       +--|sss|
          |sss|
          |sss|
          |sss|
          |sss|
          +---+

If we would prefer to see the moving window on top, we could reorder the refreshes:

# ...

    FFI::NCurses.touchline(stdscr, y, lines)      # update the background
    FFI::NCurses.wnoutrefresh(stdscr)
    FFI::NCurses.touchline(stationary, y, lines)  # update the other window
    FFI::NCurses.wnoutrefresh(stationary)
    FFI::NCurses.wnoutrefresh(moveable)
    FFI::NCurses.doupdate

# ...

Then I see this:

          +---+
          |sss|
          |sss|
          |sss|
          |sss|
        +---+s|
        |mmm|s|
        |mmm|s|
        |mmm|s|
        +---+s|
          |sss|
          |sss|
          |sss|
          |sss|
          +---+

That all works, but it's kind of a lot to keep track of. Plus we're just dealing with two windows so far. More windows could add a lot more complexity.

That's where panel comes in. It provides a stack of panels and once you place windows in that stack, panel will manage the refresh order and touching for you. stdscr is not explicitly part of the stack, but it is managed for you as well. Let's rewrite the code to use this extra abstraction:

require "ffi-ncurses"

begin
  stdscr = FFI::NCurses.initscr
  FFI::NCurses.cbreak
  FFI::NCurses.noecho
  FFI::NCurses.keypad(stdscr, true)
  FFI::NCurses.curs_set(0)

  moveable = FFI::NCurses.newwin(5, 5, 5, 0)
  FFI::NCurses.wborder(moveable, 0, 0, 0, 0, 0, 0, 0, 0)
  3.times do |y|
    FFI::NCurses.mvwaddstr(moveable, y + 1, 1, "mmm")
  end
  FFI::NCurses.wmove(moveable, 0, 0)

  stationary = FFI::NCurses.newwin(15, 5, 0, 10)
  FFI::NCurses.wborder(stationary, 0, 0, 0, 0, 0, 0, 0, 0)
  13.times do |y|
    FFI::NCurses.mvwaddstr(stationary, y + 1, 1, "sss")
  end

  # construct the panels, back to front
  panels = [ ]
  panels << FFI::NCurses.new_panel(moveable)
  panels << FFI::NCurses.new_panel(stationary)

  FFI::NCurses.update_panels  # touch and refresh all panels as needed
  FFI::NCurses.doupdate       # then update normally

  loop do
    y = FFI::NCurses.getbegy(moveable)
    x = FFI::NCurses.getbegx(moveable)

    case FFI::NCurses.getch
    when "q".codepoints.first
      break
    when FFI::NCurses::KeyDefs::KEY_LEFT
      FFI::NCurses.move_panel(panels.first, y, x - 1)  # move panels not windows
    when FFI::NCurses::KeyDefs::KEY_RIGHT
      FFI::NCurses.move_panel(panels.first, y, x + 1)
    end

    FFI::NCurses.update_panels
    FFI::NCurses.doupdate
  end
ensure
  FFI::NCurses.endwin
end

The comments again point out the major differences, but they are:

  1. We now construct the stack of panels
  2. We replace all touching and refreshing code with a single function call
  3. We need to move panels instead of windows so the panel can manage changes

As you can see, it's less code and it still works the same. Because we don't have to handle touching with this approach, we can stop worrying about the oddity of needing absolute coordinates for our subwindows as well.

This version will show the moveable window behind the stationary one, because that's the order we built the stack in. To swap them, we could reorder the stack building code, or just use one of panel's reordering functions like this:

# ...

  # construct the panels, back to front
  panels = [ ]
  panels << FFI::NCurses.new_panel(moveable)
  panels << FFI::NCurses.new_panel(stationary)
  FFI::NCurses.top_panel(panels.first)  # move panel to the top

# ...

I hope you'll agree that panels are a lot easier to manage than the manual triggering of touches and refreshing. My last piece of advice is probably pretty obvious: prefer panels for managing window ordering.

Comments (2)
  1. Duane Voth
    Duane Voth September 14th, 2015 Reply Link

    James, do you have some example code that shows how to use prefresh on a panel (basically a movable log window). The docs on prefresh are really thin ... Nice overview btw!

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  2. James Edward Gray II
    James Edward Gray II September 14th, 2015 Reply Link

    I only know of the coverage in the man page. Sorry.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader