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
This commit is contained in:
Arthur Sonzogni 2023-06-04 21:06:19 +02:00 committed by GitHub
parent d4c6cebea3
commit 7b7177b59c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 230 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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));

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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" ] },

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.