[vte/wip/egmont/bidi: 3/7] bidi: Find the BiDi mapping for the RingView's contents



commit 76726690b558113d9eb873a1e06182c6a23b7b8f
Author: Egmont Koblinger <egmont gmail com>
Date:   Sat Jun 1 15:34:13 2019 +0200

    bidi: Find the BiDi mapping for the RingView's contents
    
    Find the mapping between logical and visual positions for the rows
    of the RingView, according to the paragraph's BiDi parameters.
    
    For implicit paragraphs, the Unicode Bidirectional Algorithm is run
    via the FriBidi library.
    
    Basic Arabic shaping is also performed using presentation form
    characters.

 meson.build       |   9 +
 meson_options.txt |   7 +
 src/bidi.cc       | 792 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/bidi.hh       | 114 ++++++++
 src/meson.build   |   3 +
 src/ringview.cc   |  93 ++++++-
 src/ringview.hh   |  19 ++
 src/vte.cc        |   2 +
 src/vtegtk.cc     |   6 +
 src/vteunistr.cc  |  33 +++
 src/vteunistr.h   |  23 ++
 11 files changed, 1096 insertions(+), 5 deletions(-)
---
diff --git a/meson.build b/meson.build
index 3760b3f8..451195b9 100644
--- a/meson.build
+++ b/meson.build
@@ -34,6 +34,7 @@ project(
 gtk3_req_version          = '3.8.0'
 gtk4_req_version          = '4.0.0'
 
+fribidi_req_version       = '1.0.0'
 gio_req_version           = '2.40.0'
 glib_req_version          = '2.40.0'
 gnutls_req_version        = '3.2.7'
@@ -109,6 +110,7 @@ config_h = configuration_data()
 config_h.set_quoted('GETTEXT_PACKAGE', vte_gettext_domain)
 config_h.set_quoted('VERSION', vte_version)
 config_h.set('VTE_DEBUG', enable_debug)
+config_h.set('WITH_FRIBIDI', get_option('fribidi'))
 config_h.set('WITH_GNUTLS', get_option('gnutls'))
 config_h.set('WITH_ICONV', get_option('iconv'))
 
@@ -373,6 +375,12 @@ pcre2_dep    = dependency('libpcre2-8', version: '>=' + pcre2_req_version)
 pthreads_dep = dependency('threads')
 zlib_dep     = dependency('zlib')
 
+if get_option('fribidi')
+  fribidi_dep = dependency('fribidi', version: '>=' + fribidi_req_version)
+else
+  fribidi_dep = dependency('', required: false)
+endif
+
 if get_option('gnutls')
   gnutls_dep = dependency('gnutls', version: '>=' + gnutls_req_version)
 else
@@ -443,6 +451,7 @@ output += '\n'
 output += '  Coverage:     ' + get_option('b_coverage').to_string() + '\n'
 output += '  Debug:        ' + enable_debug.to_string() + '\n'
 output += '  Docs:         ' + get_option('docs').to_string() + '\n'
+output += '  FRIBIDI:      ' + get_option('fribidi').to_string() + '\n'
 output += '  GNUTLS:       ' + get_option('gnutls').to_string() + '\n'
 output += '  GTK+ 3.0:     ' + get_option('gtk3').to_string() + '\n'
 output += '  GTK+ 4.0:     ' + get_option('gtk4').to_string() + '\n'
diff --git a/meson_options.txt b/meson_options.txt
index b8a11a84..d29c66a3 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -34,6 +34,13 @@ option(
   description: 'Enable GObject Introspection',
 )
 
+option(
+  'fribidi',
+  type: 'boolean',
+  value: true,
+  description: 'Enable FriBidi support',
+)
+
 option(
   'gnutls',
   type: 'boolean',
diff --git a/src/bidi.cc b/src/bidi.cc
new file mode 100644
index 00000000..44e1502e
--- /dev/null
+++ b/src/bidi.cc
@@ -0,0 +1,792 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/*
+ * A BidiRow object stores the BiDi mapping between logical and visual positions
+ * for one visual line of text. (Characters are always shuffled within a line,
+ * never across lines; although how they are shuffled within a line depends on
+ * the rest of the paragraph too.)
+ *
+ * It also stores additional per-character properties: the character's direction
+ * (needed for mirroring and mouse selecting) and Arabic shaping (as currently
+ * done using presentation form characters, although HarfBuzz would probably be
+ * a better approach).
+ *
+ * There are per-line properties as well, which are actually per-paragraph
+ * properties stored for each line: the overall potentially autodetected
+ * direction (needed for keyboard arrow swapping), and whether the paragraph
+ * contains any foreign direction character (used for the cursor shape).
+ *
+ * Note that the trivial LTR mapping, with no RTL or shaped characters at all,
+ * might be denoted by setting the BidiRow's width to 0.
+ *
+ * BidiRunner is a collection of methods that run the BiDi algorithm on one
+ * paragraph of RingView, and stores the result in BidiRow objects.
+ *
+ * BiDi is implemented according to Terminal-wg/bidi v0.2:
+ * https://terminal-wg.pages.freedesktop.org/bidi/
+ */
+
+#include <config.h>
+
+#ifdef WITH_FRIBIDI
+#include <fribidi.h>
+#endif
+
+#include "bidi.hh"
+#include "debug.h"
+#include "vtedefines.hh"
+#include "vteinternal.hh"
+
+#ifdef WITH_FRIBIDI
+static_assert (sizeof (gunichar) == sizeof (FriBidiChar), "Whoooo");
+#endif
+
+using namespace vte::base;
+
+BidiRow::BidiRow()
+{
+        /* The value of 0 is a valid representation of the trivial LTR mapping. */
+        m_width = 0;
+
+        /* These will be initialized / allocated on demand, when some shuffling or shaping is needed. */
+        m_width_alloc = 0;
+        m_log2vis = nullptr;
+        m_vis2log = nullptr;
+        m_vis_rtl = nullptr;
+        m_vis_shaped_char = nullptr;
+}
+
+BidiRow::~BidiRow()
+{
+        g_free (m_log2vis);
+        g_free (m_vis2log);
+        g_free (m_vis_rtl);
+        g_free (m_vis_shaped_char);
+}
+
+void
+BidiRow::set_width(vte::grid::column_t width)
+{
+        if (G_UNLIKELY (width > m_width_alloc)) {
+                if (m_width_alloc == 0) {
+                        m_width_alloc = MAX(width, 80);
+                }
+                while (width > m_width_alloc) {
+                        /* Don't realloc too aggressively. */
+                        m_width_alloc = m_width_alloc * 5 / 4;
+                }
+                m_log2vis = (vte::grid::column_t *) g_realloc (m_log2vis, sizeof (vte::grid::column_t) * 
m_width_alloc);
+                m_vis2log = (vte::grid::column_t *) g_realloc (m_vis2log, sizeof (vte::grid::column_t) * 
m_width_alloc);
+                m_vis_rtl = (guint8 *) g_realloc (m_vis_rtl, sizeof (guint8) * m_width_alloc);
+                m_vis_shaped_char = (gunichar *) g_realloc (m_vis_shaped_char, sizeof (gunichar) * 
m_width_alloc);
+        }
+
+        m_width = width;
+}
+
+/* Converts from logical to visual column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t
+BidiRow::log2vis(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_log2vis[col];
+        } else {
+                return m_base_rtl ? m_width - 1 - col : col;
+        }
+}
+
+/* Converts from visual to logical column. Offscreen columns are mirrored
+ * for RTL lines, e.g. (assuming 80 columns) -1 <=> 80, -2 <=> 81 etc. */
+vte::grid::column_t
+BidiRow::vis2log(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_vis2log[col];
+        } else {
+                return m_base_rtl ? m_width - 1 - col : col;
+        }
+}
+
+/* Whether the cell at the given visual position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool
+BidiRow::vis_is_rtl(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                return m_vis_rtl[col];
+        } else {
+                return m_base_rtl;
+        }
+}
+
+/* Whether the cell at the given logical position has RTL directionality.
+ * For offscreen columns the line's base direction is returned. */
+bool
+BidiRow::log_is_rtl(vte::grid::column_t col) const
+{
+        if (col >= 0 && col < m_width) {
+                col = m_log2vis[col];
+                return m_vis_rtl[col];
+        } else {
+                return m_base_rtl;
+        }
+}
+
+/* Get the shaped character (vteunistr) for the given visual position.
+ *
+ * In some cases a fully LTR line is denoted by m_width being 0. In some other cases
+ * a character that didn't need shaping is stored as the value 0. For the caller's
+ * convenience, ask for the unshaped character and return that in these cases rather
+ * than returning 0.
+ *
+ * FIXMEegmont This should have a wrapper method in RingView, always returning the
+ * actual (potentially shaped) character.
+ */
+vteunistr
+BidiRow::vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const
+{
+        if (col >= m_width || m_vis_shaped_char[col] == 0)
+                return s;
+
+        return m_vis_shaped_char[col];
+}
+
+/* Whether the line's base direction is RTL. */
+bool
+BidiRow::base_is_rtl() const
+{
+        return m_base_rtl;
+}
+
+/* Whether the implicit paragraph contains a foreign directionality character.
+ * This is used in the cursor, showing the character's directionality. */
+bool
+BidiRow::has_foreign() const
+{
+        return m_has_foreign;
+}
+
+
+BidiRunner::BidiRunner(RingView *ringview)
+{
+        m_ringview = ringview;
+}
+
+BidiRunner::~BidiRunner() {}
+
+#ifdef WITH_FRIBIDI
+static inline bool
+is_arabic(gunichar c)
+{
+        return FRIBIDI_IS_ARABIC (fribidi_get_bidi_type (c));
+}
+
+/* Perform Arabic shaping on an explicit line (which could be explicit LTR or explicit RTL),
+ * using presentation form characters.
+ *
+ * Don't do shaping across lines. (I'm unsure about this design decision.
+ * Shaping across soft linebreaks would require an even much more complex code.)
+ *
+ * The FriBiDi API doesn't have a method for shaping a visual string, so we need to extract
+ * Arabic words ourselves, by walking in the visual order from right to left. It's painful.
+ *
+ * This whole shaping business with presentation form characters should be replaced by HarfBuzz.
+ */
+void
+BidiRunner::explicit_line_shape(vte::grid::row_t row)
+{
+        const VteRowData *row_data = m_ringview->get_row(row);
+
+        BidiRow *bidirow = m_ringview->get_bidirow_writable(row);
+
+        auto width = m_ringview->get_width();
+
+        GArray *fribidi_chars_array = nullptr;
+
+        FriBidiParType pbase_dir = FRIBIDI_PAR_RTL;
+        FriBidiLevel level;
+        FriBidiChar *fribidi_chars;
+        FriBidiCharType *fribidi_chartypes;
+        FriBidiBracketType *fribidi_brackettypes;
+        FriBidiJoiningType *fribidi_joiningtypes;
+        FriBidiLevel *fribidi_levels;
+
+        int count;
+
+        const VteCell *cell;
+        gunichar c;
+        gunichar base;
+        int i, j;  /* visual columns */
+
+        fribidi_chars_array = g_array_new (FALSE, FALSE, sizeof (FriBidiChar));
+
+        /* Walk in visual order from right to left. */
+        i = width - 1;
+        while (i >= 0) {
+                cell = _vte_row_data_get(row_data, bidirow->vis2log(i));
+                c = cell ? cell->c : 0;
+                base = _vte_unistr_get_base(c);
+                if (!is_arabic(base)) {
+                        i--;
+                        continue;
+                }
+
+                /* Found an Arabic character. Keep walking to the left, extracting the word. */
+                g_array_set_size(fribidi_chars_array, 0);
+                j = i;
+                do {
+                        auto prev_len = fribidi_chars_array->len;
+                        _vte_unistr_append_to_gunichars (cell->c, fribidi_chars_array);
+                        g_assert_cmpint (fribidi_chars_array->len, >, prev_len);
+
+                        j--;
+                        if (j >= 0) {
+                                cell = _vte_row_data_get(row_data, bidirow->vis2log(j));
+                                c = cell ? cell->c : 0;
+                                base = _vte_unistr_get_base(c);
+                        } else {
+                                /* Pretend that visual column -1 contains a stop char. */
+                                base = 0;
+                        }
+                } while (is_arabic(base));
+
+                /* Extracted the Arabic run. Do the BiDi. */
+
+                /* Convenience stuff, we no longer need the auto-growing GArray wrapper. */
+                count = fribidi_chars_array->len;
+                fribidi_chars = (FriBidiChar *) fribidi_chars_array->data;
+
+                /* Run the BiDi algorithm on the paragraph to get the embedding levels. */
+                fribidi_chartypes = g_newa (FriBidiCharType, count);
+                fribidi_brackettypes = g_newa (FriBidiBracketType, count);
+                fribidi_joiningtypes = g_newa (FriBidiJoiningType, count);
+                fribidi_levels = g_newa (FriBidiLevel, count);
+
+                fribidi_get_bidi_types (fribidi_chars, count, fribidi_chartypes);
+                fribidi_get_bracket_types (fribidi_chars, count, fribidi_chartypes, fribidi_brackettypes);
+                fribidi_get_joining_types (fribidi_chars, count, fribidi_joiningtypes);
+                level = fribidi_get_par_embedding_levels_ex (fribidi_chartypes, fribidi_brackettypes, count, 
&pbase_dir, fribidi_levels) - 1;
+                if (level == (FriBidiLevel)(-1)) {
+                        /* Error. Skip shaping this word. */
+                        i = j - 1;
+                        continue;
+                }
+
+                /* Shaping. */
+                fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+                fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes, 
fribidi_chars);
+
+                /* If we have the shortcut notation for the trivial LTR mapping, we need to
+                 * expand it to the nontrivial notation, in order to store the shaped character. */
+                if (bidirow->m_width == 0) {
+                        bidirow->set_width(width);
+                        for (int k = 0; k < width; k++) {
+                                bidirow->m_log2vis[k] = bidirow->m_vis2log[k] = k;
+                                bidirow->m_vis_rtl[k] = false;
+                                bidirow->m_vis_shaped_char[k] = 0;
+                        }
+                }
+
+                /* Walk through the Arabic word again. */
+                j = i;
+                while (count > 0) {
+                        g_assert_cmpint (j, >=, 0);
+                        cell = _vte_row_data_get(row_data, bidirow->vis2log(j));
+                        c = cell->c;
+                        base = _vte_unistr_get_base(c);
+                        if (*fribidi_chars != base) {
+                                /* Shaping changed the codepoint. Apply combining accents and store. */
+                                bidirow->m_vis_shaped_char[j] = _vte_unistr_replace_base(c, *fribidi_chars);
+                        }
+                        int len = _vte_unistr_strlen(c);
+                        fribidi_chars += len;
+                        count -= len;
+                        j--;
+                }
+
+                /* Ready to look for the next word. Skip the stop char which isn't Arabic. */
+                i = j - 1;
+        }
+
+        g_array_free (fribidi_chars_array, TRUE);
+}
+#endif /* WITH_FRIBIDI */
+
+/* Set up the mapping according to explicit mode for a given line.
+ *
+ * If @do_shaping then perform Arabic shaping on the visual string, independently
+ * from the paragraph direction (the @rtl parameter). This is done using
+ * presentation form characters, until we have something better (e.g. HarfBuzz)
+ * in place.
+ */
+void
+BidiRunner::explicit_line(vte::grid::row_t row, bool rtl, bool do_shaping)
+{
+        int i;
+
+        BidiRow *bidirow = m_ringview->get_bidirow_writable(row);
+        if (G_UNLIKELY (bidirow == nullptr))
+                return;
+        bidirow->m_base_rtl = rtl;
+        bidirow->m_has_foreign = false;
+
+        auto width = m_ringview->get_width();
+
+        if (G_LIKELY (!rtl)) {
+                /* Shortcut notation: a width of 0 means the trivial LTR mapping. */
+                bidirow->set_width(0);
+        } else {
+                /* Set up the explicit RTL mapping. */
+                bidirow->set_width(width);
+                for (i = 0; i < width; i++) {
+                        bidirow->m_log2vis[i] = bidirow->m_vis2log[i] = width - 1 - i;
+                        bidirow->m_vis_rtl[i] = true;
+                        bidirow->m_vis_shaped_char[i] = 0;
+                }
+        }
+
+#ifdef WITH_FRIBIDI
+        if (do_shaping)
+                explicit_line_shape(row);
+#endif
+}
+
+/* Figure out the mapping for the paragraph between the given rows. */
+void
+BidiRunner::paragraph(vte::grid::row_t start, vte::grid::row_t end,
+                      bool do_bidi, bool do_shaping)
+{
+        const VteRowData *row_data = m_ringview->get_row(start);
+
+        if (!do_bidi) {
+                explicit_paragraph(start, end, false, do_shaping);
+                return;
+        }
+
+#ifdef WITH_FRIBIDI
+        /* Have a consistent limit on the number of rows in a paragraph
+         * that can get implicit BiDi treatment, which is independent from
+         * the current scroll position. */
+        if ((row_data->attr.bidi_flags & VTE_BIDI_FLAG_IMPLICIT) &&
+            end - start <= VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX) {
+                if (implicit_paragraph(start, end, do_shaping))
+                        return;
+        }
+#endif
+
+        explicit_paragraph(start, end, row_data->attr.bidi_flags & VTE_BIDI_FLAG_RTL, do_shaping);
+}
+
+/* Set up the mapping according to explicit mode, for all the lines
+ * of a paragraph between the given lines. */
+void
+BidiRunner::explicit_paragraph(vte::grid::row_t start, vte::grid::row_t end,
+                               bool rtl, bool do_shaping)
+{
+        for (; start < end; start++) {
+                explicit_line(start, rtl, do_shaping);
+        }
+}
+
+#ifdef WITH_FRIBIDI
+/* Figure out the mapping for the implicit paragraph between the given rows.
+ * Returns success. */
+bool
+BidiRunner::implicit_paragraph(vte::grid::row_t start, vte::grid::row_t end, bool do_shaping)
+{
+        const VteCell *cell;
+        const VteRowData *row_data;
+        bool rtl;
+        bool autodir;
+        bool has_foreign;
+        vte::grid::row_t row;
+        FriBidiParType pbase_dir;
+        FriBidiLevel level;
+        FriBidiChar *fribidi_chars;
+        FriBidiCharType *fribidi_chartypes;
+        FriBidiBracketType *fribidi_brackettypes;
+        FriBidiJoiningType *fribidi_joiningtypes;
+        FriBidiLevel *fribidi_levels;
+        FriBidiStrIndex *fribidi_map;
+        FriBidiStrIndex *fribidi_to_term;
+        BidiRow *bidirow;
+
+        auto width = m_ringview->get_width();
+
+        row_data = m_ringview->get_row(start);
+        rtl = row_data->attr.bidi_flags & VTE_BIDI_FLAG_RTL;
+        autodir = row_data->attr.bidi_flags & VTE_BIDI_FLAG_AUTO;
+
+        int lines[VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX + 1];  /* offsets to the beginning of lines */
+        lines[0] = 0;
+        int line = 0;   /* line number within the paragraph */
+        int count;      /* total character count */
+        int tl, tv;     /* terminal logical and visual */
+        int fl, fv;     /* fribidi logical and visual */
+        unsigned int col;
+
+        GArray *fribidi_chars_array   = g_array_new (FALSE, FALSE, sizeof (FriBidiChar));
+        GArray *fribidi_map_array     = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+        GArray *fribidi_to_term_array = g_array_new (FALSE, FALSE, sizeof (FriBidiStrIndex));
+
+        /* Extract the paragraph's contents, omitting unused and fragment cells. */
+
+        /* Example of what is going on, showing the most important steps:
+         *
+         * Let's take the string produced by this command:
+         *   echo -e "\u0041\u05e9\u05b8\u05c1\u05dc\u05d5\u05b9\u05dd\u0031\u0032\uff1c\u05d0"
+         *
+         * This string consists of:
+         * - English letter A
+         * - Hebrew word Shalom:
+         *     - Letter Shin: ש
+         *         - Combining accent Qamats
+         *         - Combining accent Shin Dot
+         *     - Letter Lamed: ל
+         *     - Letter Vav: ו
+         *         - Combining accent Holam
+         *     - Letter Final Mem: ם
+         * - Digits One and Two
+         * - Full-width less-than sign U+ff1c: <
+         * - Hebrew letter Alef: א
+         *
+         * Features of this example:
+         * - Overall LTR direction for convenience (set up by the leading English letter)
+         * - Combining accents within RTL
+         * - Double width character with RTL resolved direction
+         * - A mapping that is not its own inverse (due to the digits being LTR inside RTL inside LTR),
+         *   to help catch if we'd look up something in the wrong direction
+         *
+         * Not demonstrated in this example:
+         * - Wrapping a paragraph to lines
+         * - Spacing marks
+         *
+         * Pre-BiDi (logical) order, using approximating glyphs ("Shalom" is "w7io", Alef is "x"):
+         *   Aw7io12<x
+         *
+         * Post-BiDi (visual) order, using approximating glyphs ("Shalom" is "oi7w", note the mirrored 
less-than):
+         *   Ax>12oi7w
+         *
+         * Terminal's logical cells:
+         *                 [0]       [1]       [2]      [3]     [4]   [5]   [6]    [7]      [8]         [9]
+         *     row_data:    A   Shin+qam+dot   Lam    Vav+hol   Mem   One   Two   Less   Less (cont)   Alef
+         *
+         * Extracted to pass to FriBidi (combining accents get -1, double wides' continuation cells are 
skipped):
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_chars:      A    Shin   qam   dot   Lam   Vav   hol   Mem   One   Two   Less   Alef
+         *     fribidi_map:        0      1    -1    -1     4     5    -1     7     8     9     10     11
+         *     fribidi_to_term:    0      1    -1    -1     2     3    -1     4     5     6      7      9
+         *
+         * Embedding levels and other properties (shaping etc.) are looked up:
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_levels:     0      1     1     1     1     1     1     1     2     2      1      1
+         *
+         * The steps above were per-paragraph. The steps below are per-line.
+         *
+         * After fribidi_reorder_line (only this array gets shuffled):
+         *                        [0]    [1]   [2]   [3]   [4]   [5]   [6]   [7]   [8]   [9]   [10]   [11]
+         *     fribidi_map:        0     11    10     8     9     7     5    -1     4     1     -1     -1
+         *
+         * To get the visual order: walk in the new fribidi_map, and for each real entry look up the
+         * logical terminal column using fribidi_to_term:
+         * - map[0] is 0, to_term[0] is 0, hence visual column 0 belongs to logical column 0 (A)
+         * - map[1] is 11, to_term[11] is 9, hence visual column 1 belongs to logical column 9 (Alef)
+         * - map[2] is 10, to_term[10] is 7, row_data[7] is the "<" sign
+         *     - this is a double wide character, we need to map the next two visual cells to two logical 
cells
+         *     - due to levels[10] being odd, this character has a resolved RTL direction
+         *     - thus we map in reverse order: visual 2 <=> logical 8, visual 3 <=> logical 7
+         *     - the glyph is also mirrorable, it'll be displayed accordingly
+         * - [3] -> 8 -> 5, so visual 4 <=> logical 5 (One)
+         * - [4] -> 9 -> 6, so visual 5 <=> logical 6 (Two)
+         * - [5] -> 7 -> 4, so visual 6 <=> logical 4 (Mem, the last, leftmost letter of Shalom)
+         * - [6] -> 5 -> 3, so visual 7 <=> logical 3 (Vav+hol)
+         * - [7] -> -1, skipped
+         * - [8] -> 4 -> 2, so visual 8 <=> logical 2 (Lam)
+         * - [9] -> 1 -> 1, so visual 9 <=> logical 1 (Shin+qam+dot, the first, rightmost letter of Shalom)
+         * - [10] -> -1, skipped
+         * - [11] -> -1, skipped
+         *
+         * Silly FriBidi API almost allows us to skip one level of indirection, by placing the to_term values
+         * in the map to be shuffled. However, we can't get the embedding levels then.
+         * TODO: File an issue for a better API.
+         */
+        for (row = start; row < end; row++) {
+                row_data = m_ringview->get_row(row);
+
+                /* A row_data might be longer, in case rewrapping is disabled and the window was narrowed.
+                 * Truncate the logical data before applying BiDi. */
+                // FIXME what the heck to do if this truncation cuts a TAB or CJK in half???
+                for (tl = 0; tl < width && tl < row_data->len; tl++) {
+                        auto prev_len = fribidi_chars_array->len;
+                        FriBidiStrIndex val;
+
+                        cell = _vte_row_data_get (row_data, tl);
+                        if (cell->attr.fragment())
+                                continue;
+
+                        /* Extract the base character and combining accents.
+                         * Convert mid-line erased cells to spaces.
+                         * Note: see the static assert at the top of this file. */
+                        _vte_unistr_append_to_gunichars (cell->c ? cell->c : ' ', fribidi_chars_array);
+                        /* Make sure at least one character was produced. */
+                        g_assert_cmpint (fribidi_chars_array->len, >, prev_len);
+
+                        /* Track the base character, assign to it its current index in fribidi_chars.
+                         * Don't track combining accents, assign -1's to them. */
+                        val = prev_len;
+                        g_array_append_val (fribidi_map_array, val);
+                        val = tl;
+                        g_array_append_val (fribidi_to_term_array, val);
+                        prev_len++;
+                        val = -1;
+                        while (prev_len++ < fribidi_chars_array->len) {
+                                g_array_append_val (fribidi_map_array, val);
+                                g_array_append_val (fribidi_to_term_array, val);
+                        }
+                }
+
+                lines[++line] = fribidi_chars_array->len;
+        }
+
+        /* Convenience stuff, we no longer need the auto-growing GArray wrapper. */
+        count = fribidi_chars_array->len;
+        fribidi_chars = (FriBidiChar *) fribidi_chars_array->data;
+        fribidi_map = (FriBidiStrIndex *) fribidi_map_array->data;
+        fribidi_to_term = (FriBidiStrIndex *) fribidi_to_term_array->data;
+
+        /* Run the BiDi algorithm on the paragraph to get the embedding levels. */
+        fribidi_chartypes = g_newa (FriBidiCharType, count);
+        fribidi_brackettypes = g_newa (FriBidiBracketType, count);
+        fribidi_joiningtypes = g_newa (FriBidiJoiningType, count);
+        fribidi_levels = g_newa (FriBidiLevel, count);
+
+        pbase_dir = autodir ? (rtl ? FRIBIDI_PAR_WRTL : FRIBIDI_PAR_WLTR)
+                            : (rtl ? FRIBIDI_PAR_RTL  : FRIBIDI_PAR_LTR );
+
+        fribidi_get_bidi_types (fribidi_chars, count, fribidi_chartypes);
+        fribidi_get_bracket_types (fribidi_chars, count, fribidi_chartypes, fribidi_brackettypes);
+        fribidi_get_joining_types (fribidi_chars, count, fribidi_joiningtypes);
+        level = fribidi_get_par_embedding_levels_ex (fribidi_chartypes, fribidi_brackettypes, count, 
&pbase_dir, fribidi_levels) - 1;
+
+        if (level == (FriBidiLevel)(-1)) {
+                /* error */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                return false;
+        }
+
+        if (do_shaping) {
+                /* Arabic shaping (on the entire paragraph in a single run). */
+                fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+                fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes, 
fribidi_chars);
+        }
+
+        /* For convenience, from now on this variable contains the resolved (i.e. possibly autodetected) 
value. */
+        g_assert_cmpint (pbase_dir, !=, FRIBIDI_PAR_ON);
+        rtl = (pbase_dir == FRIBIDI_PAR_RTL || pbase_dir == FRIBIDI_PAR_WRTL);
+
+        if (!rtl && level == 0) {
+                /* Fast and memory saving shortcut for LTR-only paragraphs. */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                explicit_paragraph (start, end, false, false);
+                return true;
+        }
+
+        /* Check if the paragraph has a foreign directionality character. In fact, also catch
+         * and treat it so if the paragraph has a mixture of multiple embedding levels, even if all
+         * of them has the same parity (direction). */
+        if (!rtl) {
+                /* LTR. We already bailed out above if level == 0, so there must be a character
+                 * with a higher embedding level. */
+                has_foreign = true;
+        } else {
+                /* RTL. Check if any character has a level other than 1. Check the paragraph's
+                 * maximum level as a shortcut, but note that in case of an empty paragraph
+                 * its value is 0 rather than 1. */
+                if (level <= 1) {
+                        has_foreign = false;
+                        for (int i = 0; i < count; i++) {
+                                if (fribidi_levels[i] != 1) {
+                                        has_foreign = true;
+                                        break;
+                                }
+                        }
+                } else {
+                        has_foreign = true;
+                }
+        }
+
+        /* Reshuffle line by line. */
+        for (row = start, line = 0; row < end; row++, line++) {
+                bidirow = m_ringview->get_bidirow_writable(row);
+                if (bidirow == nullptr)
+                        continue;
+
+                bidirow->m_base_rtl = rtl;
+                bidirow->m_has_foreign = has_foreign;
+                bidirow->set_width(width);
+
+                row_data = m_ringview->get_row(row);
+
+                level = fribidi_reorder_line (FRIBIDI_FLAGS_DEFAULT,
+                                              fribidi_chartypes,
+                                              lines[line + 1] - lines[line],
+                                              lines[line],
+                                              pbase_dir,
+                                              fribidi_levels,
+                                              NULL,
+                                              fribidi_map) - 1;
+
+                if (level == (FriBidiLevel)(-1)) {
+                        /* error, what should we do? */
+                        explicit_line (row, rtl, true);
+                        bidirow->m_has_foreign = has_foreign;
+                        continue;
+                }
+
+                if (!rtl && level == 0) {
+                        /* Fast shortcut for LTR-only lines. */
+                        explicit_line (row, false, false);
+                        bidirow->m_has_foreign = has_foreign;
+                        continue;
+                }
+
+                /* Copy to our realm. Proceed in visual order.*/
+                tv = 0;
+                if (rtl) {
+                        /* Unused cells on the left for RTL paragraphs */
+                        int unused = MAX(width - row_data->len, 0);
+                        for (; tv < unused; tv++) {
+                                bidirow->m_vis2log[tv] = width - 1 - tv;
+                                bidirow->m_vis_rtl[tv] = true;
+                                bidirow->m_vis_shaped_char[tv] = 0;
+                        }
+                }
+                for (fv = lines[line]; fv < lines[line + 1]; fv++) {
+                        /* Inflate fribidi's result by inserting fragments. */
+                        fl = fribidi_map[fv];
+                        if (fl == -1)
+                                continue;
+                        tl = fribidi_to_term[fl];
+                        cell = _vte_row_data_get (row_data, tl);
+                        g_assert (!cell->attr.fragment());
+                        g_assert (cell->attr.columns() > 0);
+                        if (FRIBIDI_LEVEL_IS_RTL(fribidi_levels[fl])) {
+                                /* RTL character directionality. Map fragments in reverse order. */
+                                for (col = 0; col < cell->attr.columns(); col++) {
+                                        bidirow->m_vis2log[tv + col] = tl + cell->attr.columns() - 1 - col;
+                                        bidirow->m_vis_rtl[tv + col] = true;
+                                        bidirow->m_vis_shaped_char[tv + col] = fribidi_chars[fl];
+                                }
+                                tv += cell->attr.columns();
+                                tl += cell->attr.columns();
+                        } else {
+                                /* LTR character directionality. */
+                                for (col = 0; col < cell->attr.columns(); col++) {
+                                        bidirow->m_vis2log[tv] = tl;
+                                        bidirow->m_vis_rtl[tv] = false;
+                                       bidirow->m_vis_shaped_char[tv] = fribidi_chars[fl];
+                                        tv++;
+                                        tl++;
+                                }
+                        }
+                }
+                if (!rtl) {
+                        /* Unused cells on the right for LTR paragraphs */
+                        g_assert_cmpint (tv, ==, MIN(row_data->len, width));
+                        for (; tv < width; tv++) {
+                                bidirow->m_vis2log[tv] = tv;
+                                bidirow->m_vis_rtl[tv] = false;
+                                bidirow->m_vis_shaped_char[tv] = 0;
+                        }
+                }
+                g_assert_cmpint (tv, ==, width);
+
+                /* From vis2log create the log2vis mapping too.
+                 * In debug mode assert that we have a bijective mapping. */
+                if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+                        for (tl = 0; tl < width; tl++) {
+                                bidirow->m_log2vis[tl] = -1;
+                        }
+                }
+
+                for (tv = 0; tv < width; tv++) {
+                        bidirow->m_log2vis[bidirow->m_vis2log[tv]] = tv;
+                }
+
+                if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+                        for (tl = 0; tl < width; tl++) {
+                                g_assert_cmpint (bidirow->m_log2vis[tl], !=, -1);
+                        }
+                }
+        }
+
+        g_array_free (fribidi_chars_array, TRUE);
+        g_array_free (fribidi_map_array, TRUE);
+        g_array_free (fribidi_to_term_array, TRUE);
+        return true;
+}
+#endif /* WITH_FRIBIDI */
+
+
+/* Find the mirrored counterpart of a codepoint, just like
+ * fribidi_get_mirror_char() or g_unichar_get_mirror_char() does.
+ * Two additions:
+ * - works with vteunistr, that is, preserves combining accents;
+ * - optionally mirrors box drawing characters.
+ */
+gboolean
+vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr *out)
+{
+        static const unsigned char mirrored_2500[0x80] = {
+                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x10, 0x11, 0x12, 
0x13,
+                0x0c, 0x0d, 0x0e, 0x0f, 0x18, 0x19, 0x1a, 0x1b, 0x14, 0x15, 0x16, 0x17, 0x24, 0x25, 0x26, 
0x27,
+                0x28, 0x29, 0x2a, 0x2b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x2c, 0x2e, 0x2d, 
0x2f,
+                0x30, 0x32, 0x31, 0x33, 0x34, 0x36, 0x35, 0x37, 0x38, 0x3a, 0x39, 0x3b, 0x3c, 0x3e, 0x3d, 
0x3f,
+                0x40, 0x41, 0x42, 0x44, 0x43, 0x46, 0x45, 0x47, 0x48, 0x4a, 0x49, 0x4b, 0x4c, 0x4d, 0x4e, 
0x4f,
+                0x50, 0x51, 0x55, 0x56, 0x57, 0x52, 0x53, 0x54, 0x5b, 0x5c, 0x5d, 0x58, 0x59, 0x5a, 0x61, 
0x62,
+                0x63, 0x5e, 0x5f, 0x60, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6e, 0x6d, 
0x70,
+                0x6f, 0x72, 0x71, 0x73, 0x76, 0x75, 0x74, 0x77, 0x7a, 0x79, 0x78, 0x7b, 0x7e, 0x7d, 0x7c, 
0x7f };
+
+        gunichar base_ch = _vte_unistr_get_base (unistr);
+        gunichar base_ch_mirrored = base_ch;
+
+        if (G_UNLIKELY (base_ch >= 0x2500 && base_ch < 0x2580)) {
+                if (G_UNLIKELY (mirror_box_drawing))
+                        base_ch_mirrored = 0x2500 + mirrored_2500[base_ch - 0x2500];
+        } else {
+#ifdef WITH_FRIBIDI
+                /* Prefer the FriBidi variant as that's more likely to be in sync with the rest of our BiDi 
stuff. */
+                fribidi_get_mirror_char (base_ch, &base_ch_mirrored);
+#else
+                /* Fall back to glib, so that we still get mirrored characters in explicit RTL mode without 
BiDi support. */
+                g_unichar_get_mirror_char (base_ch, &base_ch_mirrored);
+#endif
+        }
+
+        vteunistr unistr_mirrored = _vte_unistr_replace_base (unistr, base_ch_mirrored);
+
+        if (out)
+                *out = unistr_mirrored;
+        return unistr_mirrored == unistr;
+}
diff --git a/src/bidi.hh b/src/bidi.hh
new file mode 100644
index 00000000..95e973b2
--- /dev/null
+++ b/src/bidi.hh
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2018–2019 Egmont Koblinger
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#pragma once
+
+#include <glib.h>
+
+#include "ring.hh"
+#include "ringview.hh"
+#include "vterowdata.hh"
+#include "vtetypes.hh"
+#include "vteunistr.h"
+
+namespace vte {
+
+namespace base {  // FIXME ???
+
+class RingView;
+
+/* BidiRow contains the BiDi transformation of a single row. */
+class BidiRow {
+        friend class BidiRunner;
+
+public:
+        BidiRow();
+        ~BidiRow();
+
+        // prevent accidents
+        BidiRow(BidiRow& o) = delete;
+        BidiRow(BidiRow const& o) = delete;
+        BidiRow(BidiRow&& o) = delete;
+        BidiRow& operator= (BidiRow& o) = delete;
+        BidiRow& operator= (BidiRow const& o) = delete;
+        BidiRow& operator= (BidiRow&& o) = delete;
+
+        vte::grid::column_t log2vis(vte::grid::column_t col) const;
+        vte::grid::column_t vis2log(vte::grid::column_t col) const;
+        bool log_is_rtl(vte::grid::column_t col) const;
+        bool vis_is_rtl(vte::grid::column_t col) const;
+        vteunistr vis_get_shaped_char(vte::grid::column_t col, vteunistr s) const;
+        bool base_is_rtl() const;
+        bool has_foreign() const;
+
+private:
+        void set_width(vte::grid::column_t width);
+
+        vte::grid::column_t m_width;
+        vte::grid::column_t m_width_alloc;
+
+        vte::grid::column_t *m_log2vis;  // FIXMEegmont this is 8 bytes per item
+        vte::grid::column_t *m_vis2log;  // while probably 2 should be enough
+        guint8 *m_vis_rtl;
+        gunichar *m_vis_shaped_char;
+
+        guint8 m_base_rtl: 1;
+        guint8 m_has_foreign: 1;
+};
+
+
+/* BidiRunner is not a "real" class, rather the collection of methods that run the BiDi algorithm. */
+class BidiRunner {
+public:
+        BidiRunner(RingView *ringview);
+        ~BidiRunner();
+
+        // prevent accidents
+        BidiRunner(BidiRunner& o) = delete;
+        BidiRunner(BidiRunner const& o) = delete;
+        BidiRunner(BidiRunner&& o) = delete;
+        BidiRunner& operator= (BidiRunner& o) = delete;
+        BidiRunner& operator= (BidiRunner const& o) = delete;
+        BidiRunner& operator= (BidiRunner&& o) = delete;
+
+        void paragraph(vte::grid::row_t start, vte::grid::row_t end,
+                       bool do_bidi, bool do_shaping);
+
+private:
+        RingView *m_ringview;
+
+#ifdef WITH_FRIBIDI
+        void explicit_line_shape(vte::grid::row_t row);
+#endif
+
+        void explicit_line(vte::grid::row_t row, bool rtl, bool do_shaping);
+        void explicit_paragraph(vte::grid::row_t start, vte::grid::row_t end, bool rtl, bool do_shaping);
+#ifdef WITH_FRIBIDI
+        bool implicit_paragraph(vte::grid::row_t start, vte::grid::row_t end, bool do_shaping);
+#endif
+};
+
+}; /* namespace base */
+
+}; /* namespace vte */
+
+G_BEGIN_DECLS
+
+gboolean vte_bidi_get_mirror_char (vteunistr unistr, gboolean mirror_box_drawing, vteunistr 
*unistr_mirrored);
+
+G_END_DECLS
diff --git a/src/meson.build b/src/meson.build
index 6b97aaec..1481c089 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -56,6 +56,8 @@ utf8_sources = files(
 
 libvte_common_sources = debug_sources + modes_sources + parser_sources + utf8_sources + files(
   'attr.hh',
+  'bidi.cc',
+  'bidi.hh',
   'buffer.h',
   'caps.hh',
   'cell.hh',
@@ -146,6 +148,7 @@ libvte_common_public_deps = [
 ]
 
 libvte_common_deps = libvte_common_public_deps + [
+  fribidi_dep,
   gnutls_dep,
   pcre2_dep,
   libm_dep,
diff --git a/src/ringview.cc b/src/ringview.cc
index 42035476..8100674d 100644
--- a/src/ringview.cc
+++ b/src/ringview.cc
@@ -18,6 +18,7 @@
 
 #include <config.h>
 
+#include "bidi.hh"
 #include "debug.h"
 #include "vtedefines.hh"
 #include "vteinternal.hh"
@@ -30,6 +31,11 @@ RingView::RingView()
         m_start = m_len = m_width = 0;
         m_rows_alloc_len = 0;
 
+        m_enable_bidi = true;      /* These two are the most convenient defaults */
+        m_enable_shaping = false;  /* for short-lived ringviews. */
+        m_bidirunner = new BidiRunner(this);
+        m_bidirows_alloc_len = 0;
+
         m_invalid = true;
         m_paused = true;
 }
@@ -37,6 +43,7 @@ RingView::RingView()
 RingView::~RingView()
 {
         pause();
+        delete m_bidirunner;
 }
 
 /* Pausing a RingView frees up pretty much all of its memory.
@@ -55,8 +62,8 @@ RingView::pause()
         if (m_paused)
                 return;
 
-        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: pause, freeing %d rows.\n",
-                                              m_rows_alloc_len);
+        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: pause, freeing %d rows, %d bidirows.\n",
+                                              m_rows_alloc_len, m_bidirows_alloc_len);
 
         for (i = 0; i < m_rows_alloc_len; i++) {
                 _vte_row_data_fini(m_rows[i]);
@@ -65,6 +72,12 @@ RingView::pause()
         g_free (m_rows);
         m_rows_alloc_len = 0;
 
+        for (i = 0; i < m_bidirows_alloc_len; i++) {
+                delete m_bidirows[i];
+        }
+        g_free (m_bidirows);
+        m_bidirows_alloc_len = 0;
+
         m_invalid = true;
         m_paused = true;
 }
@@ -84,8 +97,19 @@ RingView::resume()
                 _vte_row_data_init (m_rows[i]);
         }
 
-        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: resume, allocating %d rows\n",
-                                              m_rows_alloc_len);
+        /* +2: Likely prevent a quickly following realloc.
+         * The number of lines of interest keeps jumping up and down by one
+         * due to per-pixel scrolling, and by another one due sometimes having
+         * to reshuffle another line below the bottom for the overflowing bits
+         * of the outline rectangle cursor. */
+        m_bidirows_alloc_len = m_len + 2;
+        m_bidirows = (BidiRow **) g_malloc (sizeof (BidiRow *) * m_bidirows_alloc_len);
+        for (int i = 0; i < m_bidirows_alloc_len; i++) {
+                m_bidirows[i] = new BidiRow();
+        }
+
+        _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: resume, allocating %d rows, %d bidirows\n",
+                                              m_rows_alloc_len, m_bidirows_alloc_len);
 
         m_paused = false;
 }
