#if (is(tcc) && is(linux)) || (is(gcc) && !is(mingw)) // || is(clang)
int __argc; char **__argv;
#if !is(ems)
__attribute__((constructor)) void init_argcv(int argc, char **argv) { __argc = argc; __argv = argv; }
#endif
#endif

void argvadd(const char *arg) {
    char **argv = MALLOC( sizeof(char*) * (__argc+1) );
    for( int i = 0; i < __argc; ++i ) {
        argv[i] = __argv[i];
    }
    argv[__argc] = STRDUP(arg);
    __argv = argv;
    ++__argc;
}

const char *app_path() { // @fixme: should return absolute path always. see tcc -g -run
    static char buffer[1024] = {0};
    if( buffer[0] ) return buffer;
#if is(win32)
    unsigned length = GetModuleFileNameA(NULL, buffer, sizeof(buffer)); // @todo: use GetModuleFileNameW+wchar_t && convert to utf8 instead
    char *a = strrchr(buffer, '/');  if(!a) a = buffer + strlen(buffer);
    char *b = strrchr(buffer, '\\'); if(!b) b = buffer + strlen(buffer);
    char slash = (a < b ? *a : b < a ? *b : '/');
    snprintf(buffer, 1024, "%.*s%c", length - (int)(a < b ? b - a : a - b), buffer, slash);
    if( strendi(buffer, "tools\\tcc\\") ) { // fix tcc -g -run case. @fixme: use TOOLS instead
        strcat(buffer, "..\\..\\");
    }
#else // #elif is(linux)
    char path[32] = {0};
    sprintf(path, "/proc/%d/exe", getpid());
    readlink(path, buffer, sizeof(buffer));
    if(strrchr(buffer,'/')) 1[strrchr(buffer,'/')] = '\0';
#endif
    return buffer;
}

const char *app_temp() {
    static char buffer[256] = {0};
    if( !buffer[0] ) {
        snprintf(buffer, 256, "%s", ifdef(win32, getenv("TEMP"), P_tmpdir));
        for( int i = 0; buffer[i]; ++i ) if( buffer[i] == '\\' ) buffer[i] = '/';
        if(buffer[strlen(buffer)-1] != '/') strcat(buffer, "/");
    }
    return buffer;
}

/*
    bool exporting_dll = !strcmp(STRINGIZE(API), STRINGIZE(EXPORT));
    bool importing_dll = !strcmp(STRINGIZE(API), STRINGIZE(IMPORT));
    else static_build
*/

#ifndef APP_NAME
#define APP_NAME ifdef(ems, "", (__argv ? __argv[0] : ""))
#endif

const char *app_name() {
    static char buffer[256] = {0};
    if( !buffer[0] ) {
        char s[256];
        strncpy(s, APP_NAME, 256);
        char *a = strrchr(s, '/');
        char *b = strrchr(s, '\\');
        strncpy(buffer, a > b ? a+1 : b > a ? b+1 : s, 256);
        if(strendi(buffer, ".exe")) buffer[strlen(buffer) - 4] = 0;
    }
    return buffer;
}

const char *app_cmdline() {
    static char *cmdline = 0;
    if( !cmdline ) {
        if( argc() <= 1 ) strcatf(&cmdline, "%s", " ");
        for( int i = 1; i < argc(); ++i ) strcatf(&cmdline, " %s", argv(i));
    }
    return cmdline+1;
}

const char *app_cache() {
    static char buffer[256] = {0};
    if( !buffer[0] ) {

        #if is(osx)
        snprintf(buffer, 256, "~/Library/Caches/%s/", app_name()); // getenv("user.home")
        #elif is(win32) // APPDATA for roaming?
        snprintf(buffer, 256, "%s\\%s\\", getenv("LOCALAPPDATA"), app_name()); // getenv("LOCALAPPDATA")
        #else // linux
        snprintf(buffer, 256, "~/.cache/%s/", app_name()); // getenv("user.home")
        #endif

        mkdir(buffer, 0777);

        for( int i = 0; buffer[i]; ++i ) if( buffer[i] == '\\' ) buffer[i] = '/';
    }

    return buffer;
}

