Files
mikusays-c/src/main.c
T
david f0f954d463
ci / unix (macos-15) (push) Canceled after 0s
ci / unix (macos-15-intel) (push) Canceled after 0s
ci / unix (ubuntu-24.04) (push) Canceled after 0s
ci / unix (ubuntu-24.04-arm) (push) Canceled after 0s
ci / windows (push) Canceled after 0s
ci / freebsd (push) Canceled after 0s
50% colors i'll do rainbow and whatever else i'm missing from the rust version later too lseepy rn
2026-10-13 07:43:48 -03:00

455 lines
15 KiB
C

#include "../defs/all.h"
#include "../defs/mikuart.h"
#include <stdlib.h>
#include <string.h>
char **getmikuart(int style){
if(style < 0)
style = rand() % migucount;
return mikuart[style];
}
struct win {
int winwidth;
int winheight;
};
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) {
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;
}
}
#else //or this if they're not on windows which should be posixish like linux or macos
#include <sys/ioctl.h>
#include <unistd.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;
}
}
#endif
#define TERMINALWIDTH term.winwidth
#define TERMINALHEIGHT term.winheight
static int count;
static void freelines(char **lines){
if(!lines) return;
for(int i = 0; lines[i] != NULL; i++)
free(lines[i]);
free(lines);
}
char **wraptext(const char *text, int max, int *count){
char *copy = strdup(text);
char *current_line = calloc(strlen(text)+1, sizeof(char));
char **lines = NULL;
*count = 0;
char *tok = strtok(copy, " \t\n");
while(tok != NULL){
if((size_t)displaywidth(current_line)+(size_t)displaywidth(tok) < (size_t)max){
if(current_line[0] != '\0'){
strcat(current_line, " ");
}
strcat(current_line, tok);
} else{
if(current_line[0] != '\0'){
lines = realloc(lines, (*count + 2) * sizeof(char *));
lines[*count] = strdup(current_line);
(*count)++;
lines[*count] = NULL;
}
strcpy(current_line, tok);
}
tok = strtok(NULL, " \t\n");
}
if(current_line[0] != '\0'){
lines = realloc(lines, (*count + 2) * sizeof(char *));
lines[*count] = strdup(current_line);
(*count)++;
lines[*count] = NULL;
}
free(copy);
free(current_line);
return lines;
}
#define MAX_BUBBLE_WIDTH 50
#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++) {
size_t current_len = displaywidth(templines[i]);
if (current_len > content_width)
content_width = current_len;
}
freelines(templines);
int bubble_width = content_width + 4;
if(bubble_width < MIN_BUBBLE_WIDTH) bubble_width = MIN_BUBBLE_WIDTH;
if(bubble_width > MAX_BUBBLE_WIDTH) bubble_width = MAX_BUBBLE_WIDTH;
char **lines = wraptext(text, bubble_width - 4, &count);
const char *top_left = "╭";
const char *top_right = "╮";
const char *bottom_left = "╰";
const char *bottom_right = "╯";
const char *horizontal = "─";
const char *vertical = "│";
char **bubblelines = calloc(count + 5, sizeof(char *));
bubblelines[0] = malloc(strlen(top_left) + strlen(horizontal) * bubble_width + strlen(top_right) + 1);
strcpy(bubblelines[0], top_left);
for (int i = 0; i < bubble_width; i++)
strcat(bubblelines[0], horizontal);
strcat(bubblelines[0], top_right);
for (int i = 0; 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;
int left_padding = padding_needed / 2;
int right_padding = padding_needed - left_padding;
bubblelines[i + 1] = malloc(strlen(vertical) * 2 + 2 + strlen(lines[i]) + padding_needed + 1);
sprintf(bubblelines[i + 1], "%s %*s%s%*s %s",vertical, left_padding, "", lines[i], right_padding, "", vertical);
}
bubblelines[count+1] = malloc(strlen(bottom_left) + strlen(horizontal) * bubble_width + strlen(bottom_right) + 1);
strcpy(bubblelines[count+1], bottom_left);
for (int i = 0; i < bubble_width; i++)
strcat(bubblelines[count+1], horizontal);
strcat(bubblelines[count+1], bottom_right);
bubblelines[count + 2] = strdup("\\");
bubblelines[count + 3] = strdup(" \\");
freelines(lines);
return bubblelines;
}
static void print_help(void){
printf("A \"cowsay\" clone with Hatsune Miku ASCII art and speech bubbles.\n\n"
"Usage: mikusays [OPTIONS] [TEXT]\n\n"
"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");
}
static void list_all_art_styles(void){
printf("Available Miku art styles:\n");
printf("==========================\n");
printf("Total styles: %zu\n", migucount);
for(size_t i = 0; i < migucount; i++){
printf("\n--- Style %zu ---\n", i);
for(int j = 0; mikuart[i][j] != NULL; j++)
printf("%s\n", mikuart[i][j]);
}
printf("\nUse --style <number> to select a specific style, or omit for random selection.\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
|| codepoint == 0x1680 || (codepoint >= 0x2000 && codepoint <= 0x200A) || codepoint == 0x2028
|| codepoint == 0x2029 || codepoint == 0x202F || codepoint == 0x205F || codepoint == 0x3000;
}
static size_t count_colored_chars(char **art){
size_t total = 0;
for(int i = 0; art[i] != NULL; i++){
const unsigned char *p = (const unsigned char *)art[i];
while(*p){
unsigned long codepoint;
p += utf8_decode(p, &codepoint);
if(!is_art_space(codepoint)) total++;
}
}
return total;
}
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;
if(colors.mode == MODE_NONE || (!rainbow && !has_solid)){
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);
return;
}
while(*p){//rainbow: one color per non-space char
unsigned long codepoint;
int len = utf8_decode(p, &codepoint);
if(is_art_space(codepoint)){
fwrite(p, 1, (size_t)len, 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);
fputs("\x1b[0m", stdout);
(*index)++;
}
p += len;
}
}
void draw_miku_says(char *text, int style){
char **speech_bubble_lines = getbubblelines(text);
if(speech_bubble_lines == NULL)
return;
char **miku_art = getmikuart(style);
int available_width = TERMINALWIDTH;
int available_height = TERMINALHEIGHT;
int max_content_width = 0;
for(int i = 0; speech_bubble_lines[i] != NULL; i++){
int width = displaywidth(speech_bubble_lines[i]);
if(width> max_content_width) max_content_width = width;
}
for(int i = 0; miku_art[i] != NULL; i++){
int width = displaywidth(miku_art[i]);
if(width > max_content_width) max_content_width = width;
}
int overall_left_padding;
if(available_width > max_content_width){
overall_left_padding = (available_width - max_content_width) / 2;
}else{
overall_left_padding = 0;
}
int bubble_width = displaywidth(speech_bubble_lines[0]);
const char *first_miku_line = miku_art[0] ? miku_art[0] : "";
int tail_pos = 0;
const char *ptr = first_miku_line;
while (*ptr == ' ') {
ptr += 1;
tail_pos++;
}
int bubble_center = bubble_width / 2;
int miku_tail_absolute_pos = overall_left_padding + tail_pos;
int bubble_left_padding;
if(miku_tail_absolute_pos >= bubble_center){
bubble_left_padding = miku_tail_absolute_pos - bubble_center;
}else{
bubble_left_padding = 0;
}
int bubble_line_count = 0;
for(int i = 0; speech_bubble_lines[i] != NULL; i++){
bubble_line_count++;
}
int art_count = 0;
while(miku_art[art_count] != NULL) art_count++;
char **all_content_lines = calloc(bubble_line_count + art_count + 1, sizeof(char *));
for(int i = 0; i < bubble_line_count; i++)
all_content_lines[i] = speech_bubble_lines[i];
for(int i = 0; i < art_count; i++)
all_content_lines[bubble_line_count + i] = miku_art[i];
int content_height = bubble_line_count + art_count;
int top_padding;
if(content_height < available_height){
top_padding = (available_height - content_height) / 2;
}else{
top_padding = 0;
}
for(int i = 0; i < top_padding; i++){
printf("\n");
}
size_t color_index = 0;
size_t colored_total = count_colored_chars(miku_art);
for(int i = 0; all_content_lines[i] != NULL; i++){
int is_bubble_line = i < bubble_line_count;
int is_tail_line = is_bubble_line && (i == bubble_line_count - 2 || i == bubble_line_count - 1);
int padding;
if(is_tail_line)
padding = overall_left_padding + tail_pos;
else if(is_bubble_line)
padding = bubble_left_padding;
else
padding = overall_left_padding;
if(is_bubble_line){
printf("%*s%s\n", padding, "", all_content_lines[i]);
}else{
printf("%*s", padding, "");
print_art_line(all_content_lines[i], &color_index, colored_total);
putchar('\n');
}
}
for(int i = 0; i < top_padding; i++){
printf("\n");
}
freelines(speech_bubble_lines);
free(all_content_lines);
}
int main(int argc, char **argv){
srand(time(NULL));
getterminalsize();
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);
if(!text) return 1;
*text = '\0';
for(int i = 1; i < argc; i++){
if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0){
print_help();
goto out;
} else if(strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--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++;
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;
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;
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;
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;
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;
goto out;
}
if(flag[2] == 's') colors.saturation = (unsigned char)v;
else colors.brightness = (unsigned char)v;
} else {
strcat(text, argv[i]);
strcat(text, " ");
}
}
if(rainbow && has_solid){
fprintf(stderr, "error: --rainbow cannot be used with --color\n");
ret = 2;
goto out;
}
if(list){
list_all_art_styles();
} else if(text[0] == '\0'){
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);
}
//i didn't know you could do labels like in assembly (nasm) this is very useful knowledge
out:
free(text);
return ret;
}