summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordavid <[email protected]>2026-10-26 18:27:07 -0300
committerdavid <[email protected]>2026-10-26 18:27:07 -0300
commitb5e3328f64c1f47c9df03ec8eb7c1098c5f7df83 (patch)
tree9d83bf7e140020988ab12524a29ab94a109432ea /src
parent7aff48a428f5c9c765f71d364594a9f273d04e8a (diff)
downloadmikusays-c-1.0.0.tar.gz
mikusays-c-1.0.0.zip
finally finished this. it should be 1:1 with the original rust version...v1.0.0
Diffstat (limited to 'src')
-rw-r--r--src/colors.c44
-rw-r--r--src/main.c382
2 files changed, 306 insertions, 120 deletions
diff --git a/src/colors.c b/src/colors.c
index 0978c60..973ebf6 100644
--- a/src/colors.c
+++ b/src/colors.c
@@ -560,17 +560,49 @@ ColorParseError parse_color(const char *s, rgbcolor *out){
const char *color_parse_error_message(ColorParseError error){
switch(error){
case COLOR_ERR_FORMAT:
- return "Invaliid color format";
+ return "Invalid color format";
case COLOR_ERR_HEX:
- return "Invaliid hex color code";
+ return "Invalid hex color code";
case COLOR_ERR_RGB:
- return "Invaliid RGB format. Expected format: R,G,B";
+ return "Invalid RGB format. Expected format: R,G,B";
case COLOR_ERR_HSL:
- return "Invaliid HSL format. Expected format: hsl(H,S%,L%)";
- case COLOR_ERR_UNKNOWN_NAME:
- return "Unkown named color";
+ return "Invalid HSL format. Expected format: hsl(H,S%,L%)";
case COLOR_OK:
default:
return "";
}
+}
+static const char *color_mode_name(color_mode mode){
+ switch(mode){
+ case MODE_TRUECOLOR: return "Truecolor";
+ case MODE_ANSI256: return "Ansi256";
+ case MODE_ANSI16: return "Ansi16";
+ case MODE_NONE:
+ default: return "NoColor";
+ }
+}
+
+static void print_env_debug(const char *name){
+ const char *value = getenv(name);
+ if(value)
+ printf("%s: Some(\"%s\")\n", name, value);
+ else
+ printf("%s: None\n", name);
+}
+
+void print_color_debug(const colorconfig *config, bool ansi_enabled){
+ printf("Detected color mode: %s\n", color_mode_name(config->mode));
+ printf("ANSI enabled: %s\n", ansi_enabled ? "true" : "false");
+ print_env_debug("NO_COLOR");
+ print_env_debug("COLORTERM");
+ print_env_debug("TERM");
+#ifdef _WIN32
+ {
+ const char *shell = detect_windows_shell();
+ if(shell)
+ printf("Windows shell hint: Some(\"%s\")\n", shell);
+ else
+ printf("Windows shell hint: None\n");
+ }
+#endif
}
diff --git a/src/main.c b/src/main.c
index 350832e..00c6f5f 100644
--- a/src/main.c
+++ b/src/main.c
@@ -16,28 +16,46 @@ static struct win term = {80, 24}; //if the function fails this is the defualt
#ifdef _WIN32
#include <windows.h>
- //i have no way to test this because i don't have any windows pc in my house qwq
- //(i asked claude to write this for windows because i never looked at the windows api in my life)
static void getterminalsize(void) {
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
+ int found = GetConsoleScreenBufferInfo(console, &info);
+
SetConsoleOutputCP(CP_UTF8);
- CONSOLE_SCREEN_BUFFER_INFO csbi;
- if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
- term.winwidth = csbi.srWindow.Right - csbi.srWindow.Left + 1;
- term.winheight = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
+ if(!found){
+ HANDLE fallback = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
+ if(fallback != INVALID_HANDLE_VALUE){
+ found = GetConsoleScreenBufferInfo(fallback, &info);
+ CloseHandle(fallback);
+ }
+ }
+ if(found){
+ term.winwidth = info.srWindow.Right - info.srWindow.Left + 1;
+ term.winheight = info.srWindow.Bottom - info.srWindow.Top + 1;
}
}
-#else //or this if they're not on windows which should be posixish like linux or macos
+#else
#include <sys/ioctl.h>
#include <unistd.h>
+ #include <fcntl.h>
static void getterminalsize(void) {
- struct winsize ws;
- if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
- if(ws.ws_col > 0)
- term.winwidth = ws.ws_col;
- if(ws.ws_row > 0)
- term.winheight = ws.ws_row;
+ struct winsize size_info;
+ int tty_descriptor = -1;
+ int status = ioctl(STDOUT_FILENO, TIOCGWINSZ, &size_info);
+
+ if(status != 0){
+ tty_descriptor = open("/dev/tty", O_RDONLY);
+ if(tty_descriptor >= 0)
+ status = ioctl(tty_descriptor, TIOCGWINSZ, &size_info);
}
+ if(status == 0){
+ if(size_info.ws_col > 0) term.winwidth = size_info.ws_col;
+ if(size_info.ws_row > 0) term.winheight = size_info.ws_row;
+ }
+ if(tty_descriptor >= 0)
+ close(tty_descriptor);
}
#endif
@@ -154,11 +172,9 @@ char **wraptext(const char *text, int max, int *count){
#define MIN_BUBBLE_WIDTH 20
char **getbubblelines(char *text){
char **templines = wraptext(text, MAX_BUBBLE_WIDTH - 4, &count);
- if(templines == NULL)
- return NULL;
size_t content_width = 0;
- for (int i = 0; templines[i] != NULL; i++) {
+ for (int i = 0; templines != NULL && templines[i] != NULL; i++) {
size_t current_len = displaywidth(templines[i]);
if (current_len > content_width)
content_width = current_len;
@@ -185,7 +201,7 @@ char **getbubblelines(char *text){
strcat(bubblelines[0], horizontal);
strcat(bubblelines[0], top_right);
- for (int i = 0; lines[i] != NULL; i++) {
+ for (int i = 0; lines != NULL && lines[i] != NULL; i++) {
int line_width = displaywidth(lines[i]);
int padding_needed = (bubble_width - 2) - line_width;
if (padding_needed < 0) padding_needed = 0;
@@ -215,15 +231,17 @@ static void print_help(void){
"Arguments:\n"
" [TEXT] Text to display in the speech bubble\n\n"
"Options:\n"
- " -s, --style <STYLE> Style of the Miku art. A random one is chosen if not specified\n"
- " -l, --list List all available art styles with their indices\n"
- " -h, --help Print help\n"
- " -V, --version Print version\n"
- " --rainbow Apply a smooth rainbow gradient across the art\n"
- " --saturation <0-100> Rainbow saturation (default: 100)\n"
- " --brightness <0-100> Rainbow brightness (default: 50)\n"
- " --color <COLOR> Set a single solid color\n"
- " --no-color Disable all coloring\n");
+ " -s, --style <STYLE> Style of the Miku art. A random one is chosen if not specified\n"
+ " -l, --list List all available art styles with their indices\n"
+ " --rainbow Apply a smooth rainbow gradient across the ASCII art\n"
+ " --saturation <SATURATION> Saturation level for rainbow gradient (0-100) [default: 100]\n"
+ " --brightness <BRIGHTNESS> Brightness level for rainbow gradient (0-100) [default: 50]\n"
+ " --color <COLOR> Override with a single solid color\n"
+ " --gradient <START:END> Define a custom two-color gradient (e.g., --gradient red:blue)\n"
+ " --no-color Disable all coloring (respects NO_COLOR environment variable)\n"
+ " --color-debug Print color detection/debug info and exit\n"
+ " -h, --help Print help\n"
+ " -V, --version Print version\n");
}
static void list_all_art_styles(void){
@@ -238,11 +256,24 @@ static void list_all_art_styles(void){
}
printf("\nUse --style <number> to select a specific style, or omit for random selection.\n");
+
+ printf("\nColor Options:\n"
+ " --rainbow Apply a smooth rainbow gradient\n"
+ " --saturation <0-100> Set rainbow saturation level (default: 100)\n"
+ " --brightness <0-100> Set rainbow brightness level (default: 50)\n"
+ " --color <COLOR> Set a single solid color (supports named colors, hex, rgb, hsl, ansi)\n"
+ " --gradient <START:END> Define a custom two-color gradient\n"
+ " --no-color Disable all coloring\n"
+ "\nColor Format Examples:\n"
+ " --color red Named color\n"
+ " --color #FF0000 Hex color\n"
+ " --color 255,0,128 RGB tuple\n"
+ " --color hsl(180,100%%,50%%) HSL value\n"
+ " --color 93 ANSI color code\n"
+ " --gradient red:blue Custom gradient\n");
}
static struct colorconfig colors;
-static int rainbow = 0, has_solid = 0;
-static rgbcolor solid;
static int is_art_space(unsigned long codepoint){
return (codepoint >= 0x09 && codepoint <= 0x0D) || codepoint == 0x20 || codepoint == 0x85 || codepoint == 0xA0
@@ -263,34 +294,42 @@ static size_t count_colored_chars(char **art){
return total;
}
+static rgbcolor color_for_index(size_t index, size_t total){
+ if(colors.has_gradient){
+ float position = total <= 1 ? 0.0f : (float)index / (float)(total - 1);
+ return lerp_color_hsv(colors.gradient_start, colors.gradient_end, position);
+ }
+ return hsl_to_rgb((unsigned short)((float)index / (float)total * 360.0f + 0.5f),
+ (unsigned char)colors.saturation, (unsigned char)colors.brightness);
+}
+
static void print_art_line(const char *line, size_t *index, size_t total){
- char esc[32];
- const unsigned char *p = (const unsigned char *)line;
+ char escape[32];
+ const unsigned char *cursor = (const unsigned char *)line;
+ int per_character = colors.rainbow_enabled || colors.has_gradient;
- if(colors.mode == MODE_NONE || (!rainbow && !has_solid)){
+ if(colors.mode == MODE_NONE || (!per_character && !colors.has_solid_color)){
fputs(line, stdout);
return;
}
- if(!rainbow){//solid: one color for the whole line
- rgb_to_fg_escape(solid, colors.mode, esc, sizeof(esc));
- printf("%s%s\x1b[0m", esc, line);
+ if(!per_character){
+ rgb_to_fg_escape(colors.solid_color, colors.mode, escape, sizeof(escape));
+ printf("%s%s\x1b[0m", escape, line);
return;
}
- while(*p){//rainbow: one color per non-space char
+ while(*cursor){
unsigned long codepoint;
- int len = utf8_decode(p, &codepoint);
+ int byte_count = utf8_decode(cursor, &codepoint);
if(is_art_space(codepoint)){
- fwrite(p, 1, (size_t)len, stdout);
+ fwrite(cursor, 1, (size_t)byte_count, stdout);
}else{
- unsigned short hue = (unsigned short)((float)*index / (float)total * 360.0f + 0.5f);
- rgbcolor c = hsl_to_rgb(hue, colors.saturation, colors.brightness);
- rgb_to_fg_escape(c, colors.mode, esc, sizeof(esc));
- printf("%s", esc);
- fwrite(p, 1, (size_t)len, stdout);
+ rgb_to_fg_escape(color_for_index(*index, total), colors.mode, escape, sizeof(escape));
+ fputs(escape, stdout);
+ fwrite(cursor, 1, (size_t)byte_count, stdout);
fputs("\x1b[0m", stdout);
(*index)++;
}
- p += len;
+ cursor += byte_count;
}
}
@@ -404,109 +443,224 @@ void draw_miku_says(char *text, int style){
freelines(speech_bubble_lines);
free(all_content_lines);
}
+
+#define COLOR_FORMAT_HELP "expected a named color, #RRGGBB, #RGB, R,G,B, hsl(H,S%%,L%%), or ANSI value 0-255"
+
+static int usage_error(const char *format, ...){
+ va_list arguments;
+ va_start(arguments, format);
+ fputs("error: ", stderr);
+ vfprintf(stderr, format, arguments);
+ va_end(arguments);
+ fputs("\n\nUsage: mikusays [OPTIONS] [TEXT]\n\nFor more information, try '--help'.\n", stderr);
+ return 2;
+}
+
+//returns 1 if argv[*index] is this option; *value stays NULL when the value is missing
+static int match_value_option(int argc, char **argv, int *index, const char *long_name,
+ char short_name, const char **value){
+ const char *argument = argv[*index];
+ size_t long_length = strlen(long_name);
+
+ *value = NULL;
+ if(strcmp(argument, long_name) == 0
+ || (short_name != '\0' && argument[0] == '-' && argument[1] == short_name && argument[2] == '\0')){
+ if(*index + 1 < argc){
+ (*index)++;
+ *value = argv[*index];
+ }
+ return 1;
+ }
+ if(strncmp(argument, long_name, long_length) == 0 && argument[long_length] == '='){
+ *value = argument + long_length + 1;
+ return 1;
+ }
+ if(short_name != '\0' && argument[0] == '-' && argument[1] == short_name && argument[2] != '\0'){
+ *value = argument + 2;
+ return 1;
+ }
+ return 0;
+}
+
+static int parse_percent_option(const char *flag, const char *value, int *destination){
+ char *end;
+ long parsed;
+
+ if(value == NULL)
+ return usage_error("a value is required for '%s' but none was supplied", flag);
+ parsed = strtol(value, &end, 10);
+ if(*value == '\0' || *end != '\0' || parsed < 0 || parsed > 100)
+ return usage_error("invalid value '%s' for '%s'", value, flag);
+ *destination = (int)parsed;
+ return 0;
+}
+
+static int parse_gradient(const char *specification, rgbcolor *start, rgbcolor *end){
+ const char *separator = strchr(specification, ':');
+ char start_text[64];
+ size_t start_length;
+ ColorParseError error;
+
+ if(separator == NULL)
+ return usage_error("invalid gradient: expected START:END (for example, red:blue)");
+ start_length = (size_t)(separator - specification);
+ if(start_length == 0 || separator[1] == '\0' || strchr(separator + 1, ':') != NULL)
+ return usage_error("invalid gradient: expected exactly two colors as START:END");
+ if(start_length >= sizeof(start_text))
+ return usage_error("invalid gradient start: %s; " COLOR_FORMAT_HELP,
+ color_parse_error_message(COLOR_ERR_FORMAT));
+
+ memcpy(start_text, specification, start_length);
+ start_text[start_length] = '\0';
+ error = parse_color(start_text, start);
+ if(error != COLOR_OK)
+ return usage_error("invalid gradient start '%s': %s; " COLOR_FORMAT_HELP,
+ start_text, color_parse_error_message(error));
+ error = parse_color(separator + 1, end);
+ if(error != COLOR_OK)
+ return usage_error("invalid gradient end '%s': %s; " COLOR_FORMAT_HELP,
+ separator + 1, color_parse_error_message(error));
+ return 0;
+}
int main(int argc, char **argv){
+ long style = -1; //-1 means random
+ int index, result = 0, list_requested = 0, color_debug = 0, no_color = 0;
+ int has_text = 0, options_ended = 0, ansi_enabled = 0;
+ const char *value, *color_text = NULL, *gradient_text = NULL;
+ ColorParseError color_error;
+ size_t length = 0;
+ char *text;
+
srand(time(NULL));
getterminalsize();
+ colorconfig_init(&colors);
- colorconfig_init(&colors);
-
- long style = -1; //-1 means random
- int list = 0;
- int ret = 0;
-
- size_t length = 0;
- for(int i = 1; i < argc; i++)
- length += strlen(argv[i]);
- char *text = malloc(length + argc);
+ for(index = 1; index < argc; index++)
+ length += strlen(argv[index]);
+ text = malloc(length + (size_t)argc);
if(!text) return 1;
*text = '\0';
- for(int i = 1; i < argc; i++){
- if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0){
+ for(index = 1; index < argc; index++){
+ const char *argument = argv[index];
+
+ if(!options_ended && strcmp(argument, "--") == 0){
+ options_ended = 1;
+ } else if(options_ended || argument[0] != '-' || argument[1] == '\0'){
+ strcat(text, argument);
+ strcat(text, " ");
+ has_text = 1;
+ } else if(strcmp(argument, "-h") == 0 || strcmp(argument, "--help") == 0){
print_help();
goto out;
- } else if(strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0){
+ } else if(strcmp(argument, "-V") == 0 || strcmp(argument, "--version") == 0){
printf("mikusays " VERSION "\n");
goto out;
- } else if(strcmp(argv[i], "-l") == 0 || strcmp(argv[i], "--list") == 0){
- list = 1;
- } else if(strcmp(argv[i], "-s") == 0 || strcmp(argv[i], "--style") == 0){
- if(i + 1 >= argc){
- fprintf(stderr, "error: a value is required for '--style <STYLE>' but none was supplied\n");
- ret = 2;
- goto out;
- }
- i++;
+ } else if(strcmp(argument, "-l") == 0 || strcmp(argument, "--list") == 0){
+ list_requested = 1;
+ } else if(strcmp(argument, "--no-color") == 0){
+ no_color = 1;
+ colors.mode = MODE_NONE;
+ }else if(strcmp(argument, "--rainbow") == 0){
+ colors.rainbow_enabled = true;
+ } else if(strcmp(argument, "--color-debug") == 0){
+ color_debug = 1;
+ } else if(match_value_option(argc, argv, &index, "--style", 's', &value)){
char *end;
- long v = strtol(argv[i], &end, 10);
- if(*argv[i] == '\0' || *end != '\0'){
- fprintf(stderr, "error: invalid value '%s' for '--style <STYLE>': not a number\n", argv[i]);
- ret = 2;
+ long parsed;
+ if(value == NULL){
+ result = usage_error("a value is required for '--style <STYLE>' but none was supplied");
goto out;
}
- style = v < 0 ? -1 : v; //negative = random
- } else if(strcmp(argv[i], "--no-color") == 0){
- colors.mode = MODE_NONE;
- } else if(strcmp(argv[i], "--rainbow") == 0){
- rainbow = 1;
- } else if(strcmp(argv[i], "--color") == 0){
- if(i + 1 >= argc){
- fprintf(stderr, "error: a value is required for '--color <COLOR>' but none was supplied\n");
- ret = 2;
+ parsed = strtol(value, &end, 10);
+ if(*value == '\0' || *end != '\0'){
+ result = usage_error("invalid value '%s' for '--style <STYLE>': invalid digit found in string", value);
goto out;
}
- i++;
- ColorParseError err = parse_color(argv[i], &solid);
- if(err != COLOR_OK){
- fprintf(stderr, "error: invalid color '%s': %s; expected a named color, #RRGGBB, #RGB, R,G,B, hsl(H,S%%,L%%), or ANSI value 0-255\n",
- argv[i], color_parse_error_message(err));
- ret = 2;
+ if(parsed < 0){
+ result = usage_error("invalid value '%s' for '--style <STYLE>': %ld is not in 0..2147483647", value, parsed);
goto out;
}
- has_solid = 1;
- } else if(strcmp(argv[i], "--saturation") == 0 || strcmp(argv[i], "--brightness") == 0){
- const char *flag = argv[i];
- if(i + 1 >= argc){
- fprintf(stderr, "error: a value is required for '%s <0-100>' but none was supplied\n", flag);
- ret = 2;
+ style = parsed;
+ } else if(match_value_option(argc, argv, &index, "--color", '\0', &value)){
+ if(value == NULL){
+ result = usage_error("a value is required for '--color <COLOR>' but none was supplied");
goto out;
}
- i++;
- char *end;
- long v = strtol(argv[i], &end, 10);
- if(*argv[i] == '\0' || *end != '\0' || v < 0 || v > 100){
- fprintf(stderr, "error: invalid value '%s' for '%s <0-100>'\n", argv[i], flag);
- ret = 2;
+ color_text = value;
+ } else if(match_value_option(argc, argv, &index, "--gradient", '\0', &value)){
+ if(value == NULL){
+ result = usage_error("a value is required for '--gradient <START:END>' but none was supplied");
goto out;
}
- if(flag[2] == 's') colors.saturation = (unsigned char)v;
- else colors.brightness = (unsigned char)v;
+ gradient_text = value;
+ } else if(match_value_option(argc, argv, &index, "--saturation", '\0', &value)){
+ result = parse_percent_option("--saturation <SATURATION>", value, &colors.saturation);
+ if(result) goto out;
+ } else if(match_value_option(argc, argv, &index, "--brightness", '\0', &value)){
+ result = parse_percent_option("--brightness <BRIGHTNESS>", value, &colors.brightness);
+ if(result) goto out;
} else {
- strcat(text, argv[i]);
- strcat(text, " ");
+ result = usage_error("unexpected argument '%s' found", argument);
+ goto out;
}
}
- if(rainbow && has_solid){
- fprintf(stderr, "error: --rainbow cannot be used with --color\n");
- ret = 2;
+ if(colors.rainbow_enabled && color_text){
+ result = usage_error("the argument '--rainbow' cannot be used with '--color <COLOR>'");
+ goto out;
+ }
+ if(colors.rainbow_enabled && gradient_text){
+ result = usage_error("the argument '--rainbow' cannot be used with '--gradient <START:END>'");
+ goto out;
+ }
+ if(color_text && gradient_text){
+ result = usage_error("the argument '--color <COLOR>' cannot be used with '--gradient <START:END>'");
goto out;
}
- if(list){
+ if(list_requested){
list_all_art_styles();
- } else if(text[0] == '\0'){
+ goto out;
+ }
+ if(!has_text){
fprintf(stderr, "Error: Text is required when not using --list\n");
- ret = 1;
- } else if(style >= (long)migucount){
- fprintf(stderr, "Style %ld does not exist! Choose one between 0 and %zu\n", style, migucount - 1);
- ret = 1;
- } else {
- draw_miku_says(text, (int)style);
+ result = 1;
+ goto out;
+ }
+ if(style >= (long)migucount){
+ result = usage_error("style %ld does not exist; choose a value between 0 and %lu",
+ style, (unsigned long)(migucount - 1));
+ goto out;
+ }
+
+ if(color_text){
+ color_error = parse_color(color_text, &colors.solid_color);
+ if(color_error != COLOR_OK){
+ result = usage_error("invalid color '%s': %s; " COLOR_FORMAT_HELP,
+ color_text, color_parse_error_message(color_error));
+ goto out;
+ }
+ colors.has_solid_color = true;
}
-//i didn't know you could do labels like in assembly (nasm) this is very useful knowledge
+ if(gradient_text){
+ result = parse_gradient(gradient_text, &colors.gradient_start, &colors.gradient_end);
+ if(result) goto out;
+ colors.has_gradient = true;
+ }
+
+#ifdef _WIN32
+ ansi_enabled = !no_color && enable_windows_ansi();
+#endif
+ if(color_debug){
+ print_color_debug(&colors, ansi_enabled != 0);
+ goto out;
+ }
+
+ draw_miku_says(text, (int)style);
+//i didn't know you could do labels like in assembly (nasm) this is very useful knowledge
out:
free(text);
- return ret;
-}
+ return result;
+} //ooooo 666 lines spookyyy evillll