const char * app_exec( const char *cmd ) {
    static __thread char output[4096+16] = {0};
    char *buf = output + 16; buf[0] = 0; // memset(buf, 0, 4096);

    if( !cmd[0] ) return "0               ";
    cmd = file_normalize(cmd);

    int rc = -1;

    // pick the fastest code path per platform
#if is(osx)
    for( FILE *fp = popen( cmd, "r" ); fp; rc = pclose(fp), fp = 0) {
        // while( fgets(buf, 4096 - 1, fp) ) {}
    }
    // if( rc != 0 ) {
    //     char *r = strrchr(buf, '\r'); if(r) *r = 0;
    //     char *n = strrchr(buf, '\n'); if(n) *n = 0;
    // }
#elif is(win32)
    STARTUPINFOA si = {0}; si.cb = sizeof(si);
    PROCESS_INFORMATION pi = {0};

    snprintf(output+16, 4096, "cmd /c \"%s\"", cmd);

    int prio = //strstr(cmd, "ffmpeg") || strstr(cmd, "furnace") || strstr(cmd, "ass2iqe") ?
    REALTIME_PRIORITY_CLASS; //: 0;

//prio |= DETACHED_PROCESS;
//si.dwFlags = STARTF_USESTDHANDLES;

    if( CreateProcessA(
        NULL, output+16, // cmdline
        NULL,
        NULL,
        FALSE, // FALSE: dont inherit handles
        prio /*CREATE_DEFAULT_ERROR_MODE|CREATE_NO_WINDOW*/, // 0|HIGH_PRIORITY_CLASS
        NULL, // "", // NULL would inherit env
        NULL, // current dir
        &si, &pi) )
    {
        // Wait for process
        DWORD dwExitCode2 = WaitForSingleObject(pi.hProcess, INFINITE);
        DWORD dwExitCode; GetExitCodeProcess(pi.hProcess, &dwExitCode);
        rc = dwExitCode;
    }
    else
    {
        // CreateProcess() failed
        rc = GetLastError();
    }
#else
    rc = system(cmd);
#endif

    return snprintf(output, 16, "%-15d", rc), buf[-1] = ' ', output;
}

int app_spawn( const char *cmd ) {
    if( !cmd[0] ) return false;
    cmd = file_normalize(cmd);

#if _WIN32
    bool ok = WinExec(va("cmd /c \"%s\"", cmd), SW_HIDE) > 31;
#else
    bool ok = system(va("%s &", cmd)) == 0;
#endif

    return ok;
}

#if is(osx)
#include <execinfo.h> // backtrace, backtrace_symbols
#include <dlfcn.h>    // dladdr, Dl_info
#elif is(gcc) && !is(ems) && !is(mingw) // maybe is(linux) is enough?
#include <execinfo.h>  // backtrace, backtrace_symbols
#elif is(win32) // && !defined __TINYC__
#include <winsock2.h>  // windows.h alternative
#include <dbghelp.h>
#pragma comment(lib, "DbgHelp")
#pragma comment(lib, "Kernel32")
static int backtrace( void **addr, int maxtraces ) {
    static bool init = 0;
    do_once SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_INCLUDE_32BIT_MODULES);
    do_once init = SymInitialize(GetCurrentProcess(), NULL, TRUE);
    if(!init) return 0; // error: cannot initialize DbgHelp.lib

    //typedef USHORT (WINAPI *pFN)(__in ULONG, __in ULONG, __out PVOID*, __out_opt PULONG); // _MSC_VER
    typedef USHORT (WINAPI *pFN)(); // TINYC
    static pFN rtlCaptureStackBackTrace = 0;
    if( !rtlCaptureStackBackTrace ) {
        rtlCaptureStackBackTrace = (pFN)GetProcAddress(LoadLibraryA("kernel32.dll"), "RtlCaptureStackBackTrace");
    }
    if( !rtlCaptureStackBackTrace ) {
        return 0;
    }
    return rtlCaptureStackBackTrace(1, maxtraces, (PVOID *)addr, (DWORD *) 0);
}
static char **backtrace_symbols(void *const *list,int size) {
    HANDLE process = GetCurrentProcess();

    struct symbol_t {
        SYMBOL_INFO info;
        TCHAR symbolname[256], terminator;
    } si = { {0} };
    si.info.SizeOfStruct = sizeof(SYMBOL_INFO);
    si.info.MaxNameLen = sizeof(si.symbolname) / sizeof(TCHAR); // number of chars, not bytes

    IMAGEHLP_LINE l64 = { 0 };
    l64.SizeOfStruct = sizeof(IMAGEHLP_LINE);

    static __thread char **symbols = 0; //[32][64] = {0};
    if( !symbols ) {
        symbols = SYS_MEM_REALLOC(0, 128 * sizeof(char*));
        for( int i = 0; i < 128; ++i) symbols[i] = SYS_MEM_REALLOC(0, 128 * sizeof(char));
    }

    if(size > 128) size = 128;
    for( int i = 0; i < size; ++i ) {

        char *ptr = symbols[i];
        *ptr = '\0';

        if (SymFromAddr(process, (DWORD64)(uintptr_t)list[i], 0, &si.info)) {
            //char undecorated[1024];
            //UnDecorateSymbolName(si.info.Name, undecorated, sizeof(undecorated)-1, UNDNAME_COMPLETE);
            char* undecorated = (char*)si.info.Name;
            ptr += snprintf(ptr, 128, "%s", undecorated);
        } else {
            ptr += snprintf(ptr, 128, "%s", "(?""?)");
        }

        DWORD dw = 0;
        if (SymGetLineFromAddr(process, (DWORD64)(uintptr_t)list[i], &dw, &l64)) {
            ptr += snprintf(ptr, 128 - (ptr - symbols[i]), " (%s:%u)", l64.FileName, (unsigned)l64.LineNumber);
        }
    }

    return symbols;
}
#else
static int backtrace(void **heap, int num) { return 0; }
static char **backtrace_symbols(void *const *sym,int num) { return 0; }
#endif

