mirror of
https://github.com/fastfetch-cli/fastfetch.git
synced 2026-09-12 10:22:12 +02:00
Common (stringUtil): adds ffUtf8CharLenWidth
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#include "common/wcwidth.h"
|
||||
|
||||
static inline bool ffStrSet(const char* str) {
|
||||
if (str == NULL) {
|
||||
return false;
|
||||
@@ -71,6 +73,88 @@ static inline bool ffCharIsDigit(char c) {
|
||||
return '0' <= c && c <= '9';
|
||||
}
|
||||
|
||||
// Parse one UTF-8 character, returning consumed byte count and display width.
|
||||
// Invalid / incomplete sequence falls back to one-byte width=1.
|
||||
// If the Unicode codepoint is non-printable, width becomes 0.
|
||||
static inline uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width) {
|
||||
if (__builtin_expect(length == 0 || *str == '\0', false)) {
|
||||
if (width) {
|
||||
*width = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
unsigned char first = (unsigned char) *str;
|
||||
if (__builtin_expect(first < 0x80, true)) {
|
||||
if (width) {
|
||||
*width = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t bytes;
|
||||
if ((first & 0xE0) == 0xC0) {
|
||||
bytes = 2;
|
||||
} else if ((first & 0xF0) == 0xE0) {
|
||||
bytes = 3;
|
||||
} else if ((first & 0xF8) == 0xF0) {
|
||||
bytes = 4;
|
||||
} else {
|
||||
if (width) {
|
||||
*width = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (length < bytes) {
|
||||
if (width) {
|
||||
*width = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (uint8_t i = 1; i < bytes; ++i) {
|
||||
unsigned char continuation = (unsigned char) str[i];
|
||||
if (continuation == '\0' || (continuation & 0xC0) != 0x80) {
|
||||
if (width) {
|
||||
*width = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ucs = (uint32_t) (first & ((1U << (8 - bytes)) - 1));
|
||||
for (uint8_t i = 1; i < bytes; ++i) {
|
||||
ucs <<= 6;
|
||||
ucs |= (uint32_t) ((unsigned char) str[i] & 0x3F);
|
||||
}
|
||||
|
||||
int wcWidth = mk_wcwidth(ucs);
|
||||
if (width) {
|
||||
*width = (uint8_t) (wcWidth < 0 ? 0 : wcWidth);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static inline uint32_t ffUtf8StrWidth(const char* str, uint32_t length) {
|
||||
uint32_t result = 0;
|
||||
const char* ptr = str;
|
||||
|
||||
while (length > 0 && *ptr != '\0') {
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(ptr, length, &width);
|
||||
if (__builtin_expect(bytes == 0, false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
result += width;
|
||||
ptr += bytes;
|
||||
length -= bytes;
|
||||
}
|
||||
|
||||
return result > 0 ? result : (uint32_t) (ptr - str);
|
||||
}
|
||||
|
||||
static inline bool ffCharIsHexDigit(char c) {
|
||||
return ffCharIsDigit(c) || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F');
|
||||
}
|
||||
|
||||
+3
-16
@@ -139,22 +139,9 @@ static void logoLineCacheBuild(FFLogoLineCacheState* cache, const char* data, bo
|
||||
}
|
||||
}
|
||||
|
||||
++lineWidth;
|
||||
|
||||
int codepoint = (unsigned char) *data;
|
||||
uint8_t bytes;
|
||||
|
||||
if (codepoint <= 127) {
|
||||
bytes = 1;
|
||||
} else if ((codepoint & 0xE0) == 0xC0) {
|
||||
bytes = 2;
|
||||
} else if ((codepoint & 0xF0) == 0xE0) {
|
||||
bytes = 3;
|
||||
} else if ((codepoint & 0xF8) == 0xF0) {
|
||||
bytes = 4;
|
||||
} else {
|
||||
bytes = 1;
|
||||
}
|
||||
uint8_t charWidth;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(data, UINT32_MAX, &charWidth);
|
||||
lineWidth += charWidth;
|
||||
|
||||
for (uint8_t i = 0; i < bytes; ++i) {
|
||||
if (*data == '\0') {
|
||||
|
||||
@@ -1,64 +1,10 @@
|
||||
#include "common/printing.h"
|
||||
#include "common/jsonconfig.h"
|
||||
#include "common/stringUtils.h"
|
||||
#include "common/mallocHelper.h"
|
||||
#include "common/wcwidth.h"
|
||||
#include "common/textModifier.h"
|
||||
#include "logo/logo.h"
|
||||
#include "modules/separator/separator.h"
|
||||
|
||||
#include <locale.h>
|
||||
|
||||
#if __SIZEOF_WCHAR_T__ == 4
|
||||
static inline size_t mbrtoc32(uint32_t* restrict pc32, const char* restrict s, size_t n, mbstate_t* restrict ps) {
|
||||
return mbrtowc((wchar_t*) pc32, s, n, ps);
|
||||
}
|
||||
#else
|
||||
#include <uchar.h>
|
||||
#endif
|
||||
|
||||
static uint8_t getMbrWidth(const char* mbstr, uint32_t length, const char** next, mbstate_t* state) {
|
||||
if (__builtin_expect((uint8_t) *mbstr < 0x80, true)) // ASCII fast path
|
||||
{
|
||||
if (next) {
|
||||
*next = mbstr + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t c32;
|
||||
uint32_t len = (uint32_t) mbrtoc32(&c32, mbstr, length, state);
|
||||
if (len >= (uint32_t) -3) {
|
||||
// Invalid or incomplete multibyte sequence
|
||||
if (next) {
|
||||
*next = mbstr + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
*next = mbstr + len;
|
||||
}
|
||||
int width = mk_wcwidth(c32);
|
||||
return width < 0 ? 0 : (uint8_t) width;
|
||||
}
|
||||
|
||||
static uint32_t getWcsWidth(const FFstrbuf* mbstr) {
|
||||
mbstate_t state = {};
|
||||
uint32_t remainLength = mbstr->length;
|
||||
uint32_t result = 0;
|
||||
|
||||
const char* ptr = mbstr->chars;
|
||||
while (remainLength > 0 && *ptr != '\0') {
|
||||
const char* lastPtr = NULL;
|
||||
result += getMbrWidth(ptr, remainLength, &lastPtr, &state);
|
||||
remainLength -= (uint32_t) (lastPtr - ptr);
|
||||
ptr = lastPtr;
|
||||
}
|
||||
|
||||
return result > 0 ? (uint32_t) result : mbstr->length;
|
||||
}
|
||||
|
||||
bool ffPrintSeparator(FFSeparatorOptions* options) {
|
||||
ffLogoPrintLine();
|
||||
|
||||
@@ -75,17 +21,16 @@ bool ffPrintSeparator(FFSeparatorOptions* options) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setlocale(LC_CTYPE, "");
|
||||
const FFPlatform* platform = &instance.state.platform;
|
||||
|
||||
uint32_t titleLength = 1 // @
|
||||
+ getWcsWidth(&platform->userName) // user name
|
||||
+ ffUtf8StrWidth(platform->userName.chars, platform->userName.length) // user name
|
||||
+ (instance.state.titleFqdn ? platform->hostName.length : ffStrbufFirstIndexC(&platform->hostName, '.')); // host name
|
||||
|
||||
if (__builtin_expect(options->string.length == 1, 1)) {
|
||||
ffPrintCharTimes(options->string.chars[0], titleLength);
|
||||
} else {
|
||||
uint32_t wcsLength = getWcsWidth(&options->string);
|
||||
uint32_t wcsLength = ffUtf8StrWidth(options->string.chars, options->string.length);
|
||||
|
||||
int remaining = (int) titleLength;
|
||||
// Write the whole separator as often as it fits fully into titleLength
|
||||
@@ -98,11 +43,17 @@ bool ffPrintSeparator(FFSeparatorOptions* options) {
|
||||
if (wcsLength != options->string.length) {
|
||||
// Unicode chars
|
||||
const char* ptr = options->string.chars;
|
||||
mbstate_t state = {};
|
||||
const char* next = NULL;
|
||||
while (remaining > 0 && *ptr != '\0') {
|
||||
remaining -= (int) getMbrWidth(ptr, (uint32_t) (options->string.length - (ptr - options->string.chars)), &next, &state);
|
||||
ptr = next;
|
||||
uint32_t remainBytes = options->string.length;
|
||||
while (remaining > 0 && remainBytes > 0 && *ptr != '\0') {
|
||||
uint8_t charWidth = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(ptr, remainBytes, &charWidth);
|
||||
if (__builtin_expect(bytes == 0, false)) {
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= (int) charWidth;
|
||||
ptr += bytes;
|
||||
remainBytes -= bytes;
|
||||
}
|
||||
fwrite(options->string.chars, (size_t) (ptr - options->string.chars), 1, stdout);
|
||||
} else {
|
||||
@@ -110,7 +61,6 @@ bool ffPrintSeparator(FFSeparatorOptions* options) {
|
||||
}
|
||||
}
|
||||
}
|
||||
setlocale(LC_CTYPE, "C");
|
||||
}
|
||||
|
||||
if (options->outputColor.length && !instance.config.display.pipe) {
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
#include "common/stringUtils.h"
|
||||
#include "common/textModifier.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static void verify(bool expression, const char* expressionStr, int lineNo) {
|
||||
if (expression) {
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#define VERIFY(expression) verify((expression), #expression, __LINE__)
|
||||
|
||||
int main(void) {
|
||||
{
|
||||
uint8_t width = 255;
|
||||
uint8_t bytes = ffUtf8CharLenWidth("", 0, &width);
|
||||
VERIFY(bytes == 0);
|
||||
VERIFY(width == 0);
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth("A", 1, &width);
|
||||
VERIFY(bytes == 1);
|
||||
VERIFY(width == 1);
|
||||
}
|
||||
|
||||
{
|
||||
const char* ch = "\xE6\x96\x87"; // 文 U+6587
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(ch, 3, &width);
|
||||
int expected = mk_wcwidth(0x6587);
|
||||
VERIFY(bytes == 3);
|
||||
VERIFY(width == (uint8_t) (expected < 0 ? 0 : expected));
|
||||
}
|
||||
|
||||
{
|
||||
const char* combining = "\xCC\x81"; // U+0301
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(combining, 2, &width);
|
||||
int expected = mk_wcwidth(0x0301);
|
||||
VERIFY(bytes == 2);
|
||||
VERIFY(width == (uint8_t) (expected < 0 ? 0 : expected));
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth("\xE6\x96\x87", 1, &width); // truncated
|
||||
VERIFY(bytes == 1);
|
||||
VERIFY(width == 1);
|
||||
}
|
||||
|
||||
{
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth("\xE6"
|
||||
"A",
|
||||
2,
|
||||
&width); // invalid continuation
|
||||
VERIFY(bytes == 1);
|
||||
VERIFY(width == 1);
|
||||
}
|
||||
|
||||
{
|
||||
VERIFY(ffUtf8StrWidth("abc", 3) == 3);
|
||||
}
|
||||
|
||||
{
|
||||
const char* mixed = "A"
|
||||
"\xE6\x96\x87"
|
||||
"B";
|
||||
int wide = mk_wcwidth(0x6587);
|
||||
uint32_t expected = 2 + (uint32_t) (wide < 0 ? 0 : wide);
|
||||
VERIFY(ffUtf8StrWidth(mixed, 5) == expected);
|
||||
}
|
||||
|
||||
{
|
||||
const char* combining = "A\xCC\x81";
|
||||
int wCombining = mk_wcwidth(0x0301);
|
||||
uint32_t expected = 1 + (uint32_t) (wCombining < 0 ? 0 : wCombining);
|
||||
VERIFY(ffUtf8StrWidth(combining, 3) == expected);
|
||||
}
|
||||
|
||||
{
|
||||
VERIFY(ffUtf8StrWidth("\xE6"
|
||||
"A",
|
||||
2) == 2);
|
||||
}
|
||||
|
||||
{
|
||||
VERIFY(ffUtf8StrWidth("", 0) == 0);
|
||||
VERIFY(ffUtf8StrWidth("A\0B", 3) == 1);
|
||||
}
|
||||
|
||||
{
|
||||
const char* combining = "\xCC\x81";
|
||||
int wCombining = mk_wcwidth(0x0301);
|
||||
uint32_t normalized = (uint32_t) (wCombining < 0 ? 0 : wCombining);
|
||||
uint32_t expected = normalized > 0 ? normalized : 2;
|
||||
VERIFY(ffUtf8StrWidth(combining, 2) == expected);
|
||||
}
|
||||
|
||||
{
|
||||
const char* emoji = "\xF0\x9F\x98\x80"; // U+1F600 😀
|
||||
uint8_t width = 0;
|
||||
uint8_t bytes = ffUtf8CharLenWidth(emoji, 4, &width);
|
||||
VERIFY(bytes == 4);
|
||||
VERIFY(width == 2); // Most emoji are double-width
|
||||
}
|
||||
|
||||
puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user