KazDragon / terminalpp

A C++ library for interacting with ANSI terminal windows.
MIT License
75 stars 10 forks source link

Can this library be used directly for string output rather than for screen output? #304

Closed d0vgan closed 11 months ago

d0vgan commented 1 year ago

Sorry for a newbie question, I'm trying to understand whether this library matches my use case. The use case is the following: 1) There is a running console process with its output redirected into a pipe. 2) Another GUI process is reading from that pipe, and the data from the pipe contains a lot of ANSI escape codes. 3) The goal of the GUI process that reads from the pipe is to get a final "refined" text as a string after all the ANSI escape codes have been applied. (I assume the terminal library can do some "conversion" where the ANSI escape codes are applied to the input text, producing the "refined" output text).

So I'm assuming the following flow:

[Console app] -------> [pipe] ----text+esc.codes----> [GUI app] ----text+esc.codes----> [terminal library] ----> refined text.

Is this possible with the library? And if so, what classes should I use?

d0vgan commented 1 year ago

Also, the main README.md says "Terminal++ is currently automatically tested using MSVC 2019". How can I build it using MSVC? I noticed that the "terminalpp/detail/export.hpp" seems to be an autogenerated file, but how can I produce one using MSVC?

KazDragon commented 1 year ago

Hi d0vgan, and thank you for your questions.

For the question as to whether this can be used for string output, the simple answer is yes: the library itself has no idea what its output target is (e.g. network socket or command console) and stops at the string interface.

If I understand your use case, you want to read in some plain text, do some processing on that and then output that as e.g. coloured text. This is definitely possible. Read as a plain string, convert to a terminalpp::string and fill in the attributes as necessary, then output that string via a terminal class. Have a look at the example projects for an idea on how to do that.

For your final question, I use cmake to generate all the compiler-specific files. Have a look at the Appveyor script to see how it's used in an msvc context. export.hpp is generated internally to handle building as a library vs building as a dll and not something you should need to worry about.

d0vgan commented 1 year ago

Oh, well, I was able to compile the "tprint" example by doing a few ugly hacks. At first, I created a file "terminalpp/include/terminalpp/detail/export.hpp" with the following content:

#pragma once

#define TERMINALPP_EXPORT

Second, I analyzed how the fmt library is used and decided that it's not worth to fight compiling it (the latest version uses C++ modules' features that do not seem to be fully supported even by VS2022). So I eneded up with this simple solution:

namespace fmt
{
    std::string format(const char* fmt, ...)
    {
        char buf[256];

        va_list argList;

        va_start(argList, fmt);
        vsprintf(buf, fmt, argList);
        va_end(argList);

        return std::string(buf);
    }
}

Surely, it's far from perfect - ideally I should use variadic templates. But I wanted some working result. Moreover, it allows to get rid of the whole fmt library!

Finally, I've successfully compiled the "tprint" example. I was expecting this code:

terminalpp::stdout_channel channel;
    terminalpp::terminal terminal{channel};
    auto s = terminalpp::encode("\x1b[K\x1b[13;1H\x1b[?25h\x1b[25l\x1b[?7h\x1b[31mOut[\x1b[1m\x1b[91m2\x1b[22m\x1b[31m]: \x1b[m10\x1b[15;1H\x1b[?25h\x1b[25l\x1b[?7l\x1b[?7h\x1b[32mIn [\x1b[1m\x1b[92m3\x1b[22m\x1b[32m]: \x1b[m\x1b[K");
    std::string str;
    str.reserve(s.size());
    for (const auto& elem: s)
    {
        str.append(1, elem.glyph_.character_);
    }

To produce something like "Out[]: " in the str (i.e. just the refined text) - but, instead, str contains all the codes that the original string did. What I am doing wrong?

d0vgan commented 1 year ago

By the way, this code

return terminalpp::token {
            terminalpp::virtual_key {
                terminalpp::vk::enter,
                terminalpp::vk_modifier::none,
                1,
                '\n'
            }
        };

required a small tuning:

return terminalpp::token {
            terminalpp::virtual_key {
                terminalpp::vk::enter,
                terminalpp::vk_modifier::none,
                1,
                terminalpp::byte('\n')
            }
        };

since '\n' is char whereas terminalpp::virtual_key expects std::variant<byte, control_sequence> and can't convert char to byte implicitly.

KazDragon commented 1 year ago

To produce something like "Out[]: " in the str (i.e. just the refined text) - but, instead, str contains all the codes that the original string did. What I am doing wrong?

Thanks for elaborating on your use case. On reflection, I don't believe that my library is a 100% fit with your needs right now, but it could possibly evolve in that direction. Let me explain.

First of all, terminalpp::encode (and the equivalent _ets UDL) are a DSL so that I don't have to remember ANSI codes. So terminalpp::encode("\[1RED") returns a terminalpp::string object with the text RED, all characters of which have their foreground colour attribute set to low-colour red. terminalpp::encode doesn't parse ANSI codes at all. See https://github.com/KazDragon/terminalpp/wiki/String-To-Elements-Protocol for more information on that.

ANSI parsing is usually performed via a terminalpp::terminal object (see the wait_for_mouse_click example). You could set up a channel object that is passed into the terminal's constructor that takes your input from the pipe when read from. I would copy stdout_channel as a basis for that and fill in the async_read() function to be something appropriate. As a hack before going into the channel concept, you could use terminalpp::detail::parser directly to convert your piped ANSI-encoded input into terminalpp::tokens, but there's no guarantee in the stability of this object between minor versions.

However, and this is where my earlier conclusion comes from, there is an assumption that the input is an interactive terminal, so it is set up to assume that it is reading arrow keys, mouse clicks, and individual characters. If you fed it your input, it would return a sequence of terminalpp::tokens that look like (abstractly): {token{control_sequence{erase_in_line}}, token{control_sequence{move_cursor{1,13}}, ...}. This is the point where your application would take over and decide what to do with these tokens. There's no means of converting that directly back into output right now. It would be an interesting idea to add another level of parsing to build up e.g. a terminalpp::screen object that mirrors the given input, which could then be output to a terminal, but that functionality doesn't currently exist.

Finally, thanks for noting the corrections required. I haven't updated compiler in a while; I should probably do so.

KazDragon commented 1 year ago

As for the fmt hack, you've probably broken cursor manipulation and colour codes with that since they all use the "{}" syntax instead of printf-like %s syntax. Just an FYI if you get to that stage.

d0vgan commented 1 year ago

As for the fmt hack, you've probably broken cursor manipulation and colour codes with that since they all use the "{}" syntax instead of printf-like %s syntax. Just an FYI if you get to that stage.

I used %d instead of {} since all the passed arguments have a type of int. Worth to mention to those who may follow this way :)