char *callstack( int traces ) {
    static __thread char *output = 0;
    if(!output ) output = SYS_MEM_REALLOC( 0, 128 * (64+2) );
    if( output ) output[0] = '\0';
    char *ptr = output;

    enum { skip = 1 }; /* exclude 1 trace from stack (this function) */
    enum { maxtraces = 128 };

    int inc = 1;
    if( traces < 0 ) traces = -traces, inc = -1;
    if( traces == 0 ) return "";
    if( traces > maxtraces ) traces = maxtraces;

    void* stacks[maxtraces/* + 1*/]; // = { 0 };
    traces = backtrace( stacks, traces );
    char **symbols = backtrace_symbols( stacks, traces ); // @todo: optimization: map(void*,char*) cache; and retrieve only symbols not in cache

    char demangled[1024] = "??";
    int L = 0, B = inc>0 ? skip - 1 : traces, E = inc>0 ? traces : skip - 1;
    for( int i = B; ( i += inc ) != E; ) {
#if is(linux)
        #if ENABLE_LINUX_CALLSTACKS
        // @fixme: following snippet works if compiled with '-g', albeit terribly slow
        // should concat addresses into a multi-address line

        char *binary = symbols[i];
        char *address = strchr( symbols[i], '(' ) + 1;
        *strrchr( address, ')') = '\0'; *(address - 1) = '\0';

        for( FILE *fp = popen(va("addr2line -e %s %s", binary, address), "r" ); fp ; pclose(fp), fp = 0 ) { //addr2line -e binary -f -C address
            fgets(demangled, sizeof(demangled), fp);
            int len = strlen(demangled); while( len > 0 && demangled[len-1] < 32 ) demangled[--len] = 0;
        }
        symbols[i] = demangled;
        #else
        // make it shorter. ie, `0x00558997ccc87e ./a.out(+0x20187e) [0x00558997ccc87e]`
        strchr(symbols[i], ')')[1] = '\0';
        #endif
#elif is(osx)
        /*struct*/ Dl_info info;
        if( dladdr(stacks[i], &info) && info.dli_sname ) {
            const char *dmgbuf = info.dli_sname[0] != '_' ? NULL :
                 ifdef(cpp, __cxa_demangle(info.dli_sname, NULL, 0, NULL), info.dli_sname);
            strcpy( demangled, dmgbuf ? dmgbuf : info.dli_sname );
            symbols[i] = demangled;
            if( dmgbuf ) free( (void*)dmgbuf );
        }
#endif
        ptr += sprintf(ptr, "%03d: %#016llx %s\n", ++L, (unsigned long long)(uintptr_t)stacks[i], symbols[i]); // format gymnastics because %p is not standard when printing pointers
    }

#if is(linux) || is(osx)
     if(symbols) free(symbols);
#endif

     return output ? output : "";
}

int callstackf( FILE *fp, int traces ) {
    char *buf = callstack(traces);
    fputs(buf, fp);
    return 0;
}

// trap signals ---------------------------------------------------------------

