Skip to content

Commit

Permalink
Support colorized outputs on Linux (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
matyalatte authored Sep 11, 2024
1 parent caaaa15 commit 5bf7242
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 11 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
- Added support for C-style comments and trailing commas in JSON files.
- Added support for .jsonc files.
- Added "codepage" option to support UTF-8 outputs on Windows.
- Added support for colorized outputs on Linux.
- Fixed a bug that there is no limit on the buffer size of the console window on Linux.

ver 0.7.0
- Added options for string validation.
Expand Down
16 changes: 9 additions & 7 deletions src/exec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,18 @@ ExecuteResult Execute(const std::string& cmd, bool use_utf8_on_windows) {
do {
out_read_size = ReadIO(process, READ_STDOUT, out_buf, BUF_SIZE, last_line, BUF_SIZE);
err_read_size = ReadIO(process, READ_STDERR, err_buf, BUF_SIZE, err_msg, BUF_SIZE * 2);
if (out_read_size) {
#ifdef _WIN32
if (use_utf8_on_windows) {
std::wstring wout = UTF8toUTF16(out_buf);
printf("%ls", wout.c_str());
} else {
printf("%s", out_buf);
}
if (use_utf8_on_windows) {
std::wstring wout = UTF8toUTF16(out_buf);
printf("%ls", wout.c_str());
} else {
printf("%s", out_buf);
}
#else
PrintFmt("%s", out_buf);
PrintFmt("%s", out_buf);
#endif
}
} while (subprocess_alive(&process) || out_read_size || err_read_size);

// Sometimes stderr still have unread characters
Expand Down
305 changes: 302 additions & 3 deletions src/string_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,291 @@ void EnableCSI() {
#include <stdarg.h>
#include "ui.h"

// \e[XXm
typedef int ansi_escape_code;
enum ANSI_ESCAPE: ansi_escape_code {
ANSI_RESET = 0,
ANSI_BOLD = 1,
ANSI_ITALIC = 3,
ANSI_UNDERLINE = 4,
ANSI_STRIKETHROUGH = 9,
ANSI_FONT_BLACK = 30,
ANSI_FONT_RED = 31,
ANSI_FONT_GREEN = 32,
ANSI_FONT_YELLOW = 33,
ANSI_FONT_BLUE = 34,
ANSI_FONT_MAGENTA = 35,
ANSI_FONT_CYAN = 36,
ANSI_FONT_WHITE = 37,
ANSI_BG_BLACK = 40,
ANSI_BG_RED = 41,
ANSI_BG_GREEN = 42,
ANSI_BG_YELLOW = 43,
ANSI_BG_BLUE = 44,
ANSI_BG_MAGENTA = 45,
ANSI_BG_CYAN = 46,
ANSI_BG_WHITE = 47,
};

struct pango_tag {
const char* str;
int len;
};

constexpr int strlen_constexpr(const char* str, int count = 0) {
return (*str == '\0') ? count : strlen_constexpr(str + 1, count + 1);
}

constexpr pango_tag PangoTag(const char* str) {
return { str, strlen_constexpr(str) };
}

constexpr pango_tag FONT_TAGS[] = {
PangoTag("<span foreground='black'>"),
PangoTag("<span foreground='red'>"),
PangoTag("<span foreground='green'>"),
PangoTag("<span foreground='yellow'>"),
PangoTag("<span foreground='blue'>"),
PangoTag("<span foreground='magenta'>"),
PangoTag("<span foreground='cyan'>"),
PangoTag("<span foreground='white'>")
};

constexpr pango_tag BG_TAGS[] = {
PangoTag("<span background='black'>"),
PangoTag("<span background='red'>"),
PangoTag("<span background='green'>"),
PangoTag("<span background='yellow'>"),
PangoTag("<span background='blue'>"),
PangoTag("<span background='magenta'>"),
PangoTag("<span background='cyan'>"),
PangoTag("<span background='white'>")
};

// Get opening markup tag from ANSI escape sequence
static const pango_tag GetOpeningTag(ansi_escape_code code) {
if (code == ANSI_BOLD)
return PangoTag("<b>");
else if (code == ANSI_ITALIC)
return PangoTag("<i>");
else if (code == ANSI_UNDERLINE)
return PangoTag("<u>");
else if (code == ANSI_STRIKETHROUGH)
return PangoTag("<s>");
else if (ANSI_FONT_BLACK <= code && code <= ANSI_FONT_WHITE)
return FONT_TAGS[code - ANSI_FONT_BLACK];
else if (ANSI_BG_BLACK <= code && code <= ANSI_BG_WHITE)
return BG_TAGS[code - ANSI_BG_BLACK];
return PangoTag("");
}

// Get closing markup tag from ANSI escape sequence
static const pango_tag GetClosingTag(ansi_escape_code code) {
if (code == ANSI_BOLD)
return PangoTag("</b>");
else if (code == ANSI_ITALIC)
return PangoTag("</i>");
else if (code == ANSI_UNDERLINE)
return PangoTag("</u>");
else if (code == ANSI_STRIKETHROUGH)
return PangoTag("</s>");
else if ((ANSI_FONT_BLACK <= code && code <= ANSI_FONT_WHITE) ||
(ANSI_BG_BLACK <= code && code <= ANSI_BG_WHITE))
return PangoTag("</span>");
return PangoTag("");
}

#define MAX_STACK_SIZE 64

// Stack for markup tags
class TagStack {
private:
ansi_escape_code m_tags[MAX_STACK_SIZE];
int m_top;

public:
TagStack() : m_tags(), m_top(-1) {}

void Push(ansi_escape_code code) {
if (m_top < MAX_STACK_SIZE - 1)
m_tags[++m_top] = code;
}

int Size() const { return m_top + 1; }

void Clear() { m_top = -1; }

using GetTagFunc = const pango_tag (*)(ansi_escape_code);

// Get length of stacked tags
int GetTagLength(GetTagFunc GetTag) const {
int len = 0;
const ansi_escape_code* max_tag = m_tags + m_top;
for (const ansi_escape_code* code = m_tags; code <= max_tag; code++) {
len += GetTag(*code).len;
}
return len;
}

inline int OpeningTagLength() const {
return GetTagLength(GetOpeningTag);
}

inline int ClosingTagLength() const {
return GetTagLength(GetClosingTag);
}

// Copy stacked tags to char*
int CopyTag(char* output, GetTagFunc GetTag, bool reverse) const {
char* start = output;
for (int i = 0; i <= m_top; i++) {
int idx = reverse ? m_top - i : i;
const pango_tag opening_tag = GetTag(m_tags[idx]);
memcpy(output, opening_tag.str, opening_tag.len);
output += opening_tag.len;
}
return static_cast<int>(output - start);
}

inline int CopyOpeningTag(char* output) const {
return CopyTag(output, GetOpeningTag, false);
}

inline int CopyClosingTag(char* output) const {
return CopyTag(output, GetClosingTag, true);
}
};

// Get string length for ConvertAnsiToPango()
int ConvertAnsiToPangoLength(TagStack* stack, const char *input) {
const char *p = input;
int closing_tag_len = stack->ClosingTagLength();
int len = stack->OpeningTagLength();

while (*p) {
if (*p == '\033' && *(p + 1) == '[') { // Found an ANSI escape sequence
p += 2; // Skip "\033["
while (*p) {
int code = 0;
while (*p >= '0' && *p <= '9') {
code = code * 10 + (*p - '0');
p++;
}

char c = *p;
if (c == 'm' || c == ';') {
p++; // Skip 'm' and ';'
if (code == ANSI_RESET) {
len += closing_tag_len;
closing_tag_len = 0;
} else {
len += GetOpeningTag(code).len;
closing_tag_len += GetClosingTag(code).len;
}
if (c == 'm')
break;
}
}
continue;
} else if (*p == '&') {
len += 5; // &amp;
} else if (*p == '<') {
len += 4; // &lt;
} else if (*p == '>') {
len += 4; // &gt;
} else if (*p == '\'') {
len += 6; // &apos;
} else if (*p == '"') {
len += 6; // &quot;
} else {
// Copy regular characters
len++;
}
p++;
}

len += closing_tag_len;

return len;
}

// Function to replace ANSI escape sequences with Pango markup
void ConvertAnsiToPango(TagStack* stack, const char *input, char *output) {
const char *p = input;
char *q = output;

// Add opening tags
q += stack->CopyOpeningTag(q);

while (*p) {
if (*p == '\033' && *(p + 1) == '[') { // Found an ANSI escape sequence
p += 2; // Skip "\033["

while (*p) {
int code = 0;
while (*p >= '0' && *p <= '9') {
code = code * 10 + (*p - '0');
p++;
}

char c = *p;
if (c == 'm' || c == ';') {
p++; // Skip 'm' and ';'
if (code == ANSI_RESET) {
q += stack->CopyClosingTag(q);
stack->Clear();
} else {
const pango_tag opening_tag = GetOpeningTag(code);
memcpy(q, opening_tag.str, opening_tag.len);
q += opening_tag.len;
stack->Push(code);
}
if (c == 'm')
break;
}
}
continue;
} else if (*p == '&') {
memcpy(q, "&amp;", 5);
q += 5;
} else if (*p == '<') {
memcpy(q, "&lt;", 4);
q += 4;
} else if (*p == '>') {
memcpy(q, "&gt;", 4);
q += 4;
} else if (*p == '\'') {
memcpy(q, "&apos;", 6);
q += 6;
} else if (*p == '"') {
memcpy(q, "&quot;", 6);
q += 6;
} else {
// Copy regular characters
*q++ = *p;
}
p++;
}

// Add closing tags
q += stack->CopyClosingTag(q);

// Null-terminate the output
*q = '\0';
}

#define MAX_LOG_BUFFER_SIZE 1024 * 1024

class Logger {
private:
uiMultilineEntry* m_log_entry;
std::string m_log_buffer;
TagStack m_tag_stack; // stack for markup tags
int m_buffer_length;

public:
Logger() : m_log_entry(NULL), m_log_buffer("") {}
Logger() : m_log_entry(NULL), m_log_buffer(""),
m_tag_stack(), m_buffer_length(0) {}
~Logger() {}

void SetLogEntry(void* log_entry) {
Expand All @@ -87,10 +365,31 @@ class Logger {
}

void Log(const char* str) {
int markup_length = ConvertAnsiToPangoLength(&m_tag_stack, str);
char *markup_str = new char[markup_length + 1];
ConvertAnsiToPango(&m_tag_stack, str, markup_str);
if (m_log_entry == NULL) {
m_log_buffer += str;
m_log_buffer += markup_str;
} else {
uiMultilineEntryAppend(m_log_entry, str);
if (m_buffer_length + markup_length > MAX_LOG_BUFFER_SIZE) {
m_buffer_length = 0;
char* text = uiMultilineEntryText(m_log_entry);
if (text) {
int text_len = strlen(text);
if (text_len > MAX_LOG_BUFFER_SIZE / 2) {
m_buffer_length = MAX_LOG_BUFFER_SIZE / 2;
uiMultilineEntrySetText(m_log_entry,
text + text_len - MAX_LOG_BUFFER_SIZE / 2);
} else {
uiMultilineEntrySetText(m_log_entry, "");
}
uiFreeText(text);
} else {
uiMultilineEntrySetText(m_log_entry, "");
}
}
m_buffer_length += markup_length;
uiUnixMultilineEntryMarkupAppend(m_log_entry, markup_str);
uiUnixMuntilineEntryScrollToEnd(m_log_entry);
}
}
Expand Down
2 changes: 1 addition & 1 deletion subprojects/libui.wrap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[wrap-git]
url = https://github.com/matyalatte/libui-ng.git
revision = ef635ab7789a153f8d078bc7759b3be5fb453f90
revision = ba11b3a232b661318a0c819ce147a02fdd0a6a6c
depth = 1

[provide]
Expand Down

0 comments on commit 5bf7242

Please sign in to comment.