[vte/wip/egmont/bidi: 4/23] BiDi work up to Feb 2019 squashed



commit ddb842e2dc62a3bc31c9c6ae41f61b0457ba5467
Author: Egmont Koblinger <egmont gmail com>
Date:   Fri Aug 17 18:43:19 2018 +0200

    BiDi work up to Feb 2019 squashed

 BIDI-STATUS           |  64 +++++
 doc/bidi.txt          | 205 ++++++++++++++
 meson.build           |   9 +
 meson_options.txt     |   7 +
 src/bidi.cc           | 724 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/bidi.hh           | 124 +++++++++
 src/debug.cc          |   1 +
 src/debug.h           |   1 +
 src/meson.build       |   3 +
 src/modes-ecma.hh     |  17 +-
 src/modes-private.hh  |  26 ++
 src/modes.hh          |   4 +-
 src/parser-cmd.hh     |   4 +-
 src/parser-csi.hh     |   4 +-
 src/ring.cc           |  20 +-
 src/ring.hh           |  10 +-
 src/vte.cc            | 369 ++++++++++++++++++++++---
 src/vte/vteterminal.h |   2 +-
 src/vtedefines.hh     |   3 +
 src/vtedraw.cc        |   7 +
 src/vtedraw.hh        |   2 +
 src/vtegtk.cc         |   6 +
 src/vteinternal.hh    |  26 +-
 src/vterowdata.cc     |   3 +
 src/vterowdata.hh     |   3 +-
 src/vteseq.cc         |  71 ++++-
 src/vteunistr.cc      |  33 +++
 src/vteunistr.h       |  23 ++
 28 files changed, 1707 insertions(+), 64 deletions(-)
