mirror of
https://github.com/genodelabs/genode.git
synced 2025-02-20 17:52:52 +00:00
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:
parent
97b27ee62f
commit
a61bd71a4f
@ -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
158
gems/run/terminal_mux.run
Normal 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
|
19
gems/src/server/terminal_mux/README
Normal file
19
gems/src/server/terminal_mux/README
Normal 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.
|
||||
|
746
gems/src/server/terminal_mux/main.cc
Normal file
746
gems/src/server/terminal_mux/main.cc
Normal 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 ®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<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 ®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<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;
|
||||
}
|
112
gems/src/server/terminal_mux/ncurses.cc
Normal file
112
gems/src/server/terminal_mux/ncurses.cc
Normal 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);
|
||||
}
|
||||
|
60
gems/src/server/terminal_mux/ncurses_cxx.h
Normal file
60
gems/src/server/terminal_mux/ncurses_cxx.h
Normal 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_ */
|
4
gems/src/server/terminal_mux/target.mk
Normal file
4
gems/src/server/terminal_mux/target.mk
Normal file
@ -0,0 +1,4 @@
|
||||
TARGET = terminal_mux
|
||||
SRC_CC = main.cc ncurses.cc
|
||||
LIBS = libc libc_terminal ncurses
|
||||
INC_DIR += $(PRG_DIR)
|
Loading…
x
Reference in New Issue
Block a user