const char *trap_name(int signal) {
    if(signal == SIGABRT) return "SIGABRT - \"abort\", abnormal termination";
    if(signal == SIGFPE) return "SIGFPE - floating point exception";
    if(signal == SIGILL) return "SIGILL - \"illegal\", invalid instruction";
    if(signal == SIGSEGV) return "SIGSEGV - \"segmentation violation\", invalid memory access";
    if(signal == SIGINT) return "SIGINT - \"interrupt\", interactive attention request sent to the program";
    if(signal == SIGTERM) return "SIGTERM - \"terminate\", termination request sent to the program";
    ifndef(win32, if(signal == SIGBUS) return "SIGBUS");
    ifdef(linux, if(signal == SIGSTKFLT) return "SIGSTKFLT");
    ifndef(win32, if(signal == SIGQUIT) return "SIGQUIT");
    return "??";
}
void trap_on_ignore(int sgn) {
    signal(sgn, trap_on_ignore);
}
void trap_on_quit(int sgn) {
    signal(sgn, trap_on_quit);
    exit(0);
}
void trap_on_abort(int sgn) {
    char *cs = va("Error: unexpected signal %s (%d)\n%s", trap_name(sgn), sgn, callstack(+16));
    fprintf(stderr, "%s\n", cs), alert(cs), breakpoint();
    signal(sgn, trap_on_abort);
    exit(-sgn);
}
void trap_on_debug(int sgn) { // @todo: rename to trap_on_choice() and ask the developer what to do next? abort, continue, debug
    char *cs = va("Error: unexpected signal %s (%d)\n%s", trap_name(sgn), sgn, callstack(+16));
    fprintf(stderr, "%s\n", cs), alert(cs), breakpoint();
    signal(sgn, trap_on_debug);
}
#if is(win32)
LONG WINAPI trap_on_SEH(PEXCEPTION_POINTERS pExceptionPtrs) {
    char *cs = va("Error: unexpected SEH exception\n%s", callstack(+16));
    fprintf(stderr, "%s\n", cs), alert(cs), breakpoint();
    return EXCEPTION_EXECUTE_HANDLER; // Execute default exception handler next
}
#endif
void trap_install(void) {
    // expected signals
    signal(SIGINT, trap_on_quit);
    signal(SIGTERM, trap_on_quit);
    ifndef(win32, signal(SIGQUIT, trap_on_quit));
    // unexpected signals
    signal(SIGABRT, trap_on_abort);
    signal(SIGFPE, trap_on_abort);
    signal(SIGILL, trap_on_abort);
    signal(SIGSEGV, trap_on_abort);
    ifndef(win32, signal(SIGBUS, trap_on_abort));
    ifdef(linux, signal(SIGSTKFLT, trap_on_abort));
    // others
    ifdef(win32,SetUnhandledExceptionFilter(trap_on_SEH));
}

#ifdef TRAP_DEMO
AUTORUN {
    trap_install();
    app_crash(); // app_hang();
}
#endif

// cpu -------------------------------------------------------------------------

#if is(linux)
#include <sched.h>
#endif

int app_cores() {
#if is(win32)
    DWORD_PTR pm, sm;
    if( GetProcessAffinityMask(GetCurrentProcess(), &pm, &sm) ) if( pm ) {
        int count = 0;
        while( pm ) {
            ++count;
            pm &= pm - 1;
        }
        return count;
    }
    { SYSTEM_INFO si; GetSystemInfo(&si); return (int)si.dwNumberOfProcessors; }
#else // unix
    int count = sysconf(_SC_NPROCESSORS_ONLN);
    return count > 0 ? count : 1;
#endif
#if 0
#elif is(linux)
    cpu_set_t prevmask, testmask;
    CPU_ZERO(&prevmask);
    CPU_ZERO(&testmask);
    sched_getaffinity(0, sizeof(prevmask), &prevmask);     //Get current mask
    sched_setaffinity(0, sizeof(testmask), &testmask);     //Set zero mask
    sched_getaffinity(0, sizeof(testmask), &testmask);     //Get mask for all CPUs
    sched_setaffinity(0, sizeof(prevmask), &prevmask);     //Reset current mask
    int num = CPU_COUNT(&testmask);
    return (num > 1 ? num : 1);
#elif is(cpp)
    return (int)std::thread::hardware_concurrency();
#elif defined(_OPENMP)
    // omp
    int cores = 0;
    #pragma omp parallel
    {
        #pragma omp atomic
        ++cores;
    }
    return cores;
#endif
}

// -----------------------------------------------------------------------------
// Battery API. Based on code by Rabia Alhaffar (UNLICENSE)
// - rlyeh, public domain.

#if is(win32)
#include <winsock2.h>

int app_battery() {
    SYSTEM_POWER_STATUS ibstatus;

    if (GetSystemPowerStatus(&ibstatus) == FALSE) {
        return 0;
    }

    int level = (ibstatus.BatteryLifePercent != 255) ? ibstatus.BatteryLifePercent : 0;
    int charging = (ibstatus.BatteryFlag & 8) > 0;
    return charging ? +level : -level;
}

#elif defined __linux__ // is(linux)
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

