diff --git a/repos/gems/recipes/src/terminal/used_apis b/repos/gems/recipes/src/terminal/used_apis index aec14c9df..ad8294e06 100644 --- a/repos/gems/recipes/src/terminal/used_apis +++ b/repos/gems/recipes/src/terminal/used_apis @@ -5,5 +5,6 @@ input_session nitpicker_gfx terminal_session timer_session +report_session vfs gems diff --git a/repos/gems/src/server/terminal/README b/repos/gems/src/server/terminal/README index b2ae8a590..2a3ec4dbb 100644 --- a/repos/gems/src/server/terminal/README +++ b/repos/gems/src/server/terminal/README @@ -1,14 +1,14 @@ This is a graphical terminal implementation. It provides the Terminal service and uses a nitpicker session for screen representation. -Configuration -~~~~~~~~~~~~~ + +Color configuration +~~~~~~~~~~~~~~~~~~~ The default color palette can be configured via the XML configuration node like follows. There are 16 colors configurable - index 0-7 normal color and index 8-15 bright (bold) colors. - ! ! ! @@ -17,3 +17,17 @@ index 0-7 normal color and index 8-15 bright (bold) colors. ! ... ! + +Clipboard support +~~~~~~~~~~~~~~~~~ + +With the '' attribute 'copy="yes"' specified, the terminal allows +the user to select text to be reported to a "clipboard" report. The selection +mode is activated by holding the left shift key. While the selection mode +is active, the text position under mouse pointer is highlighted and the +user can select text via the left mouse button. Upon release of the mouse +button, the selection is reported. + +Vice versa, with the '' attribute 'paste="yes"' specified, the +terminal allows the user to paste the content of a "clipboard" ROM session +to the terminal client by pressing the middle mouse button. diff --git a/repos/gems/src/server/terminal/main.cc b/repos/gems/src/server/terminal/main.cc index dd520c7bb..fbacec3b4 100644 --- a/repos/gems/src/server/terminal/main.cc +++ b/repos/gems/src/server/terminal/main.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,9 @@ struct Terminal::Main : Character_consumer Color_palette _color_palette { }; + Constructible _clipboard_rom { }; + Constructible _clipboard_reporter { }; + void _handle_config(); Signal_handler
_config_handler { @@ -82,6 +86,14 @@ struct Terminal::Main : Character_consumer Framebuffer _framebuffer { _env, _config_handler }; + Point _pointer { }; /* pointer positon in pixels */ + + bool _shift_pressed = false; + + bool _selecting = false; + + struct Paste_buffer { char buffer[READ_BUFFER_SIZE]; } _paste_buffer { }; + typedef Pixel_rgb565 PT; Constructible> _text_screen_surface { }; @@ -139,6 +151,9 @@ struct Terminal::Main : Character_consumer Signal_handler
_input_handler { _env.ep(), *this, &Main::_handle_input }; + void _report_clipboard_selection(); + void _paste_clipboard_content(); + Main(Env &env) : _env(env) { _timer .sigh(_flush_handler); @@ -170,6 +185,12 @@ void Terminal::Main::_handle_config() _font.construct(_heap, _root_dir, cache_limit); + _clipboard_reporter.conditional(config.attribute_value("copy", false), + _env, "clipboard", "clipboard"); + + _clipboard_rom.conditional(config.attribute_value("paste", false), + _env, "clipboard"); + /* * Adapt terminal to font or framebuffer mode changes */ @@ -252,6 +273,56 @@ void Terminal::Main::_handle_input() { _input.for_each_event([&] (Input::Event const &event) { + event.handle_absolute_motion([&] (int x, int y) { + + _pointer = Point(x, y); + + if (_shift_pressed) { + _text_screen_surface->pointer(_pointer); + _schedule_flush(); + } + + if (_selecting) { + _text_screen_surface->define_selection(_pointer); + _schedule_flush(); + } + }); + + if (event.key_press(Input::KEY_LEFTSHIFT)) { + if (_clipboard_reporter.constructed()) { + _shift_pressed = true; + _text_screen_surface->clear_selection(); + _text_screen_surface->pointer(_pointer); + _schedule_flush(); + } + } + + if (event.key_release(Input::KEY_LEFTSHIFT)) { + _shift_pressed = false; + _text_screen_surface->pointer(Point(-1, -1)); + _schedule_flush(); + } + + if (event.key_press(Input::BTN_LEFT)) { + if (_shift_pressed) { + _selecting = true; + _text_screen_surface->start_selection(_pointer); + } else { + _text_screen_surface->clear_selection(); + } + _schedule_flush(); + } + + if (event.key_release(Input::BTN_LEFT)) { + if (_selecting) { + _selecting = false; + _report_clipboard_selection(); + } + } + + if (event.key_press(Input::BTN_MIDDLE)) + _paste_clipboard_content(); + event.handle_press([&] (Input::Keycode, Codepoint codepoint) { /* function-key unicodes */ @@ -304,4 +375,56 @@ void Terminal::Main::_handle_input() } +void Terminal::Main::_report_clipboard_selection() +{ + if (!_clipboard_reporter.constructed()) + return; + + _clipboard_reporter->generate([&] (Xml_generator &xml) { + _text_screen_surface->for_each_selected_character([&] (Codepoint c) { + String<10> const utf8(c); + if (utf8.valid()) + xml.append_sanitized(utf8.string(), utf8.length() - 1); + }); + }); +} + + +void Terminal::Main::_paste_clipboard_content() +{ + if (!_clipboard_rom.constructed()) + return; + + _clipboard_rom->update(); + + _paste_buffer = { }; + + /* leave last byte as zero-termination in tact */ + size_t const max_len = sizeof(_paste_buffer.buffer) - 1; + size_t const len = + _clipboard_rom->xml().decoded_content(_paste_buffer.buffer, max_len); + + if (len == max_len) { + warning("clipboard content exceeds paste buffer"); + return; + } + + if (len >= (size_t)_read_buffer.avail_capacity()) { + warning("clipboard content exceeds read-buffer capacity"); + return; + } + + for (Utf8_ptr utf8(_paste_buffer.buffer); utf8.complete(); utf8 = utf8.next()) { + + Codepoint const c = utf8.codepoint(); + + /* filter out control characters */ + if (c.value < 32 && c.value != 10) + continue; + + _read_buffer.add(c); + } +} + + void Component::construct(Genode::Env &env) { static Terminal::Main main(env); } diff --git a/repos/gems/src/server/terminal/text_screen_surface.h b/repos/gems/src/server/terminal/text_screen_surface.h index 3dba87959..ac038323b 100644 --- a/repos/gems/src/server/terminal/text_screen_surface.h +++ b/repos/gems/src/server/terminal/text_screen_surface.h @@ -91,6 +91,17 @@ class Terminal::Text_screen_surface Point start() const { return Point(1, 1); } bool valid() const { return columns*lines > 0; } + + /** + * Return character position at given pixel coordinates + */ + Position position(Point p) const + { + if (char_width.value == 0 || char_height == 0) + return Position { }; + + return Position((p.x() << 8) / char_width.value, p.y() / char_height); + } }; /** @@ -133,6 +144,29 @@ class Terminal::Text_screen_surface Decoder _decoder { _character_screen }; + struct Selection + { + Position start { }; + Position end { }; + + bool defined = false; + + bool selected(Position pos) const + { + return defined && pos.in_range(start, end); + } + + template + void for_each_line(FN const &fn) const + { + for (int i = min(start.y, end.y); i <= max(start.y, end.y); i++) + fn(i); + } + + } _selection { }; + + Position _pointer { -1, -1 }; + public: /** @@ -199,7 +233,20 @@ class Terminal::Text_screen_surface Char_cell const cell = _cell_array.get_cell(column, line); - _font.apply_glyph(cell.codepoint(), [&] (Glyph_painter::Glyph const &glyph) { + Codepoint codepoint = cell.codepoint(); + + /* display absent codepoints as whitespace */ + bool const codepoint_valid = (codepoint.value != 0); + + bool const selected = _selection.selected(Position(column, line)) + && codepoint_valid; + + bool const pointer = (_pointer == Position(column, line)); + + if (!codepoint_valid) + codepoint = Codepoint{' '}; + + _font.apply_glyph(codepoint, [&] (Glyph_painter::Glyph const &glyph) { Color_palette::Highlighted const highlighted { cell.highlight() }; @@ -216,6 +263,16 @@ class Terminal::Text_screen_surface Color fg_color = _palette.foreground(fg_idx, highlighted); Color bg_color = _palette.background(bg_idx, highlighted); + if (selected) { + bg_color = Color(180, 180, 180); + fg_color = Color( 50, 50, 50); + } + + if (pointer) { + bg_color = Color(220, 220, 220); + fg_color = Color( 50, 50, 50); + } + if (cell.has_cursor()) { fg_color = Color( 63, 63, 63); bg_color = Color(255, 255, 255); @@ -271,6 +328,8 @@ class Terminal::Text_screen_surface void apply_character(Character c) { + clear_selection(); + /* submit character to sequence decoder */ _decoder.insert(c); } @@ -284,6 +343,110 @@ class Terminal::Text_screen_surface * Return size in colums/rows */ Area size() const { return _geometry.size(); } + + /** + * Set pointer position in pixels (to show the cursor) + */ + void pointer(Point pointer) + { + auto position_valid = [&] (Position pos) { + return pos.y >= 0 && pos.y < (int)_geometry.lines; }; + + /* update old position */ + if (position_valid(_pointer)) + _cell_array.mark_line_as_dirty(_pointer.y); + + _pointer = _geometry.position(pointer); + + /* update new position */ + if (position_valid(_pointer)) + _cell_array.mark_line_as_dirty(_pointer.y); + } + + /** + * Set anchor point of selection + * + * \param pointer pointer position in pixels + */ + void start_selection(Point pointer) + { + if (_selection.defined) + clear_selection(); + + _selection.start = _geometry.position(pointer); + + define_selection(pointer); + } + + /** + * Set end position of current selection + * + * \param pointer pointer position in pixels + */ + void define_selection(Point pointer) + { + _selection.for_each_line([&] (int line) { + _cell_array.mark_line_as_dirty(line); }); + + _selection.end = _geometry.position(pointer); + _selection.defined = true; + + _selection.for_each_line([&] (int line) { + _cell_array.mark_line_as_dirty(line); }); + } + + void clear_selection() + { + if (!_selection.defined) + return; + + _selection.for_each_line([&] (int line) { + _cell_array.mark_line_as_dirty(line); }); + + _selection.defined = false; + } + + template + void for_each_selected_character(FN const &fn) const + { + for (unsigned row = 0; row < _geometry.lines; row++) { + bool skip_remaining_chars_on_row = false; + + for (unsigned column = 0; column < _geometry.columns; column++) { + + if (skip_remaining_chars_on_row) + continue; + + if (!_selection.selected(Position(column, row))) + continue; + + Codepoint const c { _cell_array.get_cell(column, row).value }; + + if (c.value == 0) { + + auto remaining_line_empty = [&] () + { + for (unsigned i = column + 1; i < _geometry.columns; i++) + if (_cell_array.get_cell(i, row).value) + return false; + + return true; + }; + + /* generate one line break at the end of a selected line */ + if (remaining_line_empty()) { + fn(Codepoint{'\n'}); + skip_remaining_chars_on_row = true; + + } else { + fn(Codepoint{' '}); + } + } else { + fn(c); + } + } + } + } }; #endif /* _TEXT_SCREEN_SURFACE_H_ */ diff --git a/repos/os/include/terminal/char_cell_array_character_screen.h b/repos/os/include/terminal/char_cell_array_character_screen.h index d6c218b23..16b694121 100644 --- a/repos/os/include/terminal/char_cell_array_character_screen.h +++ b/repos/os/include/terminal/char_cell_array_character_screen.h @@ -20,7 +20,7 @@ struct Char_cell { - Genode::uint16_t value { ' ' }; + Genode::uint16_t value { 0 }; unsigned char attr; unsigned char color; diff --git a/repos/os/include/terminal/types.h b/repos/os/include/terminal/types.h index 07499879b..7bd8f3ee0 100644 --- a/repos/os/include/terminal/types.h +++ b/repos/os/include/terminal/types.h @@ -76,6 +76,23 @@ struct Terminal::Position bool operator != (Position const &pos) const { return (pos.x != x) || (pos.y != y); } + bool operator >= (Position const &other) const + { + if (y > other.y) + return true; + + if (y == other.y && x >= other.x) + return true; + + return false; + } + + bool in_range(Position start, Position end) const + { + return (end >= start) ? *this >= start && end >= *this + : *this >= end && start >= *this; + } + /** * Return true if position lies within the specified boundaries */