From 7b7177b59c9288fa68bb2f65973dc5c304f1f6d3 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <sonzogniarthur@gmail.com>
Date: Sun, 4 Jun 2023 21:06:19 +0200
Subject: [PATCH] Feature: `hyperlink` support. (#665)

See the [OSC 8 page](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
FTXUI support proposed by @aaleino in [#662](https://github.com/ArthurSonzogni/FTXUI/issues/662).

API:
```cpp
auto link = text("Click here") | hyperlink("https://github.com/FTXUI")
```

Fixed:https://github.com/ArthurSonzogni/FTXUI/issues/662
---
 CHANGELOG.md                     |  9 ++++
 CMakeLists.txt                   |  1 +
 README.md                        |  1 +
 cmake/ftxui_test.cmake           |  1 +
 examples/dom/CMakeLists.txt      |  1 +
 examples/dom/style_gallery.cpp   |  3 +-
 examples/dom/style_hyperlink.cpp | 25 +++++++++++
 include/ftxui/dom/elements.hpp   |  2 +
 include/ftxui/screen/screen.hpp  | 13 +++++-
 iwyu.imp                         | 26 ++++++++++++
 src/ftxui/component/input.cpp    |  2 +-
 src/ftxui/dom/hyperlink.cpp      | 72 ++++++++++++++++++++++++++++++++
 src/ftxui/dom/hyperlink_test.cpp | 43 +++++++++++++++++++
 src/ftxui/screen/screen.cpp      | 43 +++++++++++++++----
 14 files changed, 230 insertions(+), 12 deletions(-)
 create mode 100644 examples/dom/style_hyperlink.cpp
 create mode 100644 src/ftxui/dom/hyperlink.cpp
 create mode 100644 src/ftxui/dom/hyperlink_test.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5b366bd..427b8165 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,15 @@ current (development)
 - Feature: `input` is now supporting multiple lines.
 - Feature: `input` style is now customizeable.
 
+### Dom
+- Feature: Add `hyperlink` decorator. For instance:
+  ```cpp
+  auto link = text("Click here") | hyperlink("https://github.com/FTXUI")
+  ```
+  See the [OSC 8 page](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
+  FTXUI support proposed by @aaleino in [#662](https://github.com/ArthurSonzogni/FTXUI/issues/662).
+
+
 ### Build
 - Check version compatibility when using cmake find_package()
 - Add `FTXUI_DEV_WARNING` options to turn on warnings when building FTXUI
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c99700f8..ebed83ef 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -55,6 +55,7 @@ add_library(dom
   src/ftxui/dom/automerge.cpp
   src/ftxui/dom/blink.cpp
   src/ftxui/dom/bold.cpp
+  src/ftxui/dom/hyperlink.cpp
   src/ftxui/dom/border.cpp
   src/ftxui/dom/box_helper.cpp
   src/ftxui/dom/box_helper.hpp
diff --git a/README.md b/README.md
index 4abdb48f..763391d7 100644
--- a/README.md
+++ b/README.md
@@ -117,6 +117,7 @@ An element can be decorated using the functions:
   - `strikethrough`
   - `color`
   - `bgcolor`
+  - `hyperlink`
 
 [Example](https://arthursonzogni.github.io/FTXUI/examples_2dom_2style_gallery_8cpp-example.html)
 
diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake
index 6142d5f6..4c1b15a6 100644
--- a/cmake/ftxui_test.cmake
+++ b/cmake/ftxui_test.cmake
@@ -36,6 +36,7 @@ add_executable(ftxui-tests
   src/ftxui/dom/gauge_test.cpp
   src/ftxui/dom/gridbox_test.cpp
   src/ftxui/dom/hbox_test.cpp
+  src/ftxui/dom/hyperlink_test.cpp
   src/ftxui/dom/linear_gradient_test.cpp
   src/ftxui/dom/scroll_indicator_test.cpp
   src/ftxui/dom/separator_test.cpp
diff --git a/examples/dom/CMakeLists.txt b/examples/dom/CMakeLists.txt
index ad3285e9..911bf7fe 100644
--- a/examples/dom/CMakeLists.txt
+++ b/examples/dom/CMakeLists.txt
@@ -27,6 +27,7 @@ example(style_bold)
 example(style_color)
 example(style_dim)
 example(style_gallery)
+example(style_hyperlink)
 example(style_inverted)
 example(style_strikethrough)
 example(style_underlined)
diff --git a/examples/dom/style_gallery.cpp b/examples/dom/style_gallery.cpp
index c958c574..419f2945 100644
--- a/examples/dom/style_gallery.cpp
+++ b/examples/dom/style_gallery.cpp
@@ -19,7 +19,8 @@ int main() {
       text("blink")              | blink                , text(" ") ,
       text("strikethrough")      | strikethrough        , text(" ") ,
       text("color")              | color(Color::Blue)   , text(" ") ,
-      text("bgcolor")            | bgcolor(Color::Blue) ,
+      text("bgcolor")            | bgcolor(Color::Blue) , text(" ") ,
+      text("hyperlink")          | hyperlink("https://github.com/ArthurSonzogni/FTXUI"),
     });
   // clang-format on
   auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));
diff --git a/examples/dom/style_hyperlink.cpp b/examples/dom/style_hyperlink.cpp
new file mode 100644
index 00000000..8ab092c0
--- /dev/null
+++ b/examples/dom/style_hyperlink.cpp
@@ -0,0 +1,25 @@
+#include <ftxui/dom/elements.hpp>  // for text, operator|, bold, Fit, hbox, Element
+#include <ftxui/screen/screen.hpp>  // for Full, Screen
+#include <memory>                   // for allocator
+
+#include "ftxui/dom/node.hpp"      // for Render
+#include "ftxui/screen/color.hpp"  // for ftxui
+
+int main() {
+  using namespace ftxui;
+  auto document =  //
+      hbox({
+          text("This text is an "),
+          text("hyperlink") | hyperlink("https://www.google.com"),
+          text(". Do you like it?"),
+      });
+  auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));
+  Render(screen, document);
+  screen.Print();
+
+  return 0;
+}
+
+// Copyright 2020 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp
index b414c763..d343cdc1 100644
--- a/include/ftxui/dom/elements.hpp
+++ b/include/ftxui/dom/elements.hpp
@@ -109,6 +109,8 @@ Element bgcolor(const LinearGradient&, Element);
 Decorator focusPosition(int x, int y);
 Decorator focusPositionRelative(float x, float y);
 Element automerge(Element child);
+Decorator hyperlink(std::string link);
+Element hyperlink(std::string link, Element child);
 
 // --- Layout is
 // Horizontal, Vertical or stacked set of elements.
diff --git a/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp
index d8a5329d..0bb90a84 100644
--- a/include/ftxui/screen/screen.hpp
+++ b/include/ftxui/screen/screen.hpp
@@ -1,8 +1,9 @@
 #ifndef FTXUI_SCREEN_SCREEN_HPP
 #define FTXUI_SCREEN_SCREEN_HPP
 
+#include <cstdint>  // for uint8_t
 #include <memory>
-#include <string>  // for string, allocator, basic_string
+#include <string>  // for string, basic_string, allocator
 #include <vector>  // for vector
 
 #include "ftxui/screen/box.hpp"       // for Box
@@ -20,6 +21,10 @@ struct Pixel {
   // like: a⃦, this can potentially contains multiple codepoitns.
   std::string character = " ";
 
+  // The hyperlink associated with the pixel.
+  // 0 is the default value, meaning no hyperlink.
+  uint8_t hyperlink = 0;
+
   // Colors:
   Color background_color = Color::Default;
   Color foreground_color = Color::Default;
@@ -99,6 +104,11 @@ class Screen {
   Cursor cursor() const { return cursor_; }
   void SetCursor(Cursor cursor) { cursor_ = cursor; }
 
+  // Store an hyperlink in the screen. Return the id of the hyperlink. The id is
+  // used to identify the hyperlink when the user click on it.
+  uint8_t RegisterHyperlink(std::string link);
+  const std::string& Hyperlink(uint8_t id) const;
+
   Box stencil;
 
  protected:
@@ -106,6 +116,7 @@ class Screen {
   int dimy_;
   std::vector<std::vector<Pixel>> pixels_;
   Cursor cursor_;
+  std::vector<std::string> hyperlinks_ = {""};
 };
 
 }  // namespace ftxui
diff --git a/iwyu.imp b/iwyu.imp
index 83d8d422..4a146409 100644
--- a/iwyu.imp
+++ b/iwyu.imp
@@ -14,6 +14,32 @@
   { include: ["<gtest/gtest-printers.h>", "private", "<gtest/gtest.h>", "public" ] },
   { include: ["<gtest/gtest-test-part.h>", "private", "<gtest/gtest.h>", "public" ] },
   { include: ["<gtest/gtest-typed-test.h>", "private", "<gtest/gtest.h>", "public" ] },
+  { include: ["<assert.h>", "private", "<cassert>", "public" ] },
+  { include: ["<complex.h>", "private", "<ccomplex>", "public" ] },
+  { include: ["<ctype.h>", "private", "<cctype>", "public" ] },
+  { include: ["<errno.h>", "private", "<cerrno>", "public" ] },
+  { include: ["<fenv.h>", "private", "<cfenv>", "public" ] },
+  { include: ["<float.h>", "private", "<cfloat>", "public" ] },
+  { include: ["<inttypes.h>", "private", "<cinttypes>", "public" ] },
+  { include: ["<iso646.h>", "private", "<ciso646>", "public" ] },
+  { include: ["<limits.h>", "private", "<climits>", "public" ] },
+  { include: ["<locale.h>", "private", "<clocale>", "public" ] },
+  { include: ["<math.h>", "private", "<cmath>", "public" ] },
+  { include: ["<setjmp.h>", "private", "<csetjmp>", "public" ] },
+  { include: ["<signal.h>", "private", "<csignal>", "public" ] },
+  { include: ["<stdalign.h>", "private", "<cstdalign>", "public" ] },
+  { include: ["<stdarg.h>", "private", "<cstdarg>", "public" ] },
+  { include: ["<stdbool.h>", "private", "<cstdbool>", "public" ] },
+  { include: ["<stddef.h>", "private", "<cstddef>", "public" ] },
+  { include: ["<stdint.h>", "private", "<cstdint>", "public" ] },
+  { include: ["<stdio.h>", "private", "<cstdio>", "public" ] },
+  { include: ["<stdlib.h>", "private", "<cstdlib>", "public" ] },
+  { include: ["<string.h>", "private", "<cstring>", "public" ] },
+  { include: ["<tgmath.h>", "private", "<ctgmath>", "public" ] },
+  { include: ["<time.h>", "private", "<ctime>", "public" ] },
+  { include: ["<uchar.h>", "private", "<cuchar>", "public" ] },
+  { include: ["<wchar.h>", "private", "<cwchar>", "public" ] },
+  { include: ["<wctype.h>", "private", "<cwctype>", "public" ] },
   { symbol: ["ftxui", "private", "", "public" ] },
   { symbol: ["char_traits", "private", "<string>", "public" ] },
   { symbol: ["ECHO", "private", "<termios.h>", "public" ] },
diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp
index a854614e..c19b508d 100644
--- a/src/ftxui/component/input.cpp
+++ b/src/ftxui/component/input.cpp
@@ -1,6 +1,6 @@
-#include <stdint.h>    // for uint32_t
 #include <algorithm>   // for max, min
 #include <cstddef>     // for size_t
+#include <cstdint>     // for uint32_t
 #include <functional>  // for function
 #include <memory>   // for allocator, shared_ptr, allocator_traits<>::value_type
 #include <sstream>  // for basic_istream, stringstream
diff --git a/src/ftxui/dom/hyperlink.cpp b/src/ftxui/dom/hyperlink.cpp
new file mode 100644
index 00000000..6ce8a7ad
--- /dev/null
+++ b/src/ftxui/dom/hyperlink.cpp
@@ -0,0 +1,72 @@
+#include <cstdint>  // for uint8_t
+#include <memory>   // for make_shared
+#include <string>   // for string
+#include <utility>  // for move
+
+#include "ftxui/dom/elements.hpp"        // for Element, Decorator, hyperlink
+#include "ftxui/dom/node_decorator.hpp"  // for NodeDecorator
+#include "ftxui/screen/box.hpp"          // for Box
+#include "ftxui/screen/screen.hpp"       // for Screen, Pixel
+
+namespace ftxui {
+
+class Hyperlink : public NodeDecorator {
+ public:
+  Hyperlink(Element child, std::string link)
+      : NodeDecorator(std::move(child)), link_(link) {}
+
+  void Render(Screen& screen) override {
+    uint8_t hyperlink_id = screen.RegisterHyperlink(link_);
+    for (int y = box_.y_min; y <= box_.y_max; ++y) {
+      for (int x = box_.x_min; x <= box_.x_max; ++x) {
+        screen.PixelAt(x, y).hyperlink = hyperlink_id;
+      }
+    }
+    NodeDecorator::Render(screen);
+  }
+
+  std::string link_;
+};
+
+/// @brief Make the rendered area clickable using a web browser.
+///        The link will be opened when the user click on it.
+///        This is supported only on a limited set of terminal emulator.
+///        List: https://github.com/Alhadis/OSC8-Adoption/
+/// @param link The link
+/// @param child The input element.
+/// @return The output element with the link.
+/// @ingroup dom
+///
+/// ### Example
+///
+/// ```cpp
+/// Element document =
+///   hyperlink("https://github.com/ArthurSonzogni/FTXUI", "link");
+/// ```
+Element hyperlink(std::string link, Element child) {
+  return std::make_shared<Hyperlink>(std::move(child), link);
+}
+
+/// @brief Decorate using an hyperlink.
+///        The link will be opened when the user click on it.
+///        This is supported only on a limited set of terminal emulator.
+///        List: https://github.com/Alhadis/OSC8-Adoption/
+/// @param link The link to redirect the users to.
+/// @return The Decorator applying the hyperlink.
+/// @ingroup dom
+///
+/// ### Example
+///
+/// ```cpp
+/// Element document =
+///   text("red") | hyperlink("https://github.com/Arthursonzogni/FTXUI");
+/// ```
+Decorator hyperlink(std::string link) {
+  return [link](Element child) { return hyperlink(link, std::move(child)); };
+}
+
+}  // namespace ftxui
+
+// Copyright 2023 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
diff --git a/src/ftxui/dom/hyperlink_test.cpp b/src/ftxui/dom/hyperlink_test.cpp
new file mode 100644
index 00000000..55798314
--- /dev/null
+++ b/src/ftxui/dom/hyperlink_test.cpp
@@ -0,0 +1,43 @@
+#include <gtest/gtest.h>  // for Test, EXPECT_EQ, Message, TestPartResult, TestInfo (ptr only), TEST
+#include <string>  // for allocator, string
+
+#include "ftxui/dom/elements.hpp"  // for text, hyperlink, operator|, Element, hbox
+#include "ftxui/dom/node.hpp"      // for Render
+#include "ftxui/screen/screen.hpp"  // for Screen, Pixel
+
+namespace ftxui {
+
+TEST(HyperlinkTest, Basic) {
+  auto element = hbox({
+      text("text 1") | hyperlink("https://a.com"),
+      text("text 2") | hyperlink("https://b.com"),
+      text("text 3"),
+      text("text 4") | hyperlink("https://c.com"),
+  });
+
+  Screen screen(6 * 4, 1);
+  Render(screen, element);
+
+  EXPECT_EQ(screen.PixelAt(0, 0).hyperlink, 1u);
+  EXPECT_EQ(screen.PixelAt(5, 0).hyperlink, 1u);
+  EXPECT_EQ(screen.PixelAt(6, 0).hyperlink, 2u);
+  EXPECT_EQ(screen.PixelAt(11, 0).hyperlink, 2u);
+
+  std::string output = screen.ToString();
+  EXPECT_EQ(output,
+            "\x1B]8;;https://a.com\x1B\\"
+            "text 1"
+            "\x1B]8;;https://b.com\x1B\\"
+            "text 2"
+            "\x1B]8;;\x1B\\"
+            "text 3"
+            "\x1B]8;;https://c.com\x1B\\"
+            "text 4"
+            "\x1B]8;;\x1B\\");
+}
+
+}  // namespace ftxui
+
+// Copyright 2023 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp
index 04cc7584..0f2e3772 100644
--- a/src/ftxui/screen/screen.cpp
+++ b/src/ftxui/screen/screen.cpp
@@ -1,7 +1,7 @@
-#include <cstdint>  // for uint8_t
+#include <cstdint>  // for size_t
 #include <iostream>  // for operator<<, stringstream, basic_ostream, flush, cout, ostream
 #include <map>      // for _Rb_tree_const_iterator, map, operator!=, operator==
-#include <memory>   // for allocator
+#include <memory>   // for allocator, allocator_traits<>::value_type
 #include <sstream>  // IWYU pragma: keep
 #include <utility>  // for pair
 
@@ -50,11 +50,13 @@ void WindowsEmulateVT100Terminal() {
 #endif
 
 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
-void UpdatePixelStyle(std::stringstream& ss,
+void UpdatePixelStyle(const Screen* screen,
+                      std::stringstream& ss,
                       Pixel& previous,
                       const Pixel& next) {
-  if (next == previous) {
-    return;
+  // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+  if (next.hyperlink != previous.hyperlink) {
+    ss << "\x1B]8;;" << screen->Hyperlink(next.hyperlink) << "\x1B\\";
   }
 
   if ((!next.bold && previous.bold) ||  //
@@ -435,20 +437,20 @@ std::string Screen::ToString() {
 
   for (int y = 0; y < dimy_; ++y) {
     if (y != 0) {
-      UpdatePixelStyle(ss, previous_pixel, final_pixel);
+      UpdatePixelStyle(this, ss, previous_pixel, final_pixel);
       ss << "\r\n";
     }
     bool previous_fullwidth = false;
     for (const auto& pixel : pixels_[y]) {
       if (!previous_fullwidth) {
-        UpdatePixelStyle(ss, previous_pixel, pixel);
+        UpdatePixelStyle(this, ss, previous_pixel, pixel);
         ss << pixel.character;
       }
       previous_fullwidth = (string_width(pixel.character) == 2);
     }
   }
 
-  UpdatePixelStyle(ss, previous_pixel, final_pixel);
+  UpdatePixelStyle(this, ss, previous_pixel, final_pixel);
 
   return ss.str();
 }
@@ -517,6 +519,10 @@ void Screen::Clear() {
   }
   cursor_.x = dimx_ - 1;
   cursor_.y = dimy_ - 1;
+
+  hyperlinks_ = {
+      "",
+  };
 }
 
 // clang-format off
@@ -545,9 +551,28 @@ void Screen::ApplyShader() {
     }
   }
 }
-
 // clang-format on
 
+uint8_t Screen::RegisterHyperlink(std::string link) {
+  for (size_t i = 0; i < hyperlinks_.size(); ++i) {
+    if (hyperlinks_[i] == link) {
+      return i;
+    }
+  }
+  if (hyperlinks_.size() == 255) {
+    return 0;
+  }
+  hyperlinks_.push_back(link);
+  return hyperlinks_.size() - 1;
+}
+
+const std::string& Screen::Hyperlink(uint8_t id) const {
+  if (id >= hyperlinks_.size()) {
+    return hyperlinks_[0];
+  }
+  return hyperlinks_[id];
+}
+
 }  // namespace ftxui
 
 // Copyright 2020 Arthur Sonzogni. All rights reserved.
-- 
GitLab