From a61bd71a4fe0bc2631739ee5b046aafeed25ed51 Mon Sep 17 00:00:00 2001 From: Norman Feske Date: Thu, 21 Feb 2013 11:44:46 +0100 Subject: [PATCH] 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. --- gems/include/terminal/cell_array.h | 5 + gems/run/terminal_mux.run | 158 +++++ gems/src/server/terminal_mux/README | 19 + gems/src/server/terminal_mux/main.cc | 746 +++++++++++++++++++++ gems/src/server/terminal_mux/ncurses.cc | 112 ++++ gems/src/server/terminal_mux/ncurses_cxx.h | 60 ++ gems/src/server/terminal_mux/target.mk | 4 + 7 files changed, 1104 insertions(+) create mode 100644 gems/run/terminal_mux.run create mode 100644 gems/src/server/terminal_mux/README create mode 100644 gems/src/server/terminal_mux/main.cc create mode 100644 gems/src/server/terminal_mux/ncurses.cc create mode 100644 gems/src/server/terminal_mux/ncurses_cxx.h create mode 100644 gems/src/server/terminal_mux/target.mk 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)