int app_battery() {
    static int battery_status_handle;
    static int battery_capacity_handle;

    do_once {
        battery_status_handle = open("/sys/class/power_supply/BAT0/status", O_RDONLY);
        battery_capacity_handle = open("/sys/class/power_supply/BAT0/capacity", O_RDONLY);
    }

    if (battery_status_handle == -1 || battery_capacity_handle == -1) {
        return 0;
    }

    char buffer[512];

    // level
    lseek(battery_capacity_handle, 0, SEEK_SET);
    int readlen = read(battery_capacity_handle, buffer, 511); buffer[readlen < 0 ? 0 : readlen] = '\0';
    int level = atoi(buffer);

    // charging
    lseek(battery_status_handle, 0, SEEK_SET);
    readlen = read(battery_status_handle, buffer, 511); buffer[readlen < 0 ? 0 : readlen] = '\0';
    int charging = strstr(buffer, "Discharging") ? 0 : 1;
    return charging ? +level : -level;
}

#elif is(osx)
#import <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>
#import <IOKit/ps/IOPowerSources.h>
#import <IOKit/ps/IOPSKeys.h>

int app_battery() {
    static CFDictionaryRef psrc;

    do_once {
        CFTypeRef blob = IOPSCopyPowerSourcesInfo();
        CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
        int sourcesCount = CFArrayGetCount(sources);

        if (sourcesCount > 0) {
            psrc = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, 0));
        }
    }

    if(psrc == NULL) return 0;

    int cur_cap = 0;
    CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(psrc, CFSTR(kIOPSCurrentCapacityKey)), kCFNumberSInt32Type, &cur_cap);

    int max_cap = 0;
    CFNumberGetValue((CFNumberRef)CFDictionaryGetValue(psrc, CFSTR(kIOPSMaxCapacityKey)), kCFNumberSInt32Type, &max_cap);

    int level = (int)(cur_cap * 100.f / max_cap);
    int charging = CFDictionaryGetValue(psrc, CFSTR(kIOPSIsChargingKey)) == kCFBooleanTrue;
    return charging ? +level : -level;
}

#else

int app_battery() {
    return 0;
}

#endif

// ----------------------------------------------------------------------------
// argc/v

static void argc_init() {
#if is(tcc) && is(linux)
    do_once {
        char buffer[128], arg0[128] = {0};
        for( FILE *fp = fopen("/proc/self/status", "rb"); fp; fclose(fp), fp = 0) {
            while( fgets(buffer, 128, fp) ) {
                if( strbeg(buffer, "Name:") ) {
                    sscanf(buffer + 5, "%s", arg0 );
                    break;
                }
            }
        }
        extern char **environ;
        __argv = environ - 2; // last argv, as stack is [argc][argv0][argv1][...][NULL][envp]
        while( !strend(*__argv,arg0) ) --__argv;
        __argc = *(int*)(__argv-1);
    }
#endif
}

int argc() {
    do_once argc_init();
    return __argc;
}
char* argv(int arg) {
    do_once argc_init();
    static __thread char empty[1];
    return (unsigned)arg < __argc ? __argv[arg] : (empty[0] = '\0', empty);
}

// ----------------------------------------------------------------------------
// options

int flag(const char *commalist) {
    while( commalist[0] ) {
        const char *begin = commalist;
        while(*commalist != ',' && *commalist != '\0') ++commalist;
        const char *end = commalist;

        char token[128];
        snprintf(token,   128, "%.*s",  (int)(end - begin), begin);

        for( int i = 1; i < argc(); ++i ) {
            char *arg = argv(i);

            if( !strcmpi( arg, token ) ) {  // --arg
                return 1;
            }
        }

        commalist = end + !!end[0];
    }
    return 0;
}

const char *option(const char *commalist, const char *defaults) {
    while( commalist[0] ) {
        const char *begin = commalist;
        while(*commalist != ',' && *commalist != '\0') ++commalist;
        const char *end = commalist;

        char token[128], tokeneq[128];
        snprintf(token,   128, "%.*s",  (int)(end - begin), begin);
        snprintf(tokeneq, 128, "%.*s=", (int)(end - begin), begin);

        for( int i = 1; i < argc(); ++i ) {
            char *arg = argv(i);

            if( strbegi( arg, tokeneq ) ) { // --arg=value
                return argv(i) + strlen(tokeneq);
            }
            if( !strcmpi( arg, token ) ) {  // --arg value
                if( (i+1) < argc() ) {
                    return argv(i+1);
                }
            }
        }

        commalist = end + !!end[0];
    }
    return defaults;
}

int optioni(const char *commalist, int defaults) {
    const char *rc = option(commalist, 0);
    return rc ? atoi(rc) : defaults;
}
float optionf(const char *commalist, float defaults) {
    const char *rc = option(commalist, 0);
    return rc ? atof(rc) : defaults;
}

// ----------------------------------------------------------------------------
// tty