@@ -127,6 +151,21 @@ RingView::set_rows(vte::grid::row_t start, vte::grid::row_t len)
 
         /* m_rows is expanded on demand in update() */
 
+        /* m_bidirows needs exactly this many lines */
+        if (G_UNLIKELY (!m_paused && len > m_bidirows_alloc_len)) {
+                int i = m_bidirows_alloc_len;
+                while (len > m_bidirows_alloc_len) {
+                        /* Don't realloc too aggressively. */
+                        m_bidirows_alloc_len = std::max(m_bidirows_alloc_len + 1, m_bidirows_alloc_len * 5 / 
4 /* whatever */);
+                }
+                _vte_debug_print (VTE_DEBUG_RINGVIEW, "Ringview: reallocate to %d bidirows\n",
+                                                      m_bidirows_alloc_len);
+                m_bidirows = (BidiRow **) g_realloc (m_bidirows, sizeof (BidiRow *) * m_bidirows_alloc_len);
+                for (; i < m_bidirows_alloc_len; i++) {
+                        m_bidirows[i] = new BidiRow();
+                }
+        }
+
         m_start = start;
         m_len = len;
         m_invalid = true;
@@ -141,6 +180,26 @@ RingView::get_row(vte::grid::row_t row) const
         return m_rows[row - m_top];
 }
 
