diff --git a/gems/include/terminal/cell_array.h b/gems/include/terminal/cell_array.h
index 4bf88fce4..9aae86de3 100644
--- a/gems/include/terminal/cell_array.h
+++ b/gems/include/terminal/cell_array.h
@@ -109,6 +109,11 @@ class Cell_array
_line_dirty[line] = false;
}
+ void mark_line_as_dirty(int line)
+ {
+ _line_dirty[line] = true;
+ }
+
void scroll_up(int region_start, int region_end)
{
_scroll_vertically(region_start, region_end, true);
diff --git a/gems/run/terminal_mux.run b/gems/run/terminal_mux.run
new file mode 100644
index 000000000..844a75dca
--- /dev/null
+++ b/gems/run/terminal_mux.run
@@ -0,0 +1,158 @@
+set build_components {
+ core init drivers/timer noux/minimal lib/libc_noux test/bomb
+ drivers/uart server/terminal_mux server/terminal_log
+ noux-pkg/vim
+}
+
+build $build_components
+
+exec tar cfv bin/vim.tar -h -C bin/vim .
+
+create_boot_directory
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+
+#
+# Boot modules
+#
+
+# generic modules
+set boot_modules {
+ core init timer ld.lib.so noux terminal_mux terminal_log uart_drv
+ libc.lib.so libm.lib.so libc_noux.lib.so libc_terminal.lib.so ncurses.lib.so
+ vim.tar
+}
+
+build_boot_image $boot_modules
+
+append qemu_args " -m 256 "
+#append qemu_args " -nographic -serial mon:stdio -serial file:/tmp/foo"
+append qemu_args " -nographic -serial file:/tmp/foo -serial mon:stdio"
+
+run_genode_until forever
+
+exec rm bin/vim.tar
diff --git a/gems/src/server/terminal_mux/README b/gems/src/server/terminal_mux/README
new file mode 100644
index 000000000..270411506
--- /dev/null
+++ b/gems/src/server/terminal_mux/README
@@ -0,0 +1,19 @@
+The terminal_mux server is able to provide multiple terminal sessions over one
+terminal-client session. The user can switch between the different sessions
+using a keyboard shortcut, which brings up an ncurses-based menu.
+
+The terminal sessions provided by terminal_mux implement (a subset of) the
+Linux terminal capabilities. By implementing the those capabilities, the server
+is interchangable with the graphical terminal ('gems/src/server/terminal').
+The terminal session used by the server is expected to by VT102 compliant.
+This way, terminal_mux can be connected via an UART driver with terminal
+programs such as minicom, which typically implement VT102 rather than the Linux
+terminal capabilities.
+
+When started, terminal_mux displays a menu with a list of currently present
+terminal sessions. The first line presents status information, in particular
+the label of the currently visible session. A terminal session can be selected
+by using the cursor keys and pressing return. Once selected, the user is able
+to interact with the corresponding terminal session. Returning to the menu is
+possible at any time by pressing control-x.
+
diff --git a/gems/src/server/terminal_mux/main.cc b/gems/src/server/terminal_mux/main.cc
new file mode 100644
index 000000000..48173c06b
--- /dev/null
+++ b/gems/src/server/terminal_mux/main.cc
@@ -0,0 +1,746 @@
+/*
+ * \brief Ncurses-based terminal multiplexer
+ * \author Norman Feske
+ * \date 2013-02-20
+ */
+
+/*
+ * Copyright (C) 2013 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU General Public License version 2.
+ */
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* terminal includes */
+#include
+#include
+#include
+#include
+#include
+
+/* local includes */
+#include
+
+
+/*
+ * Convert character array into ncurses window
+ */
+static void convert_char_array_to_window(Cell_array *cell_array,
+ Ncurses::Window &window)
+{
+ for (unsigned line = 0; line < cell_array->num_lines(); line++) {
+
+ if (!cell_array->line_dirty(line))
+ continue;
+
+ window.move_cursor(0, line);
+ for (unsigned column = 0; column < cell_array->num_cols(); column++) {
+
+ Char_cell cell = cell_array->get_cell(column, line);
+ unsigned char ascii = cell.ascii;
+
+ if (ascii == 0) {
+ window.print_char(' ', false, false);
+ continue;
+ }
+
+ /* XXX add color */
+ window.print_char(ascii, cell.highlight(), cell.inverse());
+ }
+ }
+}
+
+
+/**
+ * Registry of clients of the multiplexer
+ */
+struct Registry
+{
+ struct Entry : Genode::List::Element
+ {
+ /**
+ * Flush pending drawing operations
+ */
+ virtual void flush() = 0;
+
+ /**
+ * Redraw and flush complete entry
+ */
+ virtual void flush_all() = 0;
+
+ /**
+ * Return session label
+ */
+ virtual char const *label() const = 0;
+
+ /**
+ * Submit character into entry
+ */
+ virtual void submit_input(char c) = 0;
+ };
+
+ /**
+ * List of existing terminal sessions
+ *
+ * The first entry of the list has the current focus
+ */
+ Genode::List _list;
+
+ /**
+ * Lookup entry at specified index
+ *
+ * \return entry, or 0 if index is out of range
+ */
+ Entry *entry_at(unsigned index)
+ {
+ Entry *curr = _list.first();
+ for (; curr && index--; curr = curr->next());
+ return curr;
+ }
+
+ void add(Entry *entry)
+ {
+ /*
+ * Always insert new entry at the second position. The first is
+ * occupied by the current focused entry.
+ */
+ Entry *first = _list.first();
+ if (first)
+ _list.remove(first);
+
+ _list.insert(entry);
+
+ if (first)
+ _list.insert(first);
+ }
+
+ void remove(Entry *entry)
+ {
+ _list.remove(entry);
+ }
+
+ void to_front(Entry *entry)
+ {
+ if (!entry) return;
+
+ /* make entry the first one of the list */
+ _list.remove(entry);
+ _list.insert(entry);
+ }
+
+ bool is_first(Entry const *entry)
+ {
+ return _list.first() == entry;
+ }
+};
+
+
+class Status_window;
+class Menu;
+
+
+class Session_manager
+{
+ private:
+
+ Ncurses &_ncurses;
+ Registry &_registry;
+ Status_window &_status_window;
+ Menu &_menu;
+
+ /**
+ * Update menu if it has the current focus
+ */
+ void _refresh_menu();
+
+ public:
+
+ Session_manager(Ncurses &ncurses, Registry ®istry,
+ Status_window &status_window, Menu &menu);
+
+ void activate_menu();
+ void submit_input(char c);
+ void update_ncurses_screen();
+ void add(Registry::Entry *entry);
+ void remove(Registry::Entry *entry);
+};
+
+
+namespace Terminal {
+
+ class Session_component : public Genode::Rpc_object,
+ public Registry::Entry
+ {
+ public:
+
+ enum { LABEL_MAX_LEN = 128 };
+
+ private:
+
+ Read_buffer _read_buffer;
+
+ Ncurses::Window &_window;
+
+ struct Label
+ {
+ char buf[LABEL_MAX_LEN];
+
+ Label(char const *label)
+ {
+ Genode::strncpy(buf, label, sizeof(buf));
+ }
+ } _label;
+
+ Session_manager &_session_manager;
+
+ Genode::Attached_ram_dataspace _io_buffer;
+
+ Cell_array _char_cell_array;
+ Char_cell_array_character_screen _char_cell_array_character_screen;
+ Terminal::Decoder _decoder;
+
+ Terminal::Position _last_cursor_pos;
+
+
+ public:
+
+ /**
+ * Constructor
+ */
+ Session_component(Genode::size_t io_buffer_size,
+ Ncurses &ncurses,
+ Session_manager &session_manager,
+ char const *label)
+ :
+ _window(*ncurses.create_window(0, 1, ncurses.columns(), ncurses.lines() - 1)),
+ _label(label),
+ _session_manager(session_manager),
+ _io_buffer(Genode::env()->ram_session(), io_buffer_size),
+ _char_cell_array(ncurses.columns(), ncurses.lines() - 1,
+ Genode::env()->heap()),
+ _char_cell_array_character_screen(_char_cell_array),
+ _decoder(_char_cell_array_character_screen)
+ {
+ _session_manager.add(this);
+ }
+
+ ~Session_component()
+ {
+ _session_manager.remove(this);
+ }
+
+
+ /*******************************
+ ** Registry::Entry interface **
+ *******************************/
+
+ void flush()
+ {
+ convert_char_array_to_window(&_char_cell_array, _window);
+
+ int first_dirty_line = 10000,
+ last_dirty_line = -10000;
+
+ for (int line = 0; line < (int)_char_cell_array.num_lines(); line++) {
+ if (!_char_cell_array.line_dirty(line)) continue;
+
+ first_dirty_line = Genode::min(line, first_dirty_line);
+ last_dirty_line = Genode::max(line, last_dirty_line);
+
+ _char_cell_array.mark_line_as_clean(line);
+ }
+
+ Terminal::Position cursor_pos =
+ _char_cell_array_character_screen.cursor_pos();
+ _window.move_cursor(cursor_pos.x, cursor_pos.y);
+
+ _window.refresh();
+ }
+
+ void flush_all()
+ {
+ for (unsigned line = 0; line < _char_cell_array.num_lines(); line++)
+ _char_cell_array.mark_line_as_dirty(line);
+
+ _window.erase();
+ flush();
+ }
+
+ char const *label() const
+ {
+ return _label.buf;
+ }
+
+ void submit_input(char c)
+ {
+ _read_buffer.add(c);
+ }
+
+
+ /********************************
+ ** Terminal session interface **
+ ********************************/
+
+ Size size() { return Size(_char_cell_array.num_cols(),
+ _char_cell_array.num_lines()); }
+
+ bool avail() { return !_read_buffer.empty(); }
+
+ Genode::size_t _read(Genode::size_t dst_len)
+ {
+ /* read data, block on first byte if needed */
+ unsigned num_bytes = 0;
+ unsigned char *dst = _io_buffer.local_addr();
+ Genode::size_t dst_size = Genode::min(_io_buffer.size(), dst_len);
+ do {
+ dst[num_bytes++] = _read_buffer.get();;
+ } while (!_read_buffer.empty() && num_bytes < dst_size);
+
+ return num_bytes;
+ }
+
+ void _write(Genode::size_t num_bytes)
+ {
+ unsigned char *src = _io_buffer.local_addr();
+
+ for (unsigned i = 0; i < num_bytes; i++) {
+
+ /* submit character to sequence decoder */
+ _decoder.insert(src[i]);
+ }
+ }
+
+ Genode::Dataspace_capability _dataspace()
+ {
+ return _io_buffer.cap();
+ }
+
+ void connected_sigh(Genode::Signal_context_capability sigh)
+ {
+ /*
+ * Immediately reflect connection-established signal to the
+ * client because the session is ready to use immediately after
+ * creation.
+ */
+ Genode::Signal_transmitter(sigh).submit();
+ }
+
+ void read_avail_sigh(Genode::Signal_context_capability cap)
+ {
+ _read_buffer.sigh(cap);
+ }
+
+ Genode::size_t read(void *buf, Genode::size_t) { return 0; }
+ Genode::size_t write(void const *buf, Genode::size_t) { return 0; }
+ };
+
+
+ class Root_component : public Genode::Root_component
+ {
+ private:
+
+ Ncurses &_ncurses;
+ Session_manager &_session_manager;
+
+ protected:
+
+ Session_component *_create_session(const char *args)
+ {
+ PLOG("new session args=\"%s\"", args);
+ /*
+ * XXX read I/O buffer size from args
+ */
+ Genode::size_t io_buffer_size = 4096;
+
+ char label[Session_component::LABEL_MAX_LEN];
+ Genode::Arg_string::find_arg(args, "label").string(label, sizeof(label), "");
+
+ return new (md_alloc())
+ Session_component(io_buffer_size, _ncurses, _session_manager, label);
+ }
+
+ public:
+
+ /**
+ * Constructor
+ */
+ Root_component(Genode::Rpc_entrypoint &ep,
+ Genode::Allocator &md_alloc,
+ Ncurses &ncurses,
+ Session_manager &session_manager)
+ :
+ Genode::Root_component(&ep, &md_alloc),
+ _ncurses(ncurses),
+ _session_manager(session_manager)
+ { }
+ };
+}
+
+
+class Status_window
+{
+ private:
+
+ Ncurses &_ncurses;
+ Ncurses::Window &_window;
+
+ char _label[Terminal::Session_component::LABEL_MAX_LEN];
+
+ public:
+
+ Status_window(Ncurses &ncurses)
+ :
+ _ncurses(ncurses),
+ _window(*_ncurses.create_window(0, 0, ncurses.columns(), 1))
+ {
+ _label[0] = 0;
+ }
+
+ void label(char const *label)
+ {
+ Genode::strncpy(_label, label, sizeof(_label));
+
+ _window.erase();
+ _window.move_cursor(0, 0);
+ _window.print_char('[', false, false);
+
+ unsigned const max_columns = _ncurses.columns() - 2;
+ for (unsigned i = 0; i < max_columns && _label[i]; i++)
+ _window.print_char(_label[i], false, false);
+
+ _window.print_char(']', false, false);
+ _window.refresh();
+ }
+};
+
+
+class Menu : public Registry::Entry
+{
+ private:
+
+ Ncurses &_ncurses;
+ Ncurses::Window &_window;
+ Status_window &_status_window;
+ Registry &_registry;
+ unsigned _selected_idx;
+ unsigned _max_idx;
+
+ /**
+ * State tracker for escape sequences within user input
+ *
+ * This tracker is used to decode special keys (e.g., cursor keys).
+ */
+ struct Seq_tracker
+ {
+ enum State { INIT, GOT_ESC, GOT_FIRST } state;
+ char normal, first, second;
+ bool sequence_complete;
+
+ Seq_tracker() : state(INIT), sequence_complete(false) { }
+
+ void input(char c)
+ {
+ switch (state) {
+ case INIT:
+ if (c == 27)
+ state = GOT_ESC;
+ else
+ normal = c;
+ sequence_complete = false;
+ break;
+
+ case GOT_ESC:
+ first = c;
+ state = GOT_FIRST;
+ break;
+
+ case GOT_FIRST:
+ second = c;
+ state = INIT;
+ sequence_complete = true;
+ break;
+ }
+ }
+
+ bool is_normal() const { return state == INIT && !sequence_complete; }
+
+ bool _fn_complete(char match_first, char match_second) const
+ {
+ return sequence_complete
+ && first == match_first
+ && second == match_second;
+ }
+
+ bool is_key_up() const { return _fn_complete(91, 65); }
+ bool is_key_down() const { return _fn_complete(91, 66); }
+ };
+
+ Seq_tracker _seq_tracker;
+
+ public:
+
+ Menu(Ncurses &ncurses, Registry ®istry, Status_window &status_window)
+ :
+ _ncurses(ncurses),
+ _window(*_ncurses.create_window(0, 1,
+ ncurses.columns(),
+ ncurses.lines() - 1)),
+ _status_window(status_window),
+ _registry(registry),
+ _selected_idx(0),
+ _max_idx(0)
+ { }
+
+ void reset_selection() { _selected_idx = 0; }
+
+ void flush() { }
+
+ void flush_all()
+ {
+ _window.erase();
+
+ unsigned const max_columns = _ncurses.columns() - 1;
+
+ for (unsigned i = 0; i < _ncurses.lines() - 2; i++) {
+
+ Registry::Entry *entry = _registry.entry_at(i + 1);
+ if (!entry) {
+ _max_idx = i - 1;
+ break;
+ }
+
+ bool const highlight = (i == _selected_idx);
+ if (highlight)
+ _window.horizontal_line(i + 1);
+
+ unsigned const padding = 2;
+ _window.move_cursor(padding, 1 + i);
+
+ char const *label = entry->label();
+ for (unsigned j = 0; j < (max_columns - padding) && label[j]; j++)
+ _window.print_char(label[j], highlight, highlight);
+ }
+
+ _ncurses.cursor_visible(false);
+ _window.refresh();
+ }
+
+ char const *label() const { return "-"; }
+
+ void submit_input(char c)
+ {
+ _seq_tracker.input(c);
+
+ if (_seq_tracker.is_key_up()) {
+ if (_selected_idx > 0)
+ _selected_idx--;
+ flush_all();
+ }
+
+ if (_seq_tracker.is_key_down()) {
+ if (_selected_idx < _max_idx)
+ _selected_idx++;
+ flush_all();
+ }
+
+ /*
+ * Detect selection of menu entry via [enter]
+ */
+ if (_seq_tracker.is_normal() && _seq_tracker.normal == 13) {
+
+ Entry *entry = _registry.entry_at(_selected_idx + 1);
+ if (entry) {
+ _registry.to_front(entry);
+
+ /* update status window */
+ _status_window.label(_registry.entry_at(0)->label());
+
+ _ncurses.cursor_visible(true);
+ entry->flush_all();
+ }
+ }
+ }
+};
+
+
+struct User_input
+{
+ Ncurses &_ncurses;
+
+ User_input(Ncurses &ncurses) : _ncurses(ncurses) { }
+
+ int read_character()
+ {
+ return _ncurses.read_character();
+ }
+};
+
+
+/************************************
+ ** Session manager implementation **
+ ************************************/
+
+Session_manager::Session_manager(Ncurses &ncurses, Registry ®istry,
+ Status_window &status_window, Menu &menu)
+:
+ _ncurses(ncurses), _registry(registry), _status_window(status_window),
+ _menu(menu)
+{ }
+
+
+void Session_manager::_refresh_menu()
+{
+ if (_registry.is_first(&_menu))
+ activate_menu();
+}
+
+
+void Session_manager::activate_menu()
+{
+ _menu.reset_selection();
+ _registry.to_front(&_menu);
+ _status_window.label(_menu.label());
+ _menu.flush_all();
+}
+
+
+void Session_manager::submit_input(char c)
+{
+ Registry::Entry *focused = _registry.entry_at(0);
+ if (focused)
+ focused->submit_input(c);
+}
+
+
+void Session_manager::update_ncurses_screen()
+{
+ Registry::Entry *focused = _registry.entry_at(0);
+ if (focused)
+ focused->flush();
+ _ncurses.do_update();
+}
+
+
+void Session_manager::add(Registry::Entry *entry)
+{
+ _registry.add(entry);
+ _refresh_menu();
+}
+
+
+void Session_manager::remove(Registry::Entry *entry)
+{
+ _registry.remove(entry);
+ _refresh_menu();
+}
+
+
+/*******************
+ ** Input handler **
+ *******************/
+
+struct Input_handler
+{
+ GENODE_RPC(Rpc_handle_input, void, handle);
+ GENODE_RPC_INTERFACE(Rpc_handle_input);
+};
+
+
+struct Input_handler_component : Genode::Rpc_object
+{
+ User_input &_user_input;
+ Session_manager &_session_manager;
+
+
+ Input_handler_component(User_input &user_input,
+ Session_manager &session_manager)
+ :
+ _user_input(user_input),
+ _session_manager(session_manager)
+ {
+ _session_manager.activate_menu();
+ }
+
+ void handle()
+ {
+ for (;;) {
+ int c = _user_input.read_character();
+ if (c == -1)
+ break;
+
+ /*
+ * Quirk needed when using 'qemu -serial stdio'. In this case,
+ * backspace is wrongly reported as 127.
+ */
+ if (c == 127)
+ c = 8;
+
+ /*
+ * Handle C-y by switching to the menu
+ */
+ enum { KEYCODE_C_X = 24 };
+ if (c == KEYCODE_C_X) {
+ _session_manager.activate_menu();
+ } else {
+ _session_manager.submit_input(c);
+ }
+ }
+
+ _session_manager.update_ncurses_screen();
+ }
+};
+
+
+int main(int, char **)
+{
+ using namespace Genode;
+
+ printf("--- terminal_mux service started ---\n");
+
+ static Cap_connection cap;
+
+ /* initialize entry point that serves the root interface */
+ enum { STACK_SIZE = sizeof(addr_t)*4096 };
+ static Rpc_entrypoint ep(&cap, STACK_SIZE, "terminal_mux_ep");
+
+ static Sliced_heap sliced_heap(env()->ram_session(), env()->rm_session());
+
+ static Registry registry;
+ static Ncurses ncurses;
+ static Status_window status_window(ncurses);
+ static Menu menu(ncurses, registry, status_window);
+
+ registry.add(&menu);
+
+ static User_input user_input(ncurses);
+ static Session_manager session_manager(ncurses, registry, status_window, menu);
+
+ /* create root interface for service */
+ static Terminal::Root_component root(ep, sliced_heap, ncurses, session_manager);
+
+ static Input_handler_component input_handler(user_input, session_manager);
+ Capability input_handler_cap = ep.manage(&input_handler);
+
+ /* announce service at our parent */
+ env()->parent()->announce(ep.manage(&root));
+
+ while (1) {
+ static Timer::Connection timer;
+ timer.msleep(10);
+
+ input_handler_cap.call();
+ }
+ return 0;
+}
diff --git a/gems/src/server/terminal_mux/ncurses.cc b/gems/src/server/terminal_mux/ncurses.cc
new file mode 100644
index 000000000..65f858133
--- /dev/null
+++ b/gems/src/server/terminal_mux/ncurses.cc
@@ -0,0 +1,112 @@
+/*
+ * \brief C++ wrapper for ncurses API
+ * \author Norman Feske
+ * \date 2013-02-21
+ */
+
+/* Genode includes */
+#include
+#include
+
+/* libc and ncurses includes */
+#include
+#include /* for 'setenv()' */
+#include /* for 'open()' */
+#include
+#include /* for 'dup2()' */
+
+/* local includes */
+#include
+
+
+struct Ncurses::Window::Ncurses_window : WINDOW { };
+
+
+Ncurses::Window::Window(unsigned x, unsigned y, unsigned w, unsigned h)
+:
+ _window(static_cast(newwin(h, w, y, x))),
+ _w(w)
+{ }
+
+
+void Ncurses::Window::move_cursor(unsigned x, unsigned y)
+{
+ wmove(_window, y, x);
+}
+
+
+void Ncurses::Window::print_char(unsigned long const c, bool highlight, bool inverse)
+{
+ waddch(_window, c | (highlight ? A_STANDOUT : 0)
+ | (inverse ? A_REVERSE : 0));
+}
+
+
+void Ncurses::Window::refresh()
+{
+ wnoutrefresh(_window);
+}
+
+
+void Ncurses::Window::erase()
+{
+ werase(_window);
+}
+
+
+void Ncurses::Window::horizontal_line(int line)
+{
+ mvwhline(_window, line, 0, ' ' | A_REVERSE, _w);
+}
+
+
+Ncurses::Window *Ncurses::create_window(int x, int y, int w, int h)
+{
+ return new (Genode::env()->heap()) Ncurses::Window(x, y, w, h);
+}
+
+
+void Ncurses::do_update()
+{
+ doupdate();
+}
+
+
+void Ncurses::cursor_visible(bool visible)
+{
+ if (!visible)
+ wmove(stdscr, _lines - 1, 0);
+}
+
+
+int Ncurses::read_character()
+{
+ return getch();
+}
+
+
+Ncurses::Ncurses()
+{
+ /*
+ * Redirect stdio to terminal
+ */
+ char const *device_name = "/dev/terminal";
+ int fd = open(device_name, O_RDWR);
+ if (fd < 0) {
+ PERR("Error: could not open %s", device_name);
+ return;
+ }
+ dup2(fd, 0);
+ dup2(fd, 1);
+ dup2(fd, 2);
+
+ setenv("TERM", "vt102", 1);
+
+ initscr();
+ nonl();
+ noecho();
+ nodelay(stdscr, true);
+ cbreak();
+ getmaxyx(stdscr, _lines, _columns);
+}
+
diff --git a/gems/src/server/terminal_mux/ncurses_cxx.h b/gems/src/server/terminal_mux/ncurses_cxx.h
new file mode 100644
index 000000000..366a90adb
--- /dev/null
+++ b/gems/src/server/terminal_mux/ncurses_cxx.h
@@ -0,0 +1,60 @@
+/*
+ * \brief C++ wrapper for ncurses API
+ * \author Norman Feske
+ * \date 2013-02-21
+ */
+
+#ifndef _NCURSES_CXX_H_
+#define _NCURSES_CXX_H_
+
+class Ncurses
+{
+ private:
+
+ unsigned _columns;
+ unsigned _lines;
+
+ public:
+
+ class Window
+ {
+ private:
+
+ struct Ncurses_window;
+
+ friend class Ncurses;
+
+ Ncurses_window * const _window;
+
+ int _w;
+
+ Window(unsigned x, unsigned y, unsigned w, unsigned h);
+
+ public:
+
+ void move_cursor(unsigned x, unsigned y);
+
+ void print_char(unsigned long const c, bool highlight, bool inverse);
+
+ void refresh();
+
+ void erase();
+
+ void horizontal_line(int line);
+ };
+
+ Window *create_window(int x, int y, int w, int h);
+
+ void do_update();
+
+ Ncurses();
+
+ void cursor_visible(bool);
+
+ int read_character();
+
+ unsigned columns() const { return _columns; }
+ unsigned lines() const { return _lines; }
+};
+
+#endif /* _NCURSES_CXX_H_ */
diff --git a/gems/src/server/terminal_mux/target.mk b/gems/src/server/terminal_mux/target.mk
new file mode 100644
index 000000000..e1c81b77e
--- /dev/null
+++ b/gems/src/server/terminal_mux/target.mk
@@ -0,0 +1,4 @@
+TARGET = terminal_mux
+SRC_CC = main.cc ncurses.cc
+LIBS = libc libc_terminal ncurses
+INC_DIR += $(PRG_DIR)