void tty_color(unsigned color) {
    #if is(win32)
    do_once {
        DWORD mode = 0; SetConsoleMode(GetStdHandle(-11), (GetConsoleMode(GetStdHandle(-11), &mode), mode|4));
    }
    #endif
    if( color ) {
        // if( color == RED ) alert("break on error message (RED)"), breakpoint(); // debug
        unsigned r = (color >>  0) & 255;
        unsigned g = (color >>  8) & 255;
        unsigned b = (color >> 16) & 255;
        // 24-bit console ESC[ … 38;2;<r>;<g>;<b> … m Select RGB foreground color
        // 256-color console ESC[38;5;<fgcode>m
        // 0x00-0x07:  standard colors (as in ESC [ 30..37 m)
        // 0x08-0x0F:  high intensity colors (as in ESC [ 90..97 m)
        // 0x10-0xE7:  6*6*6=216 colors: 16 + 36*r + 6*g + b (0≤r,g,b≤5)
        // 0xE8-0xFF:  grayscale from black to white in 24 steps
        r /= 51, g /= 51, b /= 51; // [0..5]
        printf("\033[38;5;%dm", r*36+g*6+b+16); // "\033[0;3%sm", color_code);
    } else {
        printf("%s", "\x1B[39;49m"); // reset
    }
}
void tty_puts(unsigned color, const char *text) {
    tty_color(color); puts(text);
}
void tty_init() {
    tty_color(0);
}
int tty_cols() {
#if is(win32)
    CONSOLE_SCREEN_BUFFER_INFO c;
    if( GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &c) ) {
        int w = c.srWindow.Right-c.srWindow.Left-c.dwCursorPosition.X;
        return w > 2 ? w - 1 : w; // w-1 to allow window resizing to a larger dimension (already printed text would break otherwise)
    }
#endif
#ifdef TIOCGWINSZ
    struct winsize ws;
    ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
    return ws.ws_col - 1;
#endif
#ifdef TIOCGSIZE
    struct ttysize ts;
    ioctl(STDIN_FILENO, TIOCGSIZE, &ts);
    return ts.ts_cols - 1;
#endif
    return 80;
}
void tty_detach() {
    ifdef(win32, FreeConsole());
}
void tty_attach() {
#if is(win32)
    // in order to have a Windows gui application with console:
    // - use WinMain() then AllocConsole(), but that may require supporintg different entry points for different platforms.
    // - /link /SUBSYSTEM:CONSOLE and then call FreeConsole() if no console is needed, but feels naive to flash the terminal for a second.
    // - /link /SUBSYSTEM:WINDOWS /entry:mainCRTStartup, then AllocConsole() as follows. Quoting @pmttavara:
    //   "following calls are the closest i'm aware you can get to /SUBSYSTEM:CONSOLE in a gui program
    //   while cleanly handling existing consoles (cmd.exe), pipes (ninja) and no console (VS/RemedyBG; double-clicking the game)"
    do_once {
        if( !AttachConsole(ATTACH_PARENT_PROCESS) && GetLastError() != ERROR_ACCESS_DENIED ) { bool ok = !!AllocConsole(); ASSERT( ok ); }
        printf("\n"); // print >= 1 byte to distinguish empty stdout from a redirected stdout (fgetpos() position <= 0)
        fpos_t pos = 0;
        if( fgetpos(stdout, &pos) != 0 || pos <= 0 ) {
            bool ok1 = !!freopen("CONIN$" , "r", stdin ); ASSERT( ok1 );
            bool ok2 = !!freopen("CONOUT$", "w", stderr); ASSERT( ok2 );
            bool ok3 = !!freopen("CONOUT$", "w", stdout); ASSERT( ok3 );
        }
    }
#endif
}

// -----------------------------------------------------------------------------
// debugger

#include <stdio.h>
void hexdumpf( FILE *fp, const void *ptr, unsigned len, int width ) {
    unsigned char *data = (unsigned char*)ptr;
    for( unsigned jt = 0; jt <= len; jt += width ) {
        fprintf( fp, "; %05d%s", jt, jt == len ? "\n" : " " );
        for( unsigned it = jt, next = it + width; it < len && it < next; ++it ) {
            fprintf( fp, "%02x %s", (unsigned char)data[it], &" \n\0...\n"[ (1+it) < len ? 2 * !!((1+it) % width) : 3 ] );
        }
        fprintf( fp, "; %05d%s", jt, jt == len ? "\n" : " " );
        for( unsigned it = jt, next = it + width; it < len && it < next; ++it ) {
            fprintf( fp, " %c %s", (signed char)data[it] >= 32 ? (signed char)data[it] : (signed char)'.', &" \n\0..."[ (1+it) < len ? 2 * !!((1+it) % width) : 3 ] );
        }
    }
    fprintf(fp, " %d bytes\n", len);
}
void hexdump( const void *ptr, unsigned len ) {
    hexdumpf( stdout, ptr, len, 16 );
}

