Terminal multiplexer

The new terminal_mux server is able to provide multiple terminal
sessions over one terminal-client session. The user can switch
between the different sessions using the keyboard shortcut C-y,
which brings up an ncurses-based menu.
This commit is contained in:
Norman Feske 2013-02-21 11:44:46 +01:00
parent 97b27ee62f
commit a61bd71a4f
7 changed files with 1104 additions and 0 deletions

View File

@ -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);

158
gems/run/terminal_mux.run Normal file
View File

@ -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 {
<config verbose="yes">
<parent-provides>
<service name="ROM"/>
<service name="LOG"/>
<service name="CAP"/>
<service name="RAM"/>
<service name="RM"/>
<service name="CPU"/>
<service name="PD"/>
<service name="IRQ"/>
<service name="IO_PORT"/>
<service name="IO_MEM"/>
<service name="SIGNAL"/>
</parent-provides>
<default-route>
<any-service> <any-child/> <parent/> </any-service>
</default-route>
<start name="timer">
<resource name="RAM" quantum="1M"/>
<provides><service name="Timer"/></provides>
<route> <any-service> <parent/> </any-service> </route>
</start>
<start name="uart_drv">
<resource name="RAM" quantum="1M"/>
<provides>
<service name="Uart"/>
<service name="Terminal"/>
</provides>
<config>
<policy label="terminal_mux" uart="1" detect_size="yes"/>
<policy label="noux" uart="1" detect_size="yes"/>
</config>
<route> <any-service> <parent/> </any-service> </route>
</start>
<start name="terminal_mux">
<resource name="RAM" quantum="2M"/>
<provides>
<service name="Terminal"/>
</provides>
<route>
<any-service> <child name="uart_drv" /> <parent/> <any-child /> </any-service>
</route>
</start>
<start name="terminal_log">
<resource name="RAM" quantum="2M"/>
<provides>
<service name="LOG"/>
</provides>
<route>
<any-service> <child name="terminal_mux" /> <any-child/> <parent/> </any-service>
</route>
</start>
<start name="noux">
<resource name="RAM" quantum="16M"/>
<config>
<fstab> <tar name="vim.tar" /> </fstab>
<start name="/bin/vim">
<env name="TERM" value="linux" />
<!-- Deactivate the loading of plugins. Otherwise, vim will
attempt to use a sub shell for pattern matching -->
<arg value="--noplugin" />
<!-- Do not use swap file. Any attempt to create of would
fail because we are on a read-only file system -->
<arg value="-n" />
<!-- Use the nocompatible mode, which is much nicer than
the plain vi mode -->
<arg value="-N" />
<!-- Permanently display status bar -->
<arg value="--cmd" />
<arg value="set laststatus=2" />
<!-- Enable highlighted search results -->
<arg value="--cmd" />
<arg value="set hls" />
</start>
</config>
<route>
<any-service> <child name="terminal_mux" /> <any-child/> <parent/> </any-service>
</route>
</start>
<start name="noux.2">
<binary name="noux"/>
<resource name="RAM" quantum="16M"/>
<config>
<fstab> <tar name="vim.tar" /> </fstab>
<start name="/bin/vim">
<env name="TERM" value="linux" />
<!-- Deactivate the loading of plugins. Otherwise, vim will
attempt to use a sub shell for pattern matching -->
<arg value="--noplugin" />
<!-- Do not use swap file. Any attempt to create of would
fail because we are on a read-only file system -->
<arg value="-n" />
<!-- Use the nocompatible mode, which is much nicer than
the plain vi mode -->
<arg value="-N" />
<!-- Permanently display status bar -->
<arg value="--cmd" />
<arg value="set laststatus=2" />
<!-- Enable highlighted search results -->
<arg value="--cmd" />
<arg value="set hls" />
</start>
</config>
<route>
<any-service> <child name="terminal_mux" /> <any-child/> <parent/> </any-service>
</route>
</start>
</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

View File

@ -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.

View File

@ -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 <base/env.h>
#include <base/printf.h>
#include <base/heap.h>
#include <base/sleep.h>
#include <util/arg_string.h>
#include <cap_session/connection.h>
#include <root/component.h>
#include <os/attached_ram_dataspace.h>
#include <timer_session/connection.h>
/* terminal includes */
#include <terminal/decoder.h>
#include <terminal/types.h>
#include <terminal/read_buffer.h>
#include <terminal/char_cell_array_character_screen.h>
#include <terminal_session/terminal_session.h>
/* local includes */
#include <ncurses_cxx.h>
/*
* Convert character array into ncurses window
*/
static void convert_char_array_to_window(Cell_array<Char_cell> *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<Entry>::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<Entry> _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 &registry,
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<Session, Session_component>,
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> _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<unsigned char>();
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<unsigned char>();
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<Session_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), "<unlabeled>");
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<Session_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 &registry, 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 &registry,
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<Input_handler,
Input_handler_component>
{
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> 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<Input_handler::Rpc_handle_input>();
}
return 0;
}

View File

@ -0,0 +1,112 @@
/*
* \brief C++ wrapper for ncurses API
* \author Norman Feske
* \date 2013-02-21
*/
/* Genode includes */
#include <base/printf.h>
#include <base/env.h>
/* libc and ncurses includes */
#include <ncurses.h>
#include <stdlib.h> /* for 'setenv()' */
#include <sys/types.h> /* for 'open()' */
#include <fcntl.h>
#include <unistd.h> /* for 'dup2()' */
/* local includes */
#include <ncurses_cxx.h>
struct Ncurses::Window::Ncurses_window : WINDOW { };
Ncurses::Window::Window(unsigned x, unsigned y, unsigned w, unsigned h)
:
_window(static_cast<Ncurses::Window::Ncurses_window *>(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);
}

View File

@ -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_ */

View File

@ -0,0 +1,4 @@
TARGET = terminal_mux
SRC_CC = main.cc ncurses.cc
LIBS = libc libc_terminal ncurses
INC_DIR += $(PRG_DIR)