---
diff --git a/BIDI-STATUS b/BIDI-STATUS
new file mode 100644
index 00000000..1a29c4f4
--- /dev/null
+++ b/BIDI-STATUS
@@ -0,0 +1,64 @@
+Done:
+- Implicit (level 1) and Explicit (a small subset only) modes (SM / RM 8).
+- Overall LTR or RTL direction (SPD 0 / 3 or SCP 1 / 2).
+- Possibility to autodetect paragraph direction (DECSET / DECRST 2501).
+- Possibility to make box drawing characters mirrorable (DECSET / DECRST 2500).
+- I-Beam cursor shows the character's resolved directionality when the
+  paragraph has a foreign directionality character.
+- Mouse highlighting, text copying (logical in normal modes, visual in
+  rectangle mode).
+- Mouse reporting.
+- Regex match and explicit hyperlink underlining on hover.
+- VTE_DEBUG=bidi highlights characters with resolved RTL directionality.
+- Arabic shaping using Unicode presentation forms.
+- Test file.
+- Configure flag.
+- Keyboard arrow swapping.
+
+Bugs:
+- The way the modes apply to paragraphs, and what happens when a paragraph
+  is split or two paragraphs are joined is just a first hack, needs to be
+  reviewed, adjusted, fixed properly.
+- SPD should also update all the onscreen lines.
+
+Missing from first release:
+- The entire screen is always invalidated. Have some more fine grained
+  solution (see also vte #26).
+- Probably we'd need to make Erase in Display (Below or All) sequences turn
+  the previous line to hard wrapped, requiring to keep one more line in the
+  ring's writable area.
+- Design doc review & publishing, public tracking issue.
+- Brief summary about the design and implemented features in the source tree.
+- Code cleanup and review, of course.
+
+Planned future improvements:
+- Real shaping (harfbuzz?).
+- Right-align RTL glyphs.
+- Implicit mode level 2 (handling BiDi control characters).
+- Mirror the glyphs that don't have mirrored counterpart.
+- Play with other possibilities for placing the cursor (especially when it's
+  at the end of the logical line).
+- Play with better placement of the preedit box.
+- Remember some lines that are no longer user-accessible as they scroll out,
+  to properly BiDi the still remaining part of that paragraph.
+- Possibility for default RTL directionality?
+- API (for what exactly)?
+
+Not planned at all:
+- Operating on the presentation component (DCSM reset).
+- BiDi in explicit mode by transferring the embedding levels (SDS, SRS...).
+- Escape sequences that modify the emulation behavior (SIMD...).
+
+Useful aliases:
+  alias ltr='echo -ne "\e[1 k"'
+  alias rtl='echo -ne "\e[2 k"'
+  alias implicit='echo -ne "\e[8h"'
+  alias explicit='echo -ne "\e[8l"'
+  alias bidi='echo -ne "\e[8h"'    # same as implicit
+  alias nobidi='echo -ne "\e[8l"'  # same as explicit
+  alias box-mirror='echo -ne "\e[?2500h"'
+  alias box-normal='echo -ne "\e[?2500l"'
+  alias auto='echo -ne "\e[?2501h"'
+  alias noauto='echo -ne "\e[?2501l"'
+  alias kbdswap='echo -ne "\e[?1243h"'
+  alias nokbdswap='echo -ne "\e[?1243l"'
diff --git a/doc/bidi.txt b/doc/bidi.txt
new file mode 100644
index 00000000..a8145824
--- /dev/null
+++ b/doc/bidi.txt
@@ -0,0 +1,205 @@
+                         ╔════════════════════════════╗
+═════════════════════════╣ BiDi test – for 80 columns ╠═════════════════════════
+                         ╚════════════════════════════╝
+
+In the text examples, the subsection title sometimes shows the wire order,
+transcribed to English (“Shalom” abbreviated to occupy the same width).
+This is followed by the reference rendering, using similar LTR glyphs.
+Finally the actual rendering which should match the line above.
+
+All words, except for subsection titles under explicit modes, should show up
+in human readable order.
+
+At box tests the reference rendering is upside down, so you should get nice
+squares everywhere.
+
+                                ┌──────────────┐
+────────────────────────────────┤ Implicit LTR ├────────────────────────────────
+                                └──────────────┘
+⸤Hello⸣ ⸤Shlm⸣
+⸤Hello⸣ ⸤oi7w⸣
+⸤Hello⸣ ⸤שָׁלוֹם⸣
+
+Hello01 ⸤Hello02⸣ Hello03 Shlm01 ⸤Shlm02⸣ Shlm03
+Hello01 ⸤Hello02⸣ Hello03 03oi7w ⸢02oi7w⸥ 01oi7w
+Hello01 ⸤Hello02⸣ Hello03 שָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03
+
+⸤Shlm⸣ ⸤Hello⸣
+⸤oi7w⸣ ⸤Hello⸣
+⸤שָׁלוֹם⸣ ⸤Hello⸣
+
+Shlm01 ⸤Shlm02⸣ Shlm03 Hello01 ⸤Hello02⸣ Hello03
+03oi7w ⸢02oi7w⸥ 01oi7w Hello01 ⸤Hello02⸣ Hello03
+שָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03 Hello01 ⸤Hello02⸣ Hello03
+
+Paragraph wrapping at foreign word
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w
+oi7 incididunt ut labore et dolore magna aliqua.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם incididunt ut 
labore et dolore magna aliqua.
+
+Double wide
+a<z  n>x
+a<z  א<ת
+
+Box
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮
+┗━┛   a╚═╝z  א┘─└ת   ╰─╯
+
+Box in mirrored mode
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮[?2500h
+┗━┛   a╚═╝z  א└─┘ת   ╰─╯[?2500l
+
+                                ┌──────────────┐
+────────────────────────────────┤ Implicit RTL ├────────────────────────────────
+                                └──────────────┘
+⸤Hello⸣ ⸤Shlm⸣
+                                                                  ⸢oi7w⸥ ⸢Hello⸥
+[2 k⸤Hello⸣ ⸤שָׁלוֹם⸣[1 k
+
+Hello01 ⸤Hello02⸣ Hello03 Shlm01 ⸤Shlm02⸣ Shlm03
+                                03oi7w ⸢02oi7w⸥ 01oi7w Hello01 ⸤Hello02⸣ Hello03
+[2 kHello01 ⸤Hello02⸣ Hello03 שָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03[1 k
+
+⸤Shlm⸣ ⸤Hello⸣
+                                                                  ⸢Hello⸥ ⸢oi7w⸥
+[2 k⸤שָׁלוֹם⸣ ⸤Hello⸣[1 k
+
+Shlm01 ⸤Shlm02⸣ Shlm03 Hello01 ⸤Hello02⸣ Hello03
+                                Hello01 ⸤Hello02⸣ Hello03 03oi7w ⸢02oi7w⸥ 01oi7w
+[2 kשָׁלוֹם01 ⸤שָׁלוֹם02⸣ שָׁלוֹם03 Hello01 ⸤Hello02⸣ Hello03[1 k
+
+Paragraph with wrong direction (should look broken)
+w Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
+                                .incididunt ut labore et dolore magna aliqua oi7
+[2 kLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם incididunt 
ut labore et dolore magna aliqua.[1 k
+
+Double wide
+                                                                      a<z  n>x
+[2 kא<ת  a<z[1 k
+
+Old Hungarian Rovásírás
+(font at https://github.com/OldHungarian/old-hungarian-font)
+                                                         ΛᛩHTΛᛩMↄH ↄ4TH4Λↄ¤ᛝ +ΛↃ
+[2 k𐲛𐳖𐳇 𐲏𐳪𐳙𐳍𐳀𐳢𐳐𐳀𐳙 𐲢𐳛𐳮𐳁𐳤𐳑𐳢𐳁𐳤[1 k
+
+Box
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k
+┛━┗   א╝═╚ת  a└─┘z   ╯─╰[1 k
+
+Box in mirrored mode
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k[?2500h
+┗━┛   א╚═╝ת  a└─┘z   ╰─╯[?2500l[1 k
+
+                        ┌──────────────────────────────┐
+────────────────────────┤ Implicit auto (LTR fallback) ├────────────────────────
+                        └──────────────────────────────┘
+⸤Hello⸣ ⸤Shlm⸣
+⸤Hello⸣ ⸤oi7w⸣
+[?2501h⸤Hello⸣ ⸤שָׁלוֹם⸣[?2501l
+
+⸤Shlm⸣ ⸤Hello⸣
+                                                                  ⸢Hello⸥ ⸢oi7w⸥
+[?2501h⸤שָׁלוֹם⸣ ⸤Hello⸣[?2501l
+
+Paragraph wrapping at foreign word
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w
+oi7 incididunt ut labore et dolore magna aliqua.
+[?2501hLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם 
incididunt ut labore et dolore magna aliqua.[?2501l
+
+Box
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮[?2501h
+┗━┛   a╚═╝z  א┘─└ת   ╰─╯[?2501l
+
+Box in mirrored mode
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮[?2501h[?2500h
+┗━┛   a╚═╝z  א└─┘ת   ╰─╯[?2500l[?2501l
+
+                        ┌──────────────────────────────┐
+────────────────────────┤ Implicit auto (RTL fallback) ├────────────────────────
+                        └──────────────────────────────┘
+⸤Hello⸣ ⸤Shlm⸣
+⸤Hello⸣ ⸤oi7w⸣
+[2 k[?2501h⸤Hello⸣ ⸤שָׁלוֹם⸣[?2501l[1 k
+
+⸤Shlm⸣ ⸤Hello⸣
+                                                                  ⸢Hello⸥ ⸢oi7w⸥
+[2 k[?2501h⸤שָׁלוֹם⸣ ⸤Hello⸣[?2501l[1 k
+
+Paragraph wrapping at foreign word
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor w
+oi7 incididunt ut labore et dolore magna aliqua.
+[2 k[?2501hLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor שָׁלוֹם 
incididunt ut labore et dolore magna aliqua.[?2501l[1 k
+
+Box
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k[?2501h
+┛━┗   א╝═╚ת  a└─┘z   ╯─╰[?2501l[1 k
+
+Box in mirrored mode
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k[?2501h[?2500h
+┗━┛   א╚═╝ת  a└─┘z   ╰─╯[?2500l[?2501l[1 k
+
+                                ┌──────────────┐
+────────────────────────────────┤ Explicit LTR ├────────────────────────────────
+                                └──────────────┘
+⸤Hello⸣ ⸤mlhS⸣
+⸤Hello⸣ ⸤oi7w⸣
+⸤Hello⸣ ⸤םוֹלשָׁ⸣
+
+⸤mlhS⸣ ⸤Hello⸣
+⸤oi7w⸣ ⸤Hello⸣
+⸤םוֹלשָׁ⸣ ⸤Hello⸣
+
+Box
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮
+┗━┛   a╚═╝z  ת└─┘א   ╰─╯
+
+Box in mirrored mode
+┏━┓   a╔═╗z  n┌─┐x   ╭─╮[?2500h
+┗━┛   a╚═╝z  ת└─┘א   ╰─╯[?2500l
+
+                                ┌──────────────┐
+────────────────────────────────┤ Explicit RTL ├────────────────────────────────
+                                └──────────────┘
+⸤olleH⸣ ⸤Shlm⸣
+                                                                  ⸢oi7w⸥ ⸢Hello⸥
+[2 k⸤olleH⸣ ⸤שָׁלוֹם⸣[1 k
+
+⸤Shlm⸣ ⸤olleH⸣
+                                                                  ⸢Hello⸥ ⸢oi7w⸥
+[2 k⸤שָׁלוֹם⸣ ⸤olleH⸣[1 k
+
+Fullwidth characters with underlines
+                        [Lorem  ipsum  dolor  sit  [4:3mamet)
+[2 k[4:3m(tema  tis  rolod  muspi  meroL][1 k
+
+Box
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k
+┛━┗   א╝═╚ת  z┘─└a   ╯─╰[1 k
+
+Box in mirrored mode
+                                                        ╭─╮   a┌─┐z  n╔═╗x   ┏━┓[2 k[?2500h
+┗━┛   א╚═╝ת  z└─┘a   ╰─╯[?2500l[1 k
+
+                                 ┌────────────┐
+─────────────────────────────────┤ Misc tests ├─────────────────────────────────
+                                 └────────────┘
+Attributes – bold ("lo S"), magenta ("hl")
+Hello oi7w
+Hello שָׁלוֹם
+
+Numbers are simply LTR – "jumps over 123 456 789 Shlm!"
+The quick brown fox jumps over the lazy dog The quick brown fox jumps over 123 4
+56 789 oi7w!
+The quick brown fox jumps over the lazy dog The quick brown fox jumps over 123 456 789 שָׁלוֹם!
+
+Numbers are inside RTL – "jumps Shlm 123 456 789 Shlm!"
+The quick brown fox jumps over the lazy dog The quick brown fox jumps 4 123 oi7w
+oi7w 789 56!
+The quick brown fox jumps over the lazy dog The quick brown fox jumps שָׁלוֹם 123 456 789 שָׁלוֹם!
+
+Mirroring across linebreak – "jumps Shlm <[<[<[<[ Shlm!"
+The quick brown fox jumps over the lazy dog The quick brown fox jumps >]>]> oi7w
+oi7w ]>]!
+The quick brown fox jumps over the lazy dog The quick brown fox jumps שָׁלוֹם <[<[<[<[ שָׁלוֹם!
+
+────────────────────────────────────────────────────────────────────────────────
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..8bdef69d
--- /dev/null
+++ b/src/bidi.cc
@@ -0,0 +1,724 @@
+/*
+ * 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
+ */
+
+#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()
+{
+        m_width = 0;
+
+        /* These will be initialized / allocated on demand, when some RTL is encountered. */
+        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 = 128;
+                }
+                while (width > m_width_alloc) {
+                        m_width_alloc *= 2;
+                }
+                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.
+ *
+ * The unshaped character (vteunistr) needs to be passed to this method because
+ * the BiDi component may not store it if no shaping was required, and does not
+ * store combining accents. This method takes care of preserving combining accents.
+ */
+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 _vte_unistr_replace_base(s, 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 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;
+}
+
+
+RingView::RingView()
+{
+        m_ring = nullptr;
+
+        m_start = m_len = m_width = 0;
+        m_height_alloc = 32;
+
+        m_bidirows = (BidiRow **) g_malloc (sizeof (BidiRow *) * m_height_alloc);
+        for (int i = 0; i < m_height_alloc; i++) {
+                m_bidirows[i] = new BidiRow();
+        }
+
+        m_invalid = true;
+}
+
+RingView::~RingView()
+{
+        for (int i = 0; i < m_height_alloc; i++) {
+                delete m_bidirows[i];
+        }
+        g_free (m_bidirows);
+}
+
+void RingView::set_ring(Ring *ring)
+{
+        if (ring == m_ring)
+                return;
+
+        m_ring = ring;
+        m_invalid = true;
+}
+
+void RingView::set_width(vte::grid::column_t width)
+{
+        if (width == m_width)
+                return;
+
+        m_width = width;
+        m_invalid = true;
+}
+
+void RingView::set_rows(vte::grid::row_t start, vte::grid::row_t len)
+{
+        if (start == m_start && len == m_len)
+                return;
+
+        if (G_UNLIKELY (len > m_height_alloc)) {
+                int i = m_height_alloc;
+                while (len > m_height_alloc) {
+                        m_height_alloc *= 2;
+                }
+                m_bidirows = (BidiRow **) g_realloc (m_bidirows, sizeof (BidiRow *) * m_height_alloc);
+                for (; i < m_height_alloc; i++) {
+                        m_bidirows[i] = new BidiRow();
+                }
+        }
+
+        m_start = start;
+        m_len = len;
+        m_invalid = true;
+}
+
+void RingView::maybe_update()
+{
+        if (!m_invalid)
+                return;
+
+        vte::grid::row_t i = m_start;
+        const VteRowData *row_data = m_ring->index_safe(m_start);
+
+        if (row_data && (row_data->attr.bidi_flags & VTE_BIDI_IMPLICIT)) {
+                i = find_paragraph(m_start);
+                if (i == -1) {
+                        i = explicit_paragraph(m_start, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+                }
+        }
+        while (i < m_start + m_len) {
+                i = paragraph(i);
+        }
+
+        m_invalid = false;
+}
+
+BidiRow const* RingView::get_row_map(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);
+
+        return m_bidirows[row - m_start];
+}
+
+BidiRow* RingView::get_row_map_writable(vte::grid::row_t row) const
+{
+        g_assert_cmpint (row, >=, m_start);
+        g_assert_cmpint (row, <, m_start + m_len);
+
+        return m_bidirows[row - m_start];
+}
+
+/* Set up the mapping according to explicit mode for a given line. */
+void RingView::explicit_line(vte::grid::row_t row, bool rtl)
+{
+        int i;
+
+        if (G_UNLIKELY (row < m_start || row >= m_start + m_len))
+                return;
+
+        BidiRow *bidirow = get_row_map_writable(row);
+        bidirow->m_base_rtl = rtl;
+        bidirow->m_has_foreign = false;
+
+        if (G_UNLIKELY (rtl)) {
+                bidirow->set_width(m_width);
+                for (i = 0; i < m_width; i++) {
+                        bidirow->m_log2vis[i] = bidirow->m_vis2log[i] = m_width - 1 - i;
+                        bidirow->m_vis_rtl[i] = true;
+                        bidirow->m_vis_shaped_char[i] = 0;
+                }
+        } else {
+                /* Shortcut: bidirow->m_width == 0 might denote a fully LTR line,
+                 * m_width_alloc might even be 0 along with log2vis and friends being nullptr in this case. 
*/
+                bidirow->set_width(0);
+        }
+}
+
+/* Set up the mapping according to explicit mode, for all the lines
+ * of a paragraph beginning at the given line.
+ * Returns the row number after the paragraph or viewport (whichever ends first). */
+vte::grid::row_t RingView::explicit_paragraph(vte::grid::row_t row, bool rtl)
+{
+        const VteRowData *row_data;
+
+        while (row < m_start + m_len) {
+                explicit_line(row, rtl);
+
+                row_data = m_ring->index_safe(row++);
+                if (row_data == nullptr || !row_data->attr.soft_wrapped)
+                        break;
+        }
+        return row;
+}
+
+/* For the given row, find the first row of its paragraph.
+ * Returns -1 if have to walk backwards too much. */
+/* FIXME this could be much cheaper, we don't need to read the actual rows (text_stream),
+ * we only need the soft_wrapped flag which is stored in row_stream. Needs method in ring. */
+vte::grid::row_t RingView::find_paragraph(vte::grid::row_t row)
+{
+        vte::grid::row_t row_stop = row - VTE_BIDI_PARAGRAPH_LENGTH_MAX;
+        const VteRowData *row_data;
+
+        while (row-- > row_stop) {
+                if (row < _vte_ring_delta(m_ring))
+                        return row + 1;
+                row_data = m_ring->index_safe(row);
+                if (row_data == nullptr || !row_data->attr.soft_wrapped)
+                        return row + 1;
+        }
+        return -1;
+}
+
+/* Figure out the mapping for the paragraph starting at the given row.
+ * Returns the row number after the paragraph or viewport (whichever ends first). */
+vte::grid::row_t RingView::paragraph(vte::grid::row_t row)
+{
+        const VteRowData *row_data = m_ring->index_safe(row);
+        if (row_data == nullptr) {
+                return explicit_paragraph(row, false);
+        }
+
+#ifndef WITH_FRIBIDI
+        return explicit_paragraph(row, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+#else
+        const VteCell *cell;
+        bool rtl;
+        bool autodir;
+        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;
+
+        if (!(row_data->attr.bidi_flags & VTE_BIDI_IMPLICIT)) {
+                return explicit_paragraph(row, row_data->attr.bidi_flags & VTE_BIDI_RTL);
+        }
+
+        rtl = row_data->attr.bidi_flags & VTE_BIDI_RTL;
+        autodir = row_data->attr.bidi_flags & VTE_BIDI_AUTO;
+
+        int lines[VTE_BIDI_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 row_orig = row;
+        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.
+         */
+        while (row < _vte_ring_next(m_ring)) {
+                row_data = m_ring->index_safe(row);
+                if (row_data == nullptr)
+                        break;
+
+                if (line == VTE_BIDI_PARAGRAPH_LENGTH_MAX) {
+                        /* Overlong paragraph, bail out. */
+                        g_array_free (fribidi_chars_array, TRUE);
+                        g_array_free (fribidi_map_array, TRUE);
+                        g_array_free (fribidi_to_term_array, TRUE);
+                        return explicit_paragraph (row_orig, rtl);
+                }
+
+                /* 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 < m_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;
+                row++;
+
+                if (!row_data->attr.soft_wrapped)
+                        break;
+        }
+
+        if (line == 0) {
+                /* Beyond the end of the ring. */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                return explicit_paragraph (row_orig, rtl);
+        }
+
+        /* 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);
+
+        if (level == 0) {
+                /* error */
+                g_array_free (fribidi_chars_array, TRUE);
+                g_array_free (fribidi_map_array, TRUE);
+                g_array_free (fribidi_to_term_array, TRUE);
+                return explicit_paragraph (row_orig, rtl);
+        }
+
+        /* Arabic shaping
+         *
+         * https://www.w3.org/TR/css-text-3/#word-break-shaping says:
+         * "When shaping scripts such as Arabic wrap [...] the characters must still be shaped (their 
joining forms chosen)
+         * as if the word were still whole."
+         *
+         * Also, FriBidi's Arabic shaping methods, as opposed to fribidi_reorder_line(), don't take an 
offset parameter.
+         * This is another weak sign that the desired behavior is to shape the entire paragraph before 
splitting to lines.
+         *
+         * We only perform shaping in implicit mode, for two reasons:
+         *
+         * Following the CSS logic, I think the sensible behavior for a partially visible word (e.g. at the 
margin of a
+         * text editor) is to use the joining/shaping form according to the entire word. Hence in explicit 
mode it must be
+         * the responsibility of the BiDi-aware application and not the terminal emulator to perform 
joining/shaping.
+         *
+         * And a technical limitation: FriBidi can only perform joining/shaping with the logical order as 
input, not with
+         * the visual order. We'd need to find another API, or do ugly workarounds, which I'd rather not. */
+        fribidi_join_arabic (fribidi_chartypes, count, fribidi_levels, fribidi_joiningtypes);
+        fribidi_shape_arabic (FRIBIDI_FLAGS_ARABIC, fribidi_levels, count, fribidi_joiningtypes, 
fribidi_chars);
+
+        g_assert_cmpint (pbase_dir, !=, FRIBIDI_PAR_ON);
+        /* For convenience, from now on this variable contains the resolved (i.e. possibly autodetected) 
value. */
+        rtl = (pbase_dir == FRIBIDI_PAR_RTL || pbase_dir == FRIBIDI_PAR_WRTL);
+
+        if (!rtl && level == 1) {
+                /* Fast 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);
+                return explicit_paragraph (row_orig, false);
+        }
+
+        /* Reshuffle line by line. */
+        row = row_orig;
+        line = 0;
+        if (G_UNLIKELY (row < m_start)) {
+                line = m_start - row;
+                row = m_start;
+        }
+
+        while (row < _vte_ring_next(m_ring) && row < m_start + m_len) {
+                bidirow = get_row_map_writable(row);
+                bidirow->m_base_rtl = rtl;
+                bidirow->m_has_foreign = true;
+                bidirow->set_width(m_width);
+
+                row_data = m_ring->index_safe(row);
+                if (row_data == nullptr)
+                        break;
+
+                level = fribidi_reorder_line (FRIBIDI_FLAGS_DEFAULT,
+                                              fribidi_chartypes,
+                                              lines[line + 1] - lines[line],
+                                              lines[line],
+                                              pbase_dir,
+                                              fribidi_levels,
+                                              NULL,
+                                              fribidi_map);
+
+                if (level == 0) {
+                        /* error, what should we do? */
+                        explicit_line (row, rtl);
+                        bidirow->m_has_foreign = true;
+                        goto next_line;
+                }
+
+                if (!rtl && level == 1) {
+                        /* Fast shortcut for LTR-only lines. */
+                        explicit_line (row, false);
+                        bidirow->m_has_foreign = true;
+                        goto next_line;
+                }
+
+                /* Copy to our realm. Proceed in visual order.*/
+                tv = 0;
+                if (rtl) {
+                        /* Unused cells on the left for RTL paragraphs */
+                        int unused = MAX(m_width - row_data->len, 0);
+                        for (; tv < unused; tv++) {
+                                bidirow->m_vis2log[tv] = m_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, m_width));
+                        for (; tv < m_width; tv++) {
+                                bidirow->m_vis2log[tv] = tv;
+                                bidirow->m_vis_rtl[tv] = false;
+                                bidirow->m_vis_shaped_char[tv] = 0;
+                        }
+                }
+                g_assert_cmpint (tv, ==, m_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 < m_width; tl++) {
+                                bidirow->m_log2vis[tl] = -1;
+                        }
+                }
+
+                for (tv = 0; tv < m_width; tv++) {
+                        bidirow->m_log2vis[bidirow->m_vis2log[tv]] = tv;
+                }
+
+                if (_vte_debug_on (VTE_DEBUG_BIDI)) {
+                        for (tl = 0; tl < m_width; tl++) {
+                                g_assert_cmpint (bidirow->m_log2vis[tl], !=, -1);
+                        }
+                }
+
+next_line:
+                line++;
+                row++;
+
+                if (!row_data->attr.soft_wrapped)
+                        break;
+        }
+
+        g_array_free (fribidi_chars_array, TRUE);
+        g_array_free (fribidi_map_array, TRUE);
+        g_array_free (fribidi_to_term_array, TRUE);
+
+        return row;
+#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..38e8035e
--- /dev/null
+++ b/src/bidi.hh
@@ -0,0 +1,124 @@
+/*
+ * 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 "vterowdata.hh"
+#include "vtetypes.hh"
+#include "vteunistr.h"
+
+namespace vte {
+
+namespace base {  // FIXME ???
+
+/* BidiRow contains the BiDi transformation of a single row. */
+class BidiRow {
+        friend class RingView;
+
+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;
+        vte::grid::column_t *m_vis2log;
+        guint8 *m_vis_rtl;
+        gunichar *m_vis_shaped_char;
+
+        guint8 m_base_rtl: 1;
+        guint8 m_has_foreign: 1;
+};
+
+
+/* RingView contains the BiDi transformations for all the rows of the viewport. */
+class RingView {
+public:
+        RingView();
+        ~RingView();
+
+        // prevent accidents
+        RingView(RingView& o) = delete;
+        RingView(RingView const& o) = delete;
+        RingView(RingView&& o) = delete;
+        RingView& operator= (RingView& o) = delete;
+        RingView& operator= (RingView const& o) = delete;
+        RingView& operator= (RingView&& o) = delete;
+
+        void set_ring(Ring *ring);
+        void set_rows(vte::grid::row_t start, vte::grid::row_t len);
+        void set_width(vte::grid::column_t width);
+
+        inline void invalidate() { m_invalid = true; }
+        void maybe_update();
+
+        BidiRow const* get_row_map(vte::grid::row_t row) const;
+
+private:
+        Ring *m_ring;
+
+        BidiRow **m_bidirows;
+
+        vte::grid::row_t m_start;
+        vte::grid::row_t m_len;
+        vte::grid::column_t m_width;
+
+        vte::grid::row_t m_height_alloc;
+
+        bool m_invalid;
+
+        BidiRow* get_row_map_writable(vte::grid::row_t row) const;
+
+        void explicit_line(vte::grid::row_t row, bool rtl);
+        vte::grid::row_t explicit_paragraph(vte::grid::row_t row, bool rtl);
+        vte::grid::row_t find_paragraph(vte::grid::row_t row);
+        vte::grid::row_t paragraph(vte::grid::row_t row);
+};
+
+}; /* 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/debug.cc b/src/debug.cc
index 5d88833b..afbd7413 100644
--- a/src/debug.cc
+++ b/src/debug.cc
@@ -57,6 +57,7 @@ _vte_debug_init(void)
     { "hyperlink",    VTE_DEBUG_HYPERLINK    },
     { "modes",        VTE_DEBUG_MODES        },
     { "emulation",    VTE_DEBUG_EMULATION    },
+    { "bidi",         VTE_DEBUG_BIDI         },
   };
 
   _vte_debug_flags = g_parse_debug_string (g_getenv("VTE_DEBUG"),
diff --git a/src/debug.h b/src/debug.h
index 41c3665d..92d9d0f9 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -64,6 +64,7 @@ typedef enum {
         VTE_DEBUG_HYPERLINK     = 1 << 24,
         VTE_DEBUG_MODES         = 1 << 25,
         VTE_DEBUG_EMULATION     = 1 << 26,
+        VTE_DEBUG_BIDI          = 1 << 27,
 } VteDebugFlags;
 
 void _vte_debug_init(void);
diff --git a/src/meson.build b/src/meson.build
index 29046b77..fef7da1e 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',
@@ -144,6 +146,7 @@ libvte_common_public_deps = [
 ]
 
 libvte_common_deps = libvte_common_public_deps + [
+  fribidi_dep,
   gnutls_dep,
   pcre2_dep,
   libm_dep,
diff --git a/src/modes-ecma.hh b/src/modes-ecma.hh
index 617d2985..9c767ac1 100644
--- a/src/modes-ecma.hh
+++ b/src/modes-ecma.hh
@@ -38,6 +38,20 @@
  */
 MODE(IRM,  4)
 
+/*
+ * BDSM - Bi-Directional Support Mode
+ *
+ * Reset state is explicit mode, set state is implicit mode
+ *
+ * References: ECMA-48
+ *             ECMA TR/53
+ *             [FIXME link to our spec]
+ *
+ * Default in ECMA: reset
+ * Default in VTE: set
+ */
+MODE(BDSM, 8)
+
 /*
  * SRM - local echo send/receive mode
  * If reset, characters entered by the keyboard are shown on the
@@ -59,8 +73,7 @@ MODE_FIXED(CRM,   3, ALWAYS_RESET)
 MODE_FIXED(SRTM,  5, ALWAYS_RESET)
 MODE_FIXED(ERM,   6, ALWAYS_RESET)
 MODE_FIXED(VEM,   7, ALWAYS_RESET)
-MODE_FIXED(BDSM,  8, ALWAYS_RESET)
-MODE_FIXED(DCSM,  9, ALWAYS_RESET)
+MODE_FIXED(DCSM,  9, ALWAYS_SET)
 MODE_FIXED(HEM,  10, ALWAYS_RESET)
 MODE_FIXED(PUM,  11, ALWAYS_RESET) /* ECMA-48 § F.4.1 Deprecated */
 MODE_FIXED(FEAM, 13, ALWAYS_RESET)
diff --git a/src/modes-private.hh b/src/modes-private.hh
index eb313823..80bc80d2 100644
--- a/src/modes-private.hh
+++ b/src/modes-private.hh
@@ -142,6 +142,32 @@ MODE(XTERM_READLINE_BRACKETED_PASTE, 2004)
 
 MODE(URXVT_MOUSE_EXT, 1015)
 
+/* VTE */
+
+/*
+ * Whether box drawing characters in the U+2500..U+257F range
+ * are to be mirrored in RTL context.
+ *
+ * The (temporary) choice of number 2500 is a misuse of hex 2500
+ * as a decimal number, but is supposed to be easily memorable.
+ */
+MODE(VTE_BOX_DRAWING_MIRROR, 2500)
+
+/*
+ * Whether BiDi paragraph direction is autodetected.
+ *
+ * The number choice is temporary.
+ */
+MODE(VTE_BIDI_AUTO, 2501)
+
+/*
+ * Whether to swap the Left and Right arrow keys if the cursor
+ * stands over an RTL paragraphs.
+ *
+ * The number choice is not necessarily final.
+ */
+MODE(VTE_BIDI_SWAP_ARROW_KEYS, 1243)
+
 /* Not supported modes: */
 
 /* DEC */
diff --git a/src/modes.hh b/src/modes.hh
index 47d0b415..af218622 100644
--- a/src/modes.hh
+++ b/src/modes.hh
@@ -170,7 +170,8 @@ public:
 #undef MODE
 #undef MODE_FIXED
 
-        constexpr ECMA() : Self{eSRM} { }
+        constexpr ECMA() : Self{eBDSM,
+                                eSRM} { }
 
 }; // class ECMA
 
@@ -226,6 +227,7 @@ public:
 
         constexpr Private() : Self{eDEC_AUTOWRAP,
                                    eDEC_TEXT_CURSOR,
+                                   eVTE_BIDI_SWAP_ARROW_KEYS,
                                    eXTERM_ALTBUF_SCROLL,
                                    eXTERM_META_SENDS_ESCAPE} { }
 
diff --git a/src/parser-cmd.hh b/src/parser-cmd.hh
index 6fcaacc2..c562d7c0 100644
--- a/src/parser-cmd.hh
+++ b/src/parser-cmd.hh
@@ -104,11 +104,13 @@ _VTE_CMD(RM_DEC) /* reset mode dec */
 _VTE_CMD(RM_ECMA) /* reset mode ecma */
 _VTE_CMD(SCORC) /* SCO restore cursor */
 _VTE_CMD(SCOSC) /* SCO save cursor */
+_VTE_CMD(SCP) /* select character path */
 _VTE_CMD(SD) /* scroll down */
 _VTE_CMD(SD_OR_XTERM_IHMT) /* scroll down or xterm initiate highlight mouse tracking */
 _VTE_CMD(SGR) /* select graphics rendition */
 _VTE_CMD(SM_DEC) /* set mode dec */
 _VTE_CMD(SM_ECMA) /* set mode ecma */
+_VTE_CMD(SPD) /* select presentation directions */
 _VTE_CMD(SS2) /* single shift 2 */
 _VTE_CMD(SS3) /* single shift 3 */
 _VTE_CMD(SUB) /* substitute */
@@ -276,7 +278,6 @@ _VTE_NOP(RLOGIN_MML) /* RLogin music macro language */
 _VTE_NOP(SACS) /* set additional character separation */
 _VTE_NOP(SAPV) /* select alternative presentation variants */
 _VTE_NOP(SCO) /* select character orientation */
-_VTE_NOP(SCP) /* select character path */
 _VTE_NOP(SCS) /* set character spacing */
 _VTE_NOP(SDS) /* start directed string */
 _VTE_NOP(SEE) /* select editing extent */
@@ -289,7 +290,6 @@ _VTE_NOP(SL) /* scroll left */
 _VTE_NOP(SLS) /* set line spacing */
 _VTE_NOP(SOH) /* start of heading */
 _VTE_NOP(SPA) /* start of protected area */
-_VTE_NOP(SPD) /* select presentation directions */
 _VTE_NOP(SPH) /* set page home */
 _VTE_NOP(SPI) /* spacing increment */
 _VTE_NOP(SPL) /* set page limit */
diff --git a/src/parser-csi.hh b/src/parser-csi.hh
index 410d533f..53e1f1f8 100644
--- a/src/parser-csi.hh
+++ b/src/parser-csi.hh
@@ -59,7 +59,7 @@ _VTE_NOQ(SEE,                    CSI,    'Q',  NONE,  0, NONE     ) /* select ed
 _VTE_NOQ(PPR,                    CSI,    'Q',  NONE,  1, SPACE    ) /* page-position-relative */
 _VTE_NOQ(PPB,                    CSI,    'R',  NONE,  1, SPACE    ) /* page-position-backward */
 _VTE_SEQ(SU,                     CSI,    'S',  NONE,  0, NONE     ) /* scroll-up */
-_VTE_NOQ(SPD,                    CSI,    'S',  NONE,  1, SPACE    ) /* select presentation directions */
+_VTE_SEQ(SPD,                    CSI,    'S',  NONE,  1, SPACE    ) /* select presentation directions */
 _VTE_NOQ(XTERM_SGFX,             CSI,    'S',  WHAT,  0, NONE     ) /* xterm-sixel-graphics */
 _VTE_SEQ(SD_OR_XTERM_IHMT,       CSI,    'T',  NONE,  0, NONE     ) /* scroll-down or 
xterm-initiate-highlight-mouse-tracking */
 _VTE_NOQ(DTA,                    CSI,    'T',  NONE,  1, SPACE    ) /* dimension text area */
@@ -114,7 +114,7 @@ _VTE_NOQ(MC_DEC,                 CSI,    'i',  WHAT,  0, NONE     ) /* media-cop
 _VTE_NOQ(HPB,                    CSI,    'j',  NONE,  0, NONE     ) /* horizontal position backward */
 _VTE_NOQ(SPL,                    CSI,    'j',  NONE,  1, SPACE    ) /* set page limit */
 _VTE_NOQ(VPB,                    CSI,    'k',  NONE,  0, NONE     ) /* line position backward */
-_VTE_NOQ(SCP,                    CSI,    'k',  NONE,  1, SPACE    ) /* select character path */
+_VTE_SEQ(SCP,                    CSI,    'k',  NONE,  1, SPACE    ) /* select character path */
 _VTE_SEQ(RM_ECMA,                CSI,    'l',  NONE,  0, NONE     ) /* reset-mode-ecma */
 _VTE_SEQ(RM_DEC,                 CSI,    'l',  WHAT,  0, NONE     ) /* reset-mode-dec */
 _VTE_SEQ(SGR,                    CSI,    'm',  NONE,  0, NONE     ) /* select-graphics-rendition */
diff --git a/src/ring.cc b/src/ring.cc
index d7a3bde1..93b8d8b0 100644
--- a/src/ring.cc
+++ b/src/ring.cc
@@ -373,6 +373,7 @@ Ring::freeze_row(row_t position,
        if (!row->attr.soft_wrapped)
                g_string_append_c (buffer, '\n');
        record.soft_wrapped = row->attr.soft_wrapped;
+       record.bidi_flags = row->attr.bidi_flags;
 
        _vte_stream_append(m_text_stream, buffer->str, buffer->len);
        append_row_record(&record, position);
@@ -435,6 +436,7 @@ Ring::thaw_row(row_t position,
                 g_string_truncate (buffer, buffer->len - 1);
        else
                row->attr.soft_wrapped = TRUE;
+       row->attr.bidi_flags = records[0].bidi_flags;
 
        p = buffer->str;
        end = p + buffer->len;
@@ -607,6 +609,15 @@ Ring::index(row_t position)
        return &m_cached_row;
 }
 
+VteRowData const*
+Ring::index_safe(row_t position)
+{
+        if (G_UNLIKELY (position < m_start || position >= m_end))
+                return nullptr;
+
+        return index(position);
+}
+
 /*
  * Returns the hyperlink idx at the given position.
  *
@@ -849,7 +860,7 @@ Ring::shrink(row_t max_len)
  * Return: the newly added row.
  */
 VteRowData*
-Ring::insert(row_t position)
+Ring::insert(row_t position, guint8 bidi_flags)
 {
        row_t i;
        VteRowData* row, tmp;
@@ -871,6 +882,7 @@ Ring::insert(row_t position)
        *get_writable_index(position) = tmp;
 
        row = get_writable_index(position);
+       row->attr.bidi_flags = bidi_flags;
        _vte_row_data_clear (row);
        m_end++;
 
@@ -921,9 +933,9 @@ Ring::remove(row_t position)
  * Return: the newly added row.
  */
 VteRowData*
-Ring::append()
+Ring::append(guint8 bidi_flags)
 {
-       return insert(next());
+       return insert(next(), bidi_flags);
 }
 
 
@@ -1187,6 +1199,7 @@ Ring::rewrap(column_t columns,
                /* Find the boundaries of the next paragraph */
                gboolean prev_record_was_soft_wrapped = FALSE;
                gboolean paragraph_is_ascii = TRUE;
+               guint8 paragraph_bidi_flags = old_record.bidi_flags;
                gsize text_offset = paragraph_start_text_offset;
                RowRecord new_record;
                column_t col = 0;
@@ -1234,6 +1247,7 @@ Ring::rewrap(column_t columns,
                new_record.text_start_offset = text_offset;
                new_record.attr_start_offset = attr_offset;
                new_record.is_ascii = paragraph_is_ascii;
+               new_record.bidi_flags = paragraph_bidi_flags;
 
                while (paragraph_len > 0) {
                        /* Wrap one continuous run of identical attributes within the paragraph. */
diff --git a/src/ring.hh b/src/ring.hh
index 1b17ca7a..831a55da 100644
--- a/src/ring.hh
+++ b/src/ring.hh
@@ -73,6 +73,7 @@ public:
         //FIXMEchpe rename this to at()
         //FIXMEchpe use references not pointers
         VteRowData const* index(row_t position); /* const? */
+        VteRowData const* index_safe(row_t position);
         VteRowData* index_writable(row_t position);
 
         void hyperlink_maybe_gc(row_t increment);
@@ -85,8 +86,8 @@ public:
         row_t reset();
         void resize(row_t max_rows = kDefaultMaxRows);
         void shrink(row_t max_len = kDefaultMaxRows);
-        VteRowData* insert(row_t position);
-        VteRowData* append();
+        VteRowData* insert(row_t position, guint8 bidi_flags);
+        VteRowData* append(guint8 bidi_flags);
         void remove(row_t position);
         void drop_scrollback(row_t position);
         void set_visible_rows(row_t rows);
@@ -120,6 +121,7 @@ private:
                 size_t attr_start_offset;  /* offset of the first character's attributes */
                 int soft_wrapped: 1;      /* end of line is not '\n' */
                 int is_ascii: 1;          /* for rewrapping speedup: guarantees that line contains 32..126 
bytes only. Can be 0 even when ascii only. */
+                guint8 bidi_flags: 4;
         } RowRecord;
 
         static_assert(std::is_pod<RowRecord>::value, "Ring::RowRecord is not POD");
@@ -248,8 +250,8 @@ static inline auto _vte_ring_get_hyperlink_at_position (VteRing *ring, gulong po
 static inline long _vte_ring_reset (VteRing *ring) { return ring->reset(); }
 static inline void _vte_ring_resize (VteRing *ring, gulong max_rows) { ring->resize(max_rows); }
 static inline void _vte_ring_shrink (VteRing *ring, gulong max_len) { ring->shrink(max_len); }
-static inline VteRowData *_vte_ring_insert (VteRing *ring, gulong position) { return ring->insert(position); 
}
-static inline VteRowData *_vte_ring_append (VteRing *ring) { return ring->append(); }
+static inline VteRowData *_vte_ring_insert (VteRing *ring, gulong position, guint8 bidi_flags) { return 
ring->insert(position, bidi_flags); }
+static inline VteRowData *_vte_ring_append (VteRing *ring, guint8 bidi_flags) { return 
ring->append(bidi_flags); }
 static inline void _vte_ring_remove (VteRing *ring, gulong position) { ring->remove(position); }
 static inline void _vte_ring_drop_scrollback (VteRing *ring, gulong position) { 
ring->drop_scrollback(position); }
 static inline void _vte_ring_set_visible_rows (VteRing *ring, gulong rows) { ring->set_visible_rows(rows); }
diff --git a/src/vte.cc b/src/vte.cc
index b02b673d..c92adab8 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -36,6 +36,7 @@
 
 #include <vte/vte.h>
 #include "vteinternal.hh"
+#include "bidi.hh"
 #include "buffer.h"
 #include "debug.h"
 #include "vtedraw.hh"
@@ -139,11 +140,11 @@ Terminal::ring_insert(vte::grid::row_t position,
         bool const not_default_bg = (m_fill_defaults.attr.back() != VTE_DEFAULT_BG);
 
        while (G_UNLIKELY (_vte_ring_next (ring) < position)) {
-               row = _vte_ring_append (ring);
+               row = _vte_ring_append (ring, get_bidi_flags());
                 if (not_default_bg)
                         _vte_row_data_fill (row, &m_fill_defaults, m_column_count);
        }
-       row = _vte_ring_insert (ring, position);
+       row = _vte_ring_insert (ring, position, get_bidi_flags());
         if (fill && not_default_bg)
                 _vte_row_data_fill (row, &m_fill_defaults, m_column_count);
        return row;
@@ -263,6 +264,13 @@ Terminal::invalidate_rows(vte::grid::row_t row_start,
                           row_start, row_end);
        _vte_debug_print (VTE_DEBUG_WORK, "?");
 
+        // HACK for BiDi: Always invalidate everything.
+        // In fact we'd need to invalidate the entire implicit paragraph.
+       if (TRUE) {
+               invalidate_all();
+               return;
+       }
+
         /* Scrolled back, visible parts didn't change. */
         if (row_start > last_displayed_row())
                 return;
@@ -388,7 +396,7 @@ Terminal::invalidate_all()
 /* Find the row in the given position in the backscroll buffer.
  * Note that calling this method may invalidate the return value of
  * a previous find_row_data() call. */
-// FIXMEchpe replace this with a method on VteRing
+// FIXMEchpe replace this with a method on VteRing (index_safe())
 VteRowData const*
 Terminal::find_row_data(vte::grid::row_t row) const
 {
@@ -1540,9 +1548,22 @@ Terminal::grid_coords_from_view_coords(vte::view::coords const& pos) const
 
         vte::grid::row_t row = pixel_to_row(pos.y);
 
+        /* BiDi: convert to logical column. */
+        vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+        col = bidirow->vis2log(col);
+
         return vte::grid::coords(row, col);
 }
 
+vte::grid::row_t
+Terminal::confine_grid_row(vte::grid::row_t const& row) const
+{
+        auto first_row = first_displayed_row();
+        auto last_row = last_displayed_row();
+
+        return CLAMP(row, first_row, last_row);
+}
+
 /*
  * Terminal::confined_grid_coords_from_view_coords:
  * @pos: the view coordinates
@@ -1604,34 +1625,46 @@ Terminal::confine_grid_coords(vte::grid::coords const& rowcol) const
 
 /*
  * Track mouse click and drag positions (the "origin" and "last" coordinates) with half cell accuracy,
- * that is, know whether the event occurred over the left or right half of the cell.
+ * that is, know whether the event occurred over the left/start or right/end half of the cell.
  * This is required because some selection modes care about the cell over which the event occurred,
  * while some care about the closest boundary between cells.
  *
  * Storing the actual view coordinates would become problematic when the font size changes (bug 756058),
  * and would cause too much work when the mouse moves within the half cell.
  *
- * Left margin or anything further to the left is denoted by column -1's right half,
- * right margin or anything further to the right is denoted by column m_column_count's left half.
+ * Left/start margin or anything further to the left/start is denoted by column -1's right half,
+ * right/end margin or anything further to the right/end is denoted by column m_column_count's left half.
+ *
+ * BiDi: returns logical (start/end) position for normal selection modes, visual (left/right) position for 
block mode.
  */
 vte::grid::halfcoords
 Terminal::selection_grid_halfcoords_from_view_coords(vte::view::coords const& pos) const
 {
         vte::grid::row_t row = pixel_to_row(pos.y);
-        vte::grid::halfcolumn_t halfcolumn;
+        vte::grid::column_t col;
+        vte::grid::half_t half;
 
         if (pos.x < 0) {
-                halfcolumn.set_column(-1);
-                halfcolumn.set_half(1);
+                col = -1;
+                half = 1;
         } else if (pos.x >= m_column_count * m_cell_width) {
-                halfcolumn.set_column(m_column_count);
-                halfcolumn.set_half(0);
+                col = m_column_count;
+                half = 0;
         } else {
-                halfcolumn.set_column(pos.x / m_cell_width);
-                halfcolumn.set_half((pos.x * 2 / m_cell_width) % 2);
+                col = pos.x / m_cell_width;
+                half = (pos.x * 2 / m_cell_width) % 2;
         }
 
-        return { row, halfcolumn };
+        if (!m_selection_block_mode) {
+                /* BiDi: convert from visual to logical half column. */
+                vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+
+                if (bidirow->vis_is_rtl(col))
+                        half = 1 - half;
+                col = bidirow->vis2log(col);
+        }
+
+        return { row, vte::grid::halfcolumn_t(col, half) };
 }
 
 /*
@@ -1648,6 +1681,8 @@ Terminal::selection_maybe_swap_endpoints(vte::view::coords const& pos)
         if (m_selection_resolved.empty())
                 return;
 
+        ringview_maybe_update();
+
         auto current = selection_grid_halfcoords_from_view_coords (pos);
 
         if (m_selection_block_mode) {
@@ -2894,6 +2929,8 @@ Terminal::insert_char(gunichar c,
                        row = ensure_row();
                         set_soft_wrapped(m_screen->cursor.row);
                         cursor_down(false);
+                        ensure_row();
+                        apply_bidi_attributes(m_screen->cursor.row, row->attr.bidi_flags, VTE_BIDI_ALL);
                } else {
                        /* Don't wrap, stay at the rightmost column. */
                         col = m_screen->cursor.col =
@@ -3030,6 +3067,72 @@ not_inserted:
         m_line_wrapped = line_wrapped;
 }
 
+guint8
+Terminal::get_bidi_flags()
+{
+        return (m_modes_ecma.BDSM() ? VTE_BIDI_IMPLICIT : 0) |
+               (m_bidi_rtl ? VTE_BIDI_RTL : 0) |
+               (m_modes_private.VTE_BIDI_AUTO() ? VTE_BIDI_AUTO : 0) |
+               (m_modes_private.VTE_BOX_DRAWING_MIRROR() ? VTE_BIDI_BOX_MIRROR : 0);
+}
+
+/* Apply the specified BiDi parameters on the paragraph beginning at the specified line. */
+void
+Terminal::apply_bidi_attributes(vte::grid::row_t row, guint8 bidi_flags, guint8 bidi_flags_mask)
+{
+        VteRowData *rowdata;
+
+        bidi_flags &= bidi_flags_mask;
+
+        while (true) {
+                rowdata = _vte_ring_index_writable (m_screen->row_data, row);
+                if (rowdata == nullptr)
+                        return;
+
+                _vte_debug_print(VTE_DEBUG_BIDI,
+                                 "Applying BiDi parameters on row %ld.\n", row);
+
+                rowdata->attr.bidi_flags &= ~bidi_flags_mask;
+                rowdata->attr.bidi_flags |= bidi_flags;
+
+                invalidate_row(row);
+                if (!rowdata->attr.soft_wrapped)
+                        return;
+                row++;
+        }
+}
+
+/* Apply the current BiDi parameters covered by bidi_flags_mask on the current paragraph
+ * if the cursor is at the first position of this paragraph. */
+void
+Terminal::maybe_apply_bidi_attributes(guint8 bidi_flags_mask)
+{
+        _vte_debug_print(VTE_DEBUG_BIDI,
+                         "Maybe applying BiDi parameters on current paragraph.\n");
+
+        if (m_screen->cursor.col != 0) {
+                _vte_debug_print(VTE_DEBUG_BIDI,
+                                 "No, cursor not in first column.\n");
+                return;
+        }
+
+        auto row = m_screen->cursor.row;
+
+        if (row > _vte_ring_delta (m_screen->row_data)) {
+                const VteRowData *rowdata = _vte_ring_index (m_screen->row_data, row - 1);
+                if (rowdata != nullptr && rowdata->attr.soft_wrapped) {
+                        _vte_debug_print(VTE_DEBUG_BIDI,
+                                         "No, we're not after a hard wrap.\n");
+                        return;
+                }
+        }
+
+        _vte_debug_print(VTE_DEBUG_BIDI,
+                         "Yes, applying.\n");
+
+        apply_bidi_attributes (row, get_bidi_flags(), bidi_flags_mask);
+}
+
 static void
 reaper_child_exited_cb(VteReaper *reaper,
                        int ipid,
@@ -3773,6 +3876,11 @@ Terminal::process_incoming()
                queue_contents_changed();
        }
 
+        /* BiDi properties might have changed, even when !modified.
+         * emit_pending_signals() requires the ringview to be updated. */
+        m_ringview.invalidate();
+        ringview_maybe_update();
+
        emit_pending_signals();
 
        if (invalidated_text) {
@@ -4888,6 +4996,39 @@ Terminal::widget_key_press(GdkEventKey *event)
                /* If the above switch statement didn't do the job, try mapping
                 * it to a literal or capability name. */
                 if (handled == FALSE) {
+                        /* In keyboard arrow swapping mode, the left and right arrows
+                         * are swapped if the cursor stands inside an RTL paragraph. */
+                        if (m_modes_private.VTE_BIDI_SWAP_ARROW_KEYS() &&
+                            (keyval == GDK_KEY_Left ||
+                             keyval == GDK_KEY_Right ||
+                             keyval == GDK_KEY_KP_Left ||
+                             keyval == GDK_KEY_KP_Right)) {
+                                /* m_ringview is for the onscreen contents and the cursor may be
+                                 * offscreen, so use a temporary ringview for the cursor's row. */
+                                vte::base::RingView *ringview = new vte::base::RingView();
+                                ringview->set_ring(m_screen->row_data);
+                                ringview->set_rows(m_screen->cursor.row, 1);
+                                ringview->set_width(m_column_count);
+                                ringview->maybe_update();
+                                if (ringview->get_row_map(m_screen->cursor.row)->base_is_rtl()) {
+                                        switch (keyval) {
+                                        case GDK_KEY_Left:
+                                                keyval = GDK_KEY_Right;
+                                                break;
+                                        case GDK_KEY_Right:
+                                                keyval = GDK_KEY_Left;
+                                                break;
+                                        case GDK_KEY_KP_Left:
+                                                keyval = GDK_KEY_KP_Right;
+                                                break;
+                                        case GDK_KEY_KP_Right:
+                                                keyval = GDK_KEY_KP_Left;
+                                                break;
+                                        }
+                                }
+                                delete ringview;
+                        }
+
                        _vte_keymap_map(keyval, m_modifiers,
                                         m_modes_private.DEC_APPLICATION_CURSOR_KEYS(),
                                         m_modes_private.DEC_APPLICATION_KEYPAD(),
@@ -5130,7 +5271,7 @@ Terminal::line_is_wrappable(vte::grid::row_t row) const
  * In block mode, similarly to char mode, we care about vertical character boundary. (This is somewhat
  * debatable, as results in asymmetrical behavior along the two axes: a rectangle can disappear by
  * becoming zero wide, but not zero high.) We cannot take care of CJKs at the endpoints now because CJKs
- * can cross the boundary in any included row. Taking care of them needs to go to cell_is_selected().
+ * can cross the boundary in any included row. Taking care of them needs to go to cell_is_selected_vis().
  * We don't care about used vs. unused cells either. The event coordinate is simply rounded to the
  * nearest vertical cell boundary.
  */
@@ -5414,6 +5555,8 @@ Terminal::modify_selection (vte::view::coords const& pos)
 {
         g_assert (m_selecting);
 
+        ringview_maybe_update();
+
         auto current = selection_grid_halfcoords_from_view_coords (pos);
 
         if (current == m_selection_last)
@@ -5427,21 +5570,29 @@ Terminal::modify_selection (vte::view::coords const& pos)
         resolve_selection();
 }
 
-/* Check if a cell is selected or not. */
+/* Check if a cell is selected or not. BiDi: the coordinate is visual. */
 bool
-Terminal::cell_is_selected(vte::grid::column_t col,
-                                     vte::grid::row_t row) const
+Terminal::cell_is_selected_vis(vte::grid::column_t col,
+                               vte::grid::row_t row) const
 {
         if (m_selection_block_mode) {
                 /* In block mode, make sure CJKs and TABs aren't cut in half. */
+                /* BiDi: convert to logical column... */
+                vte::base::BidiRow const* bidirow = m_ringview.get_row_map(row);
+                col = bidirow->vis2log(col);
                 while (col > 0) {
                         VteCell const* cell = find_charcell(col, row);
                         if (!cell || !cell->attr.fragment())
                                 break;
                         col--;
                 }
+                /* ... and back to visual. */
+                col = bidirow->log2vis(col);
                 return m_selection_resolved.box_contains ({ row, col });
         } else {
+                /* BiDi: convert to logical column. */
+                vte::base::BidiRow const* bidirow = m_ringview.get_row_map(row);
+                col = bidirow->vis2log(col);
                 /* In normal modes, resolve_selection() made sure to generate such boundaries for 
m_selection_resolved. */
                 return m_selection_resolved.contains ({ row, col });
         }
@@ -5902,6 +6053,10 @@ Terminal::match_hilite_update()
         glong col = pos.x / m_cell_width;
         glong row = pixel_to_row(pos.y);
 
+        /* BiDi: convert to logical column. */
+        vte::base::BidiRow const* bidirow = m_ringview.get_row_map(confine_grid_row(row));
+        col = bidirow->vis2log(col);
+
        _vte_debug_print(VTE_DEBUG_EVENTS,
                          "Match hilite update (%ld, %ld) -> %ld, %ld\n",
                          pos.x, pos.y, col, row);
@@ -6106,6 +6261,9 @@ Terminal::get_text(vte::grid::row_t start_row,
        GString *string;
        struct _VteCharAttributes attr;
        vte::color::rgb fore, back;
+        vte::base::RingView *ringview = nullptr;
+        vte::base::BidiRow const *bidirow = nullptr;
+        vte::grid::column_t col_vis;
 
        if (attributes)
                g_array_set_size (attributes, 0);
@@ -6116,26 +6274,50 @@ Terminal::get_text(vte::grid::row_t start_row,
         if (start_col < 0)
                 start_col = 0;
 
-        vte::grid::column_t next_first_column = block ? start_col : 0;
-        vte::grid::column_t col = start_col;
+        if (block) {
+                /* Rectangular selection operates on the visual contents, not the logical.
+                 * m_ringview corresponds to the currently onscreen bits, therefore does not
+                 * necessarily include the entire selection.
+                 * Modifying m_ringview and then reverting would be a bit cumbersome,
+                 * creating a new one for the selection is simpler. */
+                ringview = new vte::base::RingView();
+                ringview->set_ring(m_screen->row_data);
+                ringview->set_rows(start_row, end_row - start_row + 1);
+                ringview->set_width(m_column_count);
+                ringview->maybe_update();
+        }
+
+        vte::grid::column_t col = block ? 0 : start_col;
         vte::grid::row_t row;
-       for (row = start_row; row < end_row + 1; row++, col = next_first_column) {
+        for (row = start_row; row < end_row + 1; row++, col = 0) {
                VteRowData const* row_data = find_row_data(row);
                 gsize last_empty, last_nonempty;
                 vte::grid::column_t last_emptycol, last_nonemptycol;
-                vte::grid::column_t line_last_column = (block || row == end_row) ? end_col : G_MAXLONG;
+                vte::grid::column_t line_last_column = (!block && row == end_row) ? end_col : m_column_count;
 
                 last_empty = last_nonempty = string->len;
                 last_emptycol = last_nonemptycol = -1;
 
                attr.row = row;
-               attr.column = col;
+               attr.column = col;  // FIXME is attr.column supposed to contain logical or visual? What is it 
used for?
                pcell = NULL;
                if (row_data != NULL) {
+                        bidirow = ringview ? ringview->get_row_map(row) : nullptr;
                         while (col < line_last_column &&
                                (pcell = _vte_row_data_get (row_data, col))) {
 
-                               attr.column = col;
+                                /* In block mode, we scan each row from its very beginning to its very end 
in logical order,
+                                 * and here filter out the characters that are visually outside of the 
block. */
+                                if (bidirow) {
+                                        col_vis = bidirow->log2vis(col);
+                                        // FIXME handle CJK and friends consistently with cell_is_selected().
+                                        if (col_vis < start_col || col_vis >= end_col) {
+                                                col++;
+                                                continue;
+                                        }
+                                }
+
+                               attr.column = col;  // FIXME ditto
 
                                /* If it's not part of a multi-column character,
                                 * and passes the selection criterion, add it to
@@ -6234,6 +6416,8 @@ Terminal::get_text(vte::grid::row_t start_row,
                }
        }
 
+        delete ringview;
+
        /* Sanity check. */
         if (attributes != nullptr)
                 g_assert_cmpuint(string->len, ==, attributes->len);
@@ -6807,6 +6991,8 @@ Terminal::widget_motion_notify(GdkEventMotion *event)
 {
        bool handled = false;
 
+        ringview_maybe_update();
+
         GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
         auto pos = view_coords_from_event(base_event);
         auto rowcol = grid_coords_from_view_coords(pos);
@@ -6815,6 +7001,8 @@ Terminal::widget_motion_notify(GdkEventMotion *event)
                          "Motion notify %s %s\n",
                          pos.to_string(), rowcol.to_string());
 
+        ringview_maybe_update();
+
        read_modifiers(base_event);
 
        switch (event->type) {
@@ -6872,6 +7060,8 @@ Terminal::widget_button_press(GdkEventButton *event)
        bool handled = false;
        gboolean start_selecting = FALSE, extend_selecting = FALSE;
 
+        ringview_maybe_update();
+
         GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
         auto pos = view_coords_from_event(base_event);
         auto rowcol = grid_coords_from_view_coords(pos);
@@ -7021,6 +7211,8 @@ Terminal::widget_button_release(GdkEventButton *event)
 {
        bool handled = false;
 
+        ringview_maybe_update();
+
         GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
         auto pos = view_coords_from_event(base_event);
         auto rowcol = grid_coords_from_view_coords(pos);
@@ -7138,6 +7330,8 @@ Terminal::widget_focus_out(GdkEventFocus *event)
 void
 Terminal::widget_enter(GdkEventCrossing *event)
 {
+        ringview_maybe_update();
+
         GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
         auto pos = view_coords_from_event(base_event);
 
@@ -7155,6 +7349,8 @@ Terminal::widget_enter(GdkEventCrossing *event)
 void
 Terminal::widget_leave(GdkEventCrossing *event)
 {
+        ringview_maybe_update();
+
         GdkEvent* base_event = reinterpret_cast<GdkEvent*>(event);
         auto pos = view_coords_from_event(base_event);
 
@@ -8827,6 +9023,27 @@ Terminal::draw_cells_with_attributes(struct _vte_draw_text_request *items,
 }
 
 
+void
+Terminal::ringview_maybe_update()
+{
+        m_ringview.set_ring (m_screen->row_data);
+        /* Due to possibly unaligned height and per-pixel scrolling, up to 2 more lines than the
+         * logical height can be partially visible.
+         * If a row is just underneath the current viewport, we still need to run BiDi on it
+         * because the top an outlined rectangle cursor shape peeks in into the viewport, and we need
+         * to know where the BiDi algorithm maps that cursor. However, +2 is still enough for this
+         * as long as the outline cursor is 1px thin. If we ever make it wider, we'll need +3.
+         */
+        m_ringview.set_rows ((long) m_screen->scroll_delta, m_row_count + 2);
+        m_ringview.set_width (m_column_count);
+        m_ringview.maybe_update ();
+}
+
+/* XXX tmp hack */
+#define _vte_row_data_get_visual(row_data_p, bidimap, col) \
+        row_data_p == nullptr ? nullptr : _vte_row_data_get(row_data_p, bidimap->vis2log(col))
+
+
 /* Paint the contents of a given row at the given location.  Take advantage
  * of multiple-draw APIs by finding runs of characters with identical
  * attributes and bundling them together. */
@@ -8846,10 +9063,12 @@ Terminal::draw_rows(VteScreen *screen_,
         guint fore = VTE_DEFAULT_FG, nfore, back = VTE_DEFAULT_BG, nback, deco = VTE_DEFAULT_FG, ndeco;
         gboolean hyperlink = FALSE, nhyperlink, hilite = FALSE, nhilite;
         gboolean selected;
+        gboolean nrtl = FALSE, rtl;  /* for debugging */
         uint32_t attr = 0, nattr;
        guint item_count;
        const VteCell *cell;
        VteRowData const* row_data;
+        vte::base::BidiRow const* bidirow;
 
         auto const column_count = m_column_count;
         uint32_t const attr_mask = m_allow_bold ? ~0 : ~VTE_ATTR_BOLD_MASK;
@@ -8877,25 +9096,28 @@ Terminal::draw_rows(VteScreen *screen_,
                 if (row_data == nullptr)
                         continue; /* Skip row. FIXME: just paint this row empty? */
 
+                bidirow = m_ringview.get_row_map(row);
                 i = j = 0;
                 /* Walk the line.
                  * Locate runs of identical bg colors within a row, and paint each run as a single 
rectangle. */
                 do {
                         /* Get the first cell's contents. */
-                        cell = row_data ? _vte_row_data_get (row_data, i) : nullptr;
+                        cell = row_data ? _vte_row_data_get_visual (row_data, bidirow, i) : nullptr;
                         /* Find the colors for this cell. */
-                        selected = cell_is_selected(i, row);
+                        selected = cell_is_selected_vis(i, row);
                         determine_colors(cell, selected, &fore, &back, &deco);
+                        rtl = bidirow->vis_is_rtl(i);
 
                         while (++j < column_count) {
                                 /* Retrieve the next cell. */
-                                cell = row_data ? _vte_row_data_get (row_data, j) : nullptr;
+                                cell = row_data ? _vte_row_data_get_visual (row_data, bidirow, j) : nullptr;
                                 /* Resolve attributes to colors where possible and
                                  * compare visual attributes to the first character
                                  * in this chunk. */
-                                selected = cell_is_selected(j, row);
+                                selected = cell_is_selected_vis(j, row);
                                 determine_colors(cell, selected, &nfore, &nback, &ndeco);
-                                if (nback != back) {
+                                nrtl = bidirow->vis_is_rtl(j);
+                                if (nback != back || (_vte_debug_on (VTE_DEBUG_BIDI) && nrtl != rtl)) {
                                         break;
                                 }
                         }
@@ -8909,6 +9131,21 @@ Terminal::draw_rows(VteScreen *screen_,
                                                           row_height,
                                                           &bg, VTE_DRAW_OPAQUE);
                         }
+                        if (G_UNLIKELY (_vte_debug_on (VTE_DEBUG_BIDI) && rtl)) {
+                                /* Debug: Highlight RTL letters with a slightly different background. */
+                                vte::color::rgb bg;
+                                rgb_from_index<8, 8, 8>(back, bg);
+                                bg.red   = 0xC000 + (bg.red   - 0xC000) / 2;
+                                bg.green = 0xC000 + (bg.green - 0xC000) / 2;
+                                bg.blue  = 0xC000 + (bg.blue  - 0xC000) / 2;
+                                _vte_draw_fill_rectangle (
+                                                m_draw,
+                                                i * column_width,
+                                                y + row_height / 8,
+                                                (j - i) * column_width,
+                                                row_height * 3 / 4,
+                                                &bg, VTE_DRAW_OPAQUE);
+                        }
                         /* We'll need to continue at the first cell which didn't
                          * match the first one in this set. */
                         i = j;
@@ -8939,15 +9176,18 @@ Terminal::draw_rows(VteScreen *screen_,
                 /* Ensure that drawing is restricted to the cell (plus the overdraw area) */
                 _vte_draw_autoclip_t clipper{m_draw, &rect};
 
+                bidirow = m_ringview.get_row_map(row);
+
                 /* Walk the line.
                  * Locate runs of identical attributes within a row, and draw each run using a single 
draw_cells() call. */
                 item_count = 0;
                 for (col = 0; col < column_count; ) {
                         /* Get the character cell's contents. */
-                        cell = _vte_row_data_get (row_data, col);
+                        cell = _vte_row_data_get_visual (row_data, bidirow, col);
                         if (cell == NULL) {
-                                /* There'll be no more real cells in this row. */
-                                break;
+                                /* We're rendering BiDi text in visual order, so an unused cell can be 
followed by a used one. */
+                                col++;
+                                continue;
                         }
 
                         nhyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
@@ -8966,11 +9206,11 @@ Terminal::draw_rows(VteScreen *screen_,
 
                         /* Find the colors for this cell. */
                         nattr = cell->attr.attr;
-                        selected = cell_is_selected(col, row);
+                        selected = cell_is_selected_vis(col, row);
                         determine_colors(cell, selected, &nfore, &nback, &ndeco);
 
                         nhilite = (nhyperlink && cell->attr.hyperlink_idx == m_hyperlink_hover_idx) ||
-                                  (!nhyperlink && m_match != nullptr && m_match_span.contains(row, col));
+                                  (!nhyperlink && m_match != nullptr && m_match_span.contains(row, 
bidirow->vis2log(col)));
 
                         /* See if it no longer fits the run. */
                         if (item_count > 0 &&
@@ -9025,10 +9265,12 @@ Terminal::draw_rows(VteScreen *screen_,
                         hilite = nhilite;
 
                         g_assert_cmpint (item_count, <, column_count);
-                        items[item_count].c = c;
+                        items[item_count].c = bidirow->vis_get_shaped_char(col, c);
                         items[item_count].columns = j - col;
-                        items[item_count].x = col * column_width;
+                        items[item_count].x = (col - (bidirow->vis_is_rtl(col) ? j - col - 1 : 0)) * 
column_width;
                         items[item_count].y = y;
+                        items[item_count].mirror = bidirow->vis_is_rtl(col);
+                        items[item_count].box_mirror = !!(row_data->attr.bidi_flags & VTE_BIDI_BOX_MIRROR);
                         item_count++;
 
                         g_assert_cmpint (j, >, col);
@@ -9051,7 +9293,7 @@ Terminal::paint_cursor()
 {
        struct _vte_draw_text_request item;
         vte::grid::row_t drow;
-        vte::grid::column_t col;
+        vte::grid::column_t col, viscol;
         int width, height, cursor_width;
         guint style = 0;
         guint fore, back, deco;
@@ -9084,6 +9326,9 @@ Terminal::paint_cursor()
 
         /* Find the first cell of the character "under" the cursor.
          * This is for CJK.  For TAB, paint the cursor where it really is. */
+        VteRowData const *row_data = find_row_data(drow);
+        vte::base::BidiRow const *bidirow = m_ringview.get_row_map(drow);
+
        auto cell = find_charcell(col, drow);
         while (cell != NULL && cell->attr.fragment() && cell->c != '\t' && col > 0) {
                col--;
@@ -9091,15 +9336,18 @@ Terminal::paint_cursor()
        }
 
        /* Draw the cursor. */
-       item.c = (cell && cell->c) ? cell->c : ' ';
+        viscol = bidirow->log2vis(col);
+       item.c = (cell && cell->c) ? bidirow->vis_get_shaped_char(viscol, cell->c) : ' ';
        item.columns = item.c == '\t' ? 1 : cell ? cell->attr.columns() : 1;
-       item.x = col * width;
+        item.x = (viscol - ((cell && bidirow->vis_is_rtl(viscol)) ? cell->attr.columns() - 1 : 0)) * width;
        item.y = row_to_pixel(drow);
+        item.mirror = bidirow->vis_is_rtl(viscol);
+        item.box_mirror = (row_data && (row_data->attr.bidi_flags & VTE_BIDI_BOX_MIRROR));
        if (cell && cell->c != 0) {
                style = _vte_draw_get_style(cell->attr.bold(), cell->attr.italic());
        }
 
-       selected = cell_is_selected(col, drow);
+       selected = cell_is_selected_vis(viscol, drow);
         determine_cursor_colors(cell, selected, &fore, &back, &deco);
         rgb_from_index<8, 8, 8>(back, bg);
 
@@ -9121,9 +9369,23 @@ Terminal::paint_cursor()
                         stem_width = (int) (((float) (m_char_ascent + m_char_descent)) * 
m_cursor_aspect_ratio + 0.5);
                         stem_width = CLAMP (stem_width, VTE_LINE_WIDTH, m_cell_width);
 
+                        // FIXME make an exception when the cursor is just after the last character:
+                        // show it next to the last character (wherever it appears due to BiDi) rather than
+                        // beyond the end of the string, as KDE/Qt does. Maybe not just for I-beam.
+
+                        if (row_data && bidirow->vis_is_rtl(viscol))
+                                x += item.columns * m_cell_width - stem_width;
+
                         _vte_draw_fill_rectangle(m_draw,
                                                  x, y + m_char_padding.top, stem_width, m_char_ascent + 
m_char_descent,
                                                  &bg, VTE_DRAW_OPAQUE);
+
+                        if (focus && row_data && bidirow->has_foreign())
+                                _vte_draw_fill_rectangle(m_draw,
+                                                         bidirow->vis_is_rtl(viscol) ? x - stem_width : x + 
stem_width, y + m_char_padding.top,
+                                                         stem_width, stem_width,
+                                                         &bg, VTE_DRAW_OPAQUE);
+
                        break;
                 }
 
@@ -9207,12 +9469,19 @@ void
 Terminal::paint_im_preedit_string()
 {
        int col, columns;
+       long row;
        long width, height;
        int i, len;
 
        if (m_im_preedit.empty())
                return;
 
+        /* Get the row's BiDi information. */
+        row = m_screen->cursor.row;
+        if (row < first_displayed_row() || row > last_displayed_row())
+                return;
+        vte::base::BidiRow const *bidirow = m_ringview.get_row_map(row);
+
        /* Keep local copies of rendering information. */
        width = m_cell_width;
        height = m_cell_height;
@@ -9223,7 +9492,7 @@ Terminal::paint_im_preedit_string()
 
        /* If the pre-edit string won't fit on the screen if we start
         * drawing it at the cursor's position, move it left. */
-        col = m_screen->cursor.col;
+        col = bidirow->log2vis(m_screen->cursor.col);
        if (col + columns > m_column_count) {
                col = MAX(0, m_column_count - columns);
        }
@@ -9234,7 +9503,7 @@ Terminal::paint_im_preedit_string()
                const char *preedit = m_im_preedit.c_str();
                int preedit_cursor;
 
-               items = g_new(struct _vte_draw_text_request, len);
+               items = g_new0(struct _vte_draw_text_request, len);
                for (i = columns = 0; i < len; i++) {
                        items[i].c = g_utf8_get_char(preedit);
                         items[i].columns = _vte_unichar_width(items[i].c,
@@ -9301,6 +9570,8 @@ Terminal::widget_draw(cairo_t *cr)
         if (region == NULL)
                 return;
 
+        ringview_maybe_update();
+
         allocated_width = get_allocated_width();
         allocated_height = get_allocated_height();
 
@@ -9433,7 +9704,8 @@ Terminal::widget_scroll(GdkEventScroll *event)
        int button;
 
         GdkEvent *base_event = reinterpret_cast<GdkEvent*>(event);
-        auto rowcol = confined_grid_coords_from_event(base_event);
+
+        ringview_maybe_update();
 
        read_modifiers(base_event);
 
@@ -9468,6 +9740,8 @@ Terminal::widget_scroll(GdkEventScroll *event)
                                "Scroll application by %d lines, smooth scroll delta set back to %f\n",
                                cnt, m_mouse_smooth_scroll_delta);
 
+                auto rowcol = confined_grid_coords_from_event(base_event);
+
                button = cnt > 0 ? 5 : 4;
                if (cnt < 0)
                        cnt = -cnt;
@@ -9946,6 +10220,8 @@ Terminal::reset(bool clear_tabstops,
         /* Reset the saved cursor. */
         save_cursor(&m_normal_screen);
         save_cursor(&m_alternate_screen);
+        /* BiDi */
+        m_bidi_rtl = FALSE;
        /* Cause everything to be redrawn (or cleared). */
        invalidate_all();
 
@@ -10310,8 +10586,13 @@ Terminal::process(bool emit_adj_changed)
                         process_incoming();
                 }
                 m_input_bytes = 0;
-        } else
+        } else {
+                // FIXMEegmont why is it needed here, and is this the best place?
+                // Without this, sudden two-finger kinetic scrolls result in crash.
+                ringview_maybe_update();
+
                 emit_pending_signals();
+        }
 
         return is_active;
 }
diff --git a/src/vte/vteterminal.h b/src/vte/vteterminal.h
index 26ac236e..4ca6efb4 100644
--- a/src/vte/vteterminal.h
+++ b/src/vte/vteterminal.h
@@ -113,7 +113,7 @@ struct _VteTerminalClass {
 /* The structure we return as the supplemental attributes for strings. */
 struct _VteCharAttributes {
         /*< private >*/
-       long row, column;
+       long row, column;  // FIXME clarify if column is logical or visual
        PangoColor fore, back;
        guint underline:1, strikethrough:1, columns:4;
 };
diff --git a/src/vtedefines.hh b/src/vtedefines.hh
index 71896a76..3502fec1 100644
--- a/src/vtedefines.hh
+++ b/src/vtedefines.hh
@@ -136,3 +136,6 @@
 
 /* Max depth of title stack */
 #define VTE_WINDOW_TITLE_STACK_MAX_DEPTH (8)
+
+/* Maximum length of a paragraph, in lines, that might get proper BiDi treatment. */
+#define VTE_BIDI_PARAGRAPH_LENGTH_MAX   500
diff --git a/src/vtedraw.cc b/src/vtedraw.cc
index 215e27ee..aafb012b 100644
--- a/src/vtedraw.cc
+++ b/src/vtedraw.cc
@@ -26,6 +26,7 @@
 #include <glib.h>
 #include <gtk/gtk.h>
 
+#include "bidi.hh"
 #include "vtedraw.hh"
 #include "vtedefines.hh"
 #include "debug.h"
@@ -1487,6 +1488,12 @@ _vte_draw_text_internal (struct _vte_draw *draw,
 
        for (i = 0; i < n_requests; i++) {
                vteunistr c = requests[i].c;
+
+                if (G_UNLIKELY (requests[i].mirror)) {
+                        // FIXME what if 'c' is actually a real vteunistr?
+                        vte_bidi_get_mirror_char (c, requests[i].box_mirror, &c);
+                }
+
                struct unistr_info *uinfo = font_info_get_unistr_info (font, c);
                union unistr_font_info *ufi = &uinfo->ufi;
                 int x, y;
diff --git a/src/vtedraw.hh b/src/vtedraw.hh
index 7352d376..7006b77d 100644
--- a/src/vtedraw.hh
+++ b/src/vtedraw.hh
@@ -44,6 +44,8 @@ struct _vte_draw;
 struct _vte_draw_text_request {
        vteunistr c;
        gshort x, y, columns;
+       guint8 mirror : 1;
+       guint8 box_mirror : 1;
 };
 
 guint _vte_draw_get_style(gboolean bold, gboolean italic);
diff --git a/src/vtegtk.cc b/src/vtegtk.cc
index f8f296b9..262691bd 100644
--- a/src/vtegtk.cc
+++ b/src/vtegtk.cc
@@ -1791,6 +1791,12 @@ vte_get_features (void)
                 "+GNUTLS"
 #else
                 "-GNUTLS"
+#endif
+                " "
+#ifdef WITH_FRIBIDI
+                "+BIDI"
+#else
+                "-BIDI"
 #endif
                 ;
 }
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index 7d4815e0..8193610b 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -24,6 +24,7 @@
 #include "vtetypes.hh"
 #include "vtedraw.hh"
 #include "reaper.hh"
+#include "bidi.hh"
 #include "ring.hh"
 #include "buffer.h"
 #include "parser.hh"
@@ -86,6 +87,14 @@ enum {
         VTE_SGR_COLOR_SPEC_LEGACY = 5
 };
 
+enum {
+        VTE_BIDI_IMPLICIT   = 1 << 0,
+        VTE_BIDI_RTL        = 1 << 1,
+        VTE_BIDI_AUTO       = 1 << 2,
+        VTE_BIDI_BOX_MIRROR = 1 << 3,
+        VTE_BIDI_ALL        = (1 << 4) - 1,
+};
+
 struct vte_regex_and_flags {
         VteRegex *regex;
         guint32 match_flags;
@@ -392,7 +401,7 @@ public:
         gboolean m_selecting_had_delta;
         gboolean m_selection_block_mode;  // FIXMEegmont move it into a 4th value in vte_selection_type?
         enum vte_selection_type m_selection_type;
-        vte::grid::halfcoords m_selection_origin, m_selection_last;
+        vte::grid::halfcoords m_selection_origin, m_selection_last;  /* BiDi: logical in normal modes, 
visual in m_selection_block_mode */
         vte::grid::span m_selection_resolved;
 
        /* Clipboard data information. */
@@ -608,6 +617,10 @@ public:
         const char *m_hyperlink_hover_uri; /* data is owned by the ring */
         long m_hyperlink_auto_id;
 
+        /* BiDi parameters outside of ECMA and DEC private modes */
+        guint m_bidi_rtl  : 1;
+        vte::base::RingView m_ringview;
+
 public:
 
         // FIXMEchpe inline!
@@ -660,6 +673,10 @@ public:
         void invalidate_match_span();
         void invalidate_all();
 
+        guint8 get_bidi_flags();
+        void apply_bidi_attributes(vte::grid::row_t row, guint8 bidi_flags, guint8 bidi_flags_mask);
+        void maybe_apply_bidi_attributes(guint8 bidi_flags_mask);
+
         void reset_update_rects();
         bool invalidate_dirty_rects_and_process_updates();
         void time_process_incoming();
@@ -713,6 +730,7 @@ public:
 
         inline bool grid_coords_in_scrollback(vte::grid::coords const& rowcol) const { return rowcol.row() < 
m_screen->insert_delta; }
 
+        vte::grid::row_t confine_grid_row(vte::grid::row_t const& row) const;
         vte::grid::coords confine_grid_coords(vte::grid::coords const& rowcol) const;
         vte::grid::coords confined_grid_coords_from_event(GdkEvent const* event) const;
         vte::grid::coords confined_grid_coords_from_view_coords(vte::view::coords const& pos) const;
@@ -936,8 +954,8 @@ public:
         void resolve_selection();
         void selection_maybe_swap_endpoints(vte::view::coords const& pos);
         void modify_selection(vte::view::coords const& pos);
-        bool cell_is_selected(vte::grid::column_t col,
-                              vte::grid::row_t) const;
+        bool cell_is_selected_vis(vte::grid::column_t col,
+                                  vte::grid::row_t) const;
 
         void reset_default_attributes(bool reset_hyperlink);
 
@@ -1313,6 +1331,8 @@ public:
                                    vte::parser::StringTokeniser::const_iterator& token,
                                    vte::parser::StringTokeniser::const_iterator const& endtoken) noexcept;
 
+        void ringview_maybe_update();
+
         /* Sequence handlers */
         bool m_line_wrapped; // signals line wrapped from character insertion
         // Note: inlining the handlers seems to worsen the performance, so we don't do that
diff --git a/src/vterowdata.cc b/src/vterowdata.cc
index 88edba1a..cef1858f 100644
--- a/src/vterowdata.cc
+++ b/src/vterowdata.cc
@@ -82,7 +82,10 @@ _vte_cells_free (VteCells *cells)
 void
 _vte_row_data_init (VteRowData *row)
 {
+       // FIXME pass the bidi attrs to this method?
+       guint8 bidi_flags_save = row->attr.bidi_flags;
        memset (row, 0, sizeof (*row));
+       row->attr.bidi_flags = bidi_flags_save;
 }
 
 void
diff --git a/src/vterowdata.hh b/src/vterowdata.hh
index 9d4b2767..12cbf311 100644
--- a/src/vterowdata.hh
+++ b/src/vterowdata.hh
@@ -36,7 +36,8 @@ G_BEGIN_DECLS
  */
 
 typedef struct _VteRowAttr {
-       guint8 soft_wrapped: 1;
+        guint8 soft_wrapped  : 1;
+        guint8 bidi_flags    : 4;
 } VteRowAttr;
 static_assert(sizeof (VteRowAttr) == 1, "VteRowAttr has wrong size");
 
diff --git a/src/vteseq.cc b/src/vteseq.cc
index d12ee329..59387676 100644
--- a/src/vteseq.cc
+++ b/src/vteseq.cc
@@ -315,6 +315,7 @@ Terminal::clear_current_line()
                /* Add enough cells to the end of the line to fill out the row. */
                 _vte_row_data_fill (rowdata, &m_fill_defaults, m_column_count);
                 set_hard_wrapped(m_screen->cursor.row);
+                rowdata->attr.bidi_flags = get_bidi_flags();
                /* Repaint this row. */
                 invalidate_row(m_screen->cursor.row);
        }
@@ -342,6 +343,7 @@ Terminal::clear_above_current()
                        /* Add new cells until we fill the row. */
                         _vte_row_data_fill (rowdata, &m_fill_defaults, m_column_count);
                         set_hard_wrapped(i);
+                        rowdata->attr.bidi_flags = get_bidi_flags();
                        /* Repaint the row. */
                         invalidate_row(i);
                }
@@ -469,6 +471,13 @@ Terminal::set_mode_ecma(vte::parser::Sequence const& seq,
                         continue;
 
                 m_modes_ecma.set(mode, set);
+
+                if (mode == m_modes_ecma.eBDSM) {
+                        _vte_debug_print(VTE_DEBUG_BIDI,
+                                         "BiDi %s mode\n",
+                                         set ? "implicit" : "explicit");
+                        maybe_apply_bidi_attributes(VTE_BIDI_IMPLICIT);
+                }
         }
 }
 
@@ -597,6 +606,20 @@ Terminal::set_mode_private(int mode,
                         feed_focus_event_initial();
                 break;
 
+        case vte::terminal::modes::Private::eVTE_BOX_DRAWING_MIRROR:
+                _vte_debug_print(VTE_DEBUG_BIDI,
+                                 "BiDi box drawing mirroring %s\n",
+                                 set ? "enabled" : "disabled");
+                maybe_apply_bidi_attributes(VTE_BIDI_BOX_MIRROR);
+                break;
+
+        case vte::terminal::modes::Private::eVTE_BIDI_AUTO:
+                        _vte_debug_print(VTE_DEBUG_BIDI,
+                                         "BiDi dir autodetection %s\n",
+                                         set ? "enabled" : "disabled");
+                maybe_apply_bidi_attributes(VTE_BIDI_AUTO);
+                break;
+
         default:
                 break;
         }
@@ -745,6 +768,8 @@ Terminal::clear_below_current()
                         _vte_row_data_fill(rowdata, &m_fill_defaults, m_column_count);
                }
                 set_hard_wrapped(i);
+               if (i > m_screen->cursor.row)
+                        rowdata->attr.bidi_flags = get_bidi_flags();
                /* Repaint this row. */
                 invalidate_row(i);
        }
@@ -1023,6 +1048,7 @@ Terminal::line_feed()
 {
         ensure_cursor_is_onscreen();
         cursor_down(true);
+        maybe_apply_bidi_attributes(VTE_BIDI_ALL);
 }
 
 void
@@ -4746,7 +4772,7 @@ Terminal::DECSTBM(vte::parser::Sequence const& seq)
        } else {
                /* Maybe extend the ring -- bug 710483 */
                 while (_vte_ring_next(m_screen->row_data) < m_screen->insert_delta + m_row_count)
-                        _vte_ring_insert(m_screen->row_data, _vte_ring_next(m_screen->row_data));
+                        _vte_ring_insert(m_screen->row_data, _vte_ring_next(m_screen->row_data), 
get_bidi_flags());
        }
 
         home_cursor();
@@ -6918,7 +6944,27 @@ Terminal::SCP(vte::parser::Sequence const& seq)
          *   args[1]: no default
          *
          * References: ECMA-48 § 8.3.111
+         *             [FIXME link to our spec]
          */
+
+        auto const param = seq.collect1(0);
+        switch (param) {
+        case 1:
+                m_bidi_rtl = FALSE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: switch to LTR\n");
+                break;
+        case 2:
+                m_bidi_rtl = TRUE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: switch to RTL\n");
+                break;
+        default:
+                /* FIXME switch to the emulator's default */
+                m_bidi_rtl = FALSE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: default direction restored\n");
+                break;
+        }
+
+        maybe_apply_bidi_attributes(VTE_BIDI_RTL);
 }
 
 void
@@ -7386,7 +7432,30 @@ Terminal::SPD(vte::parser::Sequence const& seq)
          *   args[1]: 0
          *
          * References: ECMA-48 § 8.3.130
+         *             [FIXME link to our spec]
          */
+
+        auto const param = seq.collect1(0);
+        switch (param) {
+        case -1:
+        case 0:
+                m_bidi_rtl = FALSE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: switch to LTR\n");
+                break;
+        case 3:
+                m_bidi_rtl = TRUE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: switch to RTL\n");
+                break;
+        default:
+                /* FIXME switch to the emulator's default */
+                m_bidi_rtl = FALSE;
+                _vte_debug_print(VTE_DEBUG_BIDI, "BiDi: default direction restored\n");
+                break;
+        }
+
+        maybe_apply_bidi_attributes(VTE_BIDI_RTL);
+
+        // FIXME apply to all the onscreen lines?
 }
 
 void
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]