#if 0 // is(cl) only
static void debugbreak(void) {
    do { \
        __try { DebugBreak(); } \
        __except (GetExceptionCode() == EXCEPTION_BREAKPOINT ? \
            EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {} \
    } while(0);
}
#endif

#if is(win32)
static void debugbreak(void) { if(IsDebuggerPresent()) DebugBreak(); }
#else // is(unix)
static int is_debugger_present = -1;
static void _sigtrap_handler(int signum) {
    is_debugger_present = 0;
    signal(SIGTRAP, SIG_DFL);
}
static void debugbreak(void) { // break if debugger present
    // __builtin_trap(); //
    //raise(SIGABRT); // SIGTRAP);
    //__asm__ volatile("int $0x03");
    if( is_debugger_present < 0 ) {
        is_debugger_present = 1;
        signal(SIGTRAP, _sigtrap_handler);
        raise(SIGTRAP);
    }
}
#endif

void alert(const char *message) { // @todo: move to app_, besides die()
    window_visible(false);
    message = message[0] == '!' ? (const char*)va("%s\n%s", message+1, callstack(+48)) : message;

#if is(win32)
    MessageBoxA(0, message, 0,0);
#elif is(ems)
    emscripten_run_script(va("alert('%s')", message));
#elif is(linux)
    for(FILE *fp = fopen("/tmp/v4k.warning","wb");fp;fp=0)
    fputs(message,fp), fclose(fp), system("xmessage -center -file /tmp/v4k.warning");
#elif is(osx)
    system(va("osascript -e 'display alert \"Alert\" message \"%s\"'", message));
#endif

    window_visible(true);
}

void breakpoint() {
    debugbreak();
}

bool has_debugger() {
#if is(win32)
    return IsDebuggerPresent(); // SetLastError(123); OutputDebugStringA("\1"); enabled = GetLastError() != 123;
#else
    return false;
#endif
}

void die(const char *message) {
   fprintf(stderr, "%s\n", message);
   fflush(stderr);
   alert(message);
   exit(-1);
}

// ----------------------------------------------------------------------------
// logger

//static int __thread _thread_id;
//#define PRINTF(...)      (printf("%03d %07.3fs|%-16s|", (((unsigned)(uintptr_t)&_thread_id)>>8) % 1000, time_ss(), __FUNCTION__), printf(__VA_ARGS__), printf("%s", 1[#__VA_ARGS__] == '!' ? callstack(+48) : "")) // verbose logger

int (PRINTF)(const char *text, const char *stack, const char *file, int line, const char *function) {
    double secs = time_ss();
    uint32_t color = 0;
    /**/ if( strstri(text, "fail") || strstri(text, "error") ) color = RED;
    else if( strstri(text, "warn") || strstri(text, "not found") ) color = YELLOW;
    #if is(cl)
    char *slash = strrchr(file, '\\'); if(slash) file = slash + 1;
    #endif
    char *location = va("|%s|%s:%d", /*errno?strerror(errno):*/function, file, line);
    int cols = tty_cols() + 1 - (int)strlen(location);

    flockfile(stdout);

    tty_color(color);
    printf("\r%*.s%s", cols, "", location);
    printf("\r%07.3fs|%s%s", secs, text, stack);
    tty_color(0);

    funlockfile(stdout);

    return 1;
}

// ----------------------------------------------------------------------------
// panic

static void *panic_oom_reserve; // for out-of-memory recovery
int (PANIC)(const char *error, const char *file, int line) {
    panic_oom_reserve = SYS_MEM_REALLOC(panic_oom_reserve, 0);

    tty_color(RED);

    error += error[0] == '!';
    fprintf(stderr, "Error: %s (%s:%d) (errno:%s)\n", error, file, line, strerror(errno));
    fprintf(stderr, "%s", callstack(+16)); // no \n
    fflush(0); // fflush(stderr);

    tty_color(0);

    alert(error);
    breakpoint();

    exit(-line);
    return 1;
}

// ----------------------------------------------------------------------------
// threads

struct thread_wrapper {
    int (*func)(void *user_data);
    void *user_data;
};

static
int thread_proc( void* user_data ) {
    struct thread_wrapper *w = (struct thread_wrapper*)user_data;
    int return_code = w->func( w->user_data );
    thread_exit( return_code );
    FREE(w);
    return 0;
}

void* thread( int (*thread_func)(void* user_data), void* user_data ) {
    struct thread_wrapper *w = MALLOC(sizeof(struct thread_wrapper));
    w->func = thread_func;
    w->user_data = user_data;

    int thread_stack_size = 0;
    const char *thread_name = "";
    thread_ptr_t thd = thread_init( thread_proc, w, thread_name, thread_stack_size );
    return thd;
}
void thread_destroy( void *thd ) {
    int rc = thread_join(thd);
    thread_term(thd);
}

void app_hang() {
    for(;;);
}
void app_crash() {
    volatile int *p = 0;
    *p = 42;
}
void app_beep() {
    ifdef(win32, app_spawn("rundll32 user32.dll,MessageBeep"); return; );
    ifdef(linux, app_spawn("paplay /usr/share/sounds/freedesktop/stereo/message.oga"); return; );
    ifdef(osx,   app_spawn("tput bel"); return; );

    //fallback:
    fputc('\x7', stdout);

    // win32:
    // rundll32 user32.dll,MessageBeep ; ok
    // rundll32 cmdext.dll,MessageBeepStub ; ok

    // osx:
    // tput bel
    // say "beep"
    // osascript -e 'beep'
    // osascript -e "beep 1"
    // afplay /System/Library/Sounds/Ping.aiff
    // /usr/bin/printf "\a"

    // linux:
    // paplay /usr/share/sounds/freedesktop/stereo/message.oga ; ok
    // paplay /usr/share/sounds/freedesktop/stereo/complete.oga ; ok
    // paplay /usr/share/sounds/freedesktop/stereo/bell.oga ; ok
    // beep ; apt-get
    // echo -e '\007' ; mute
    // echo -e "\007" >/dev/tty10 ; sudo
    // tput bel ; mute
}

void app_singleton(const char *guid) {
    #ifdef _WIN32
    do_once {
        char buffer[128];
        snprintf(buffer, 128, "Global\\{%s}", guid);
        static HANDLE app_mutex = 0;
        app_mutex = CreateMutexA(NULL, FALSE, buffer);
        if( ERROR_ALREADY_EXISTS == GetLastError() ) {
            exit(-1);
        }
    }
    #endif
}

#ifdef APP_SINGLETON_GUID
AUTORUN { app_singleton(APP_SINGLETON_GUID); }
#endif

static
bool app_open_folder(const char *file) {
    char buf[1024];
#ifdef _WIN32
    snprintf(buf, sizeof(buf), "start \"\" \"%s\"", file);
#elif __APPLE__
    snprintf(buf, sizeof(buf), "%s \"%s\"", file_directory(file) ? "open" : "open --reveal", file);
#else
    snprintf(buf, sizeof(buf), "xdg-open \"%s\"", file);
#endif
    return app_spawn(buf);
}

static
bool app_open_file(const char *file) {
    char buf[1024];
#ifdef _WIN32
    snprintf(buf, sizeof(buf), "start \"\" \"%s\"", file);
#elif __APPLE__
    snprintf(buf, sizeof(buf), "open \"%s\"", file);
#else
    snprintf(buf, sizeof(buf), "xdg-open \"%s\"", file);
#endif
    return app_spawn(buf);
}

static
bool app_open_url(const char *url) {
    return app_open_file(url);
}

bool app_open(const char *link) {
    if( file_directory(link) ) return app_open_folder(link);
    if( file_exist(link) ) return app_open_file(link);
    return app_open_url(link);
}

const char* app_loadfile() {
    const char *windowTitle = NULL;
    const char *defaultPathFile = NULL;
    const char *filterHints = NULL; // "image files"
    const char *filters[] = { "*.*" };
    int allowMultipleSelections = 0;

    tinyfd_assumeGraphicDisplay = 1;
    return tinyfd_openFileDialog( windowTitle, defaultPathFile, countof(filters), filters, filterHints, allowMultipleSelections );
}
const char* app_savefile() {
    const char *windowTitle = NULL;
    const char *defaultPathFile = NULL;
    const char *filterHints = NULL; // "image files"
    const char *filters[] = { "*.*" };

    tinyfd_assumeGraphicDisplay = 1;
    return tinyfd_saveFileDialog( windowTitle, defaultPathFile, countof(filters), filters, filterHints );
}

// ----------------------------------------------------------------------------
// tests

static __thread int test_oks, test_errors, test_once;
static void test_exit(void) { fprintf(stderr, "%d/%d tests passed\n", test_oks, test_oks+test_errors); }
int (test)(const char *file, int line, const char *expr, bool result) {
    static int breakon = -1; if(breakon<0) breakon = optioni("--test-break", 0);
    if( breakon == (test_oks+test_errors+1) ) alert("user requested to break on this test"), breakpoint();
    test_once = test_once || !(atexit)(test_exit);
    test_oks += result, test_errors += !result;
    return (result || (tty_color(RED), fprintf(stderr, "(Test `%s` failed %s:%d)\n", expr, file, line), tty_color(0), 0) );
}