+void
+RingView::set_enable_bidi(bool enable_bidi)
+{
+        if (G_LIKELY (enable_bidi == m_enable_bidi))
+                return;
+
+        m_enable_bidi = enable_bidi;
+        m_invalid = true;
+}
+
+void
+RingView::set_enable_shaping(bool enable_shaping)
+{
+        if (G_LIKELY (enable_shaping == m_enable_shaping))
+                return;
+
+        m_enable_shaping = enable_shaping;
+        m_invalid = true;
+}
+
 void
 RingView::update()
 {
@@ -218,7 +277,11 @@ RingView::update()
                 if (!row_data->attr.soft_wrapped || row == m_top + m_rows_len - 1) {
                         /* Found a paragraph from @top to @row, inclusive. */
 
-                        /* Doing BiDi, syntax highlighting etc. come here in the future. */
+                        /* Run the BiDi algorithm. */
+                        m_bidirunner->paragraph(top, row + 1,
+                                                m_enable_bidi, m_enable_shaping);
+
+                        /* Doing syntax highlighting etc. come here in the future. */
 
                         top = row + 1;
                 }
@@ -227,3 +290,23 @@ RingView::update()
 
         m_invalid = false;
 }
+
+BidiRow const* RingView::get_bidirow(vte::grid::row_t row) const
+{
+        g_assert_cmpint (row, >=, m_start);
+        g_assert_cmpint (row, <, m_start + m_len);
+        g_assert_false (m_invalid);
+        g_assert_false (m_paused);
+
+        return m_bidirows[row - m_start];
+}
+
+/* For internal use by BidiRunner. Get where the BiDi mapping for the given row
+ * needs to be stored, of nullptr if it's a context row. */
+BidiRow* RingView::get_bidirow_writable(vte::grid::row_t row) const
+{
+        if (row < m_start || row >= m_start + m_len)
+                return nullptr;
+
+        return m_bidirows[row - m_start];
+}
diff --git a/src/ringview.hh b/src/ringview.hh
index 6c68241a..255a230c 100644
--- a/src/ringview.hh
+++ b/src/ringview.hh
@@ -20,6 +20,7 @@
 
 #include <glib.h>
 
+#include "bidi.hh"
 #include "ring.hh"
 #include "vterowdata.hh"
 #include "vtetypes.hh"
@@ -29,6 +30,9 @@ namespace vte {
 
 namespace base {  // FIXME ???
 
+class BidiRow;
+class BidiRunner;
+
 /*
  * RingView provides a "view" to a continuous segment of the Ring (or stream),
  * typically the user visible area.
@@ -49,6 +53,8 @@ namespace base {  // FIXME ???
  * infrastructure one day.
  */
 class RingView {
+        friend class BidiRunner;
+
 public:
         RingView();
         ~RingView();
@@ -65,6 +71,8 @@ public:
         void set_rows(vte::grid::row_t start, vte::grid::row_t len);
         void set_width(vte::grid::column_t width);
         vte::grid::column_t get_width() const { return m_width; }
+        void set_enable_bidi(bool enable_bidi);
+        void set_enable_shaping(bool enable_shaping);
 
         inline void invalidate() { m_invalid = true; }
         void update();
@@ -72,6 +80,8 @@ public:
 
         VteRowData const* get_row(vte::grid::row_t row) const;
 
+        BidiRow const* get_bidirow(vte::grid::row_t row) const;
+
 private:
         Ring *m_ring;
 
@@ -79,6 +89,13 @@ private:
         int m_rows_len;
         int m_rows_alloc_len;
 
+        bool m_enable_bidi;
+        bool m_enable_shaping;
+        BidiRow **m_bidirows;
+        int m_bidirows_alloc_len;
+
+        BidiRunner *m_bidirunner;
+
         vte::grid::row_t m_top;  /* the row of the Ring corresponding to m_rows[0] */
 
         vte::grid::row_t m_start;
@@ -89,6 +106,8 @@ private:
         bool m_paused;
 
         void resume();
+
+        BidiRow* get_bidirow_writable(vte::grid::row_t row) const;
 };
 
 }; /* namespace base */
diff --git a/src/vte.cc b/src/vte.cc
index fdcc1010..8f6b557b 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -9033,6 +9033,8 @@ Terminal::ringview_update()
         m_ringview.set_ring (m_screen->row_data);
         m_ringview.set_rows (first_row, last_row - first_row + 1);
         m_ringview.set_width (m_column_count);
+        m_ringview.set_enable_bidi (m_enable_bidi);
+        m_ringview.set_enable_shaping (m_enable_shaping);
         m_ringview.update ();
 }
 
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index 338d7f41..63fc193a 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -1823,6 +1823,12 @@ const char *
 vte_get_features (void)
 {
         return
+#ifdef WITH_FRIBIDI
+                "+BIDI"
+#else
+                "-BIDI"
+#endif
+                " "
 #ifdef WITH_GNUTLS
                 "+GNUTLS"
 #else
diff --git a/src/vteunistr.cc b/src/vteunistr.cc
index 69c47fe0..319bd6bf 100644
--- a/src/vteunistr.cc
+++ b/src/vteunistr.cc
@@ -162,6 +162,39 @@ _vte_unistr_get_base (vteunistr s)
        return (gunichar) s;
 }
 
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a)
+{
+        if (G_UNLIKELY (s >= VTE_UNISTR_START)) {
+                struct VteUnistrDecomp *decomp;
+                decomp = &DECOMP_FROM_UNISTR (s);
+                _vte_unistr_append_to_gunichars (decomp->prefix, a);
+                s = decomp->suffix;
+        }
+        gunichar val = (gunichar) s;
+        g_array_append_val (a, val);
+}
+
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c)
+{
+        g_return_val_if_fail (s < unistr_next, s);
+
+        if (G_LIKELY (_vte_unistr_get_base(s) == c))
+                return s;
+
+        GArray *a = g_array_new (FALSE, FALSE, sizeof (gunichar));
+        _vte_unistr_append_to_gunichars (s, a);
+        g_assert_cmpint(a->len, >=, 1);
+
+        s = c;
+        for (glong i = 1; i < a->len; i++)
+                s = _vte_unistr_append_unichar (s, g_array_index (a, gunichar, i));
+
+        g_array_free (a, TRUE);
+        return s;
+}
+
 void
 _vte_unistr_append_to_string (vteunistr s, GString *gs)
 {
diff --git a/src/vteunistr.h b/src/vteunistr.h
index 9bde6b2c..84cb0562 100644
--- a/src/vteunistr.h
+++ b/src/vteunistr.h
@@ -73,6 +73,19 @@ _vte_unistr_append_unistr (vteunistr s, vteunistr t);
 gunichar
 _vte_unistr_get_base (vteunistr s);
 
+/**
+ * _vte_unistr_append_to_string:
+ * @s: a #vteunistr
+ * @c: Unicode character to replace the base character of @s.
+ *
+ * Creates a vteunistr value where the base character from @s is
+ * replaced by @c, while the combining characters from @s are carried over.
+ *
+ * Returns: the new #vteunistr value
+ */
+vteunistr
+_vte_unistr_replace_base (vteunistr s, gunichar c);
+
 /**
  * _vte_unistr_append_to_string:
  * @s: a #vteunistr
@@ -84,6 +97,16 @@ _vte_unistr_get_base (vteunistr s);
 void
 _vte_unistr_append_to_string (vteunistr s, GString *gs);
 
+/**
+ * _vte_unistr_append_to_gunichars:
+ * @s: a #vteunistr
+ * @a: a #GArray of #gunichar items to append @s to
+ *
+ * Appends @s to @a.
+ **/
+void
+_vte_unistr_append_to_gunichars (vteunistr s, GArray *a);
+
 /**
  * _vte_unistr_strlen:
  * @s: a #vteunistr


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]