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
455 lines
15 KiB
C
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;
|
|
}
|