#include #include #include #include /* Copyright (c) 2021-2026 Devine Lu Linvega, Andrew Alderwick, Andrew Richards, Eiríkr Åsheim, Sigrid Solveig Haflínudóttir Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE. cc -DNDEBUG -O2 -g0 -s src/uxncli.c -o bin/uxncli */ typedef signed char Sint8; typedef unsigned char Uint8; typedef unsigned short Uint16; typedef void (*deo_handler)(void); typedef Uint8 (*dei_handler)(void); static Uint8 *ram, dev[0x100], ptr[2], stk[2][0x100]; static unsigned int uxn_eval(Uint16 pc); static int rL1, rL2; /* clang-format off */ #define BANKS 0x10 #define BANKS_CAP (BANKS * 0x10000) static inline Uint16 peek2(const Uint8 *d) { return ((Uint16)d[0] << 8) | d[1]; } static inline void poke2(Uint8 *d, Uint16 v) { d[0] = v >> 8, d[1] = v; } /* clang-format on */ /* @|System ------------------------------------------------------------ */ static char *system_boot_path; static void system_print(char *name, int r) { Uint8 i; fprintf(stderr, "%s%c", name, ptr[r] - 8 ? ' ' : '|'); for(i = ptr[r] - 8; i != ptr[r]; i++) fprintf(stderr, "%02x%c", stk[r][i], i == 0xff ? '|' : ' '); fprintf(stderr, "<%02x\n", ptr[r]); } static unsigned int system_load(const char *rom_path) { FILE *f = fopen(rom_path, "rb"); if(f) { unsigned int i = 0, l = fread(ram + 0x100, 0x10000 - 0x100, 1, f); while(l && ++i < BANKS) l = fread(ram + i * 0x10000, 0x10000, 1, f); fclose(f); } return !!f; } static unsigned int system_boot(char *rom_path, const unsigned int has_args) { ram = (Uint8 *)calloc(BANKS_CAP, sizeof(Uint8)); system_boot_path = rom_path; dev[0x17] = has_args; return ram && system_load(rom_path); } static void system_deo_expansion(void) { const Uint16 exp = peek2(dev + 2); Uint8 *aptr = ram + exp; Uint16 length = peek2(aptr + 1); unsigned int bank = peek2(aptr + 3) * 0x10000; unsigned int addr = peek2(aptr + 5); if(ram[exp] == 0x0) { if(bank < BANKS_CAP) memset(ram + bank + addr, ram[exp + 7], length); } else if(ram[exp] == 1 || ram[exp] == 2) { unsigned int dst_bank = peek2(aptr + 7) * 0x10000; unsigned int dst_addr = peek2(aptr + 9); if(bank < BANKS_CAP && dst_bank < BANKS_CAP) memmove(ram + dst_bank + dst_addr, ram + bank + addr, length); } else fprintf(stderr, "Unknown command: %02x\n", ram[exp]); } /* clang-format off */ static Uint8 system_dei_wst(void) { return ptr[0]; } static Uint8 system_dei_rst(void) { return ptr[1]; } static void system_deo_wst(void) { ptr[0] = dev[4]; } static void system_deo_rst(void) { ptr[1] = dev[5]; } static void system_deo_print(void) { system_print("WST", 0), system_print("RST", 1); } /* clang-format on */ /* @|Console ----------------------------------------------------------- */ #define CONSOLE_STD 0x1 #define CONSOLE_ARG 0x2 #define CONSOLE_EOA 0x3 #define CONSOLE_END 0x4 static unsigned int console_vector; static void console_input(int c, unsigned int type) { dev[0x12] = c, dev[0x17] = type; if(console_vector && !dev[0x0f]) uxn_eval(console_vector); } /* clang-format off */ static void console_deo_vector(void) { console_vector = peek2(&dev[0x10]); } static void console_deo_stdout(void) { fputc(dev[0x18], stdout), fflush(stdout); } static void console_deo_stderr(void) { fputc(dev[0x19], stderr), fflush(stderr); } static void console_deo_hb(void) { fprintf(stderr, "%02x", dev[0x1a]); } static void console_deo_lb(void) { fprintf(stderr, "%02x", dev[0x1b]); } /* clang-format on */ /* @|File -------------------------------------------------------------- */ #include #include #include typedef struct { FILE *f; DIR *dir; char *filepath; enum { IDLE, FILE_READ, FILE_WRITE, DIR_READ, DIR_WRITE } state; } UxnFile; static UxnFile ufs[2]; static void make_pathfile(char *pathbuf, const char *filepath, const char *basename) { char c = '/'; while(*filepath) c = *filepath++, *pathbuf = c, pathbuf++; if(c != '/') *pathbuf = '/', pathbuf++; while(*basename) *pathbuf = *basename++, pathbuf++; *pathbuf = 0; } static inline unsigned int put_fill(Uint8 *dest, unsigned int len, char c) { memset(dest, c, len); return len; } static inline unsigned int put_size(Uint8 *dest, unsigned int len, unsigned int size) { unsigned int i; for(i = 0, dest += len; i < len; i++, size >>= 4) *(--dest) = "0123456789abcdef"[(Uint8)(size & 0xf)]; return len; } static inline unsigned int put_text(Uint8 *dest, const char *text) { Uint8 *anchor = dest; while(*text) *dest = *text++, dest++; *dest = 0; return dest - anchor; } static unsigned int put_stat(Uint8 *dest, unsigned int len, unsigned int size, unsigned int err, unsigned int dir, unsigned int capsize) { if(err) return put_fill(dest, len, '!'); if(dir) return put_fill(dest, len, '-'); if(capsize && size >= 0x10000) return put_fill(dest, len, '?'); return put_size(dest, len, size); } static unsigned int put_statfile(Uint8 *dest, const char *filepath, const char *basename) { unsigned int err, dir; struct stat st; Uint8 *anchor = dest; char pathbuf[0x200]; make_pathfile(pathbuf, filepath, basename); err = stat(pathbuf, &st); dir = !err && S_ISDIR(st.st_mode); dest += put_stat(dest, 4, st.st_size, err, dir, 1); dest += put_text(dest, " "); dest += put_text(dest, basename); dest += put_text(dest, dir ? "/\n" : "\n"); return dest - anchor; } static unsigned int put_fdir(Uint8 *dest, unsigned int len, const char *filepath, DIR *dir) { Uint8 *p = dest, *end = dest + len - 1; struct dirent *de; while((de = readdir(dir)) && p < end) { const char *name = de->d_name; if(name[0] == '.' && (!name[1] || (name[1] == '.' && !name[2]))) continue; p += put_statfile(p, filepath, name); } *p = 0; return p - dest; } static unsigned int is_dir_path(const char *p) { const char *end = p; while(*end) end++; return end > p && end[-1] == '/'; } static unsigned int is_dir_real(char *p) { struct stat st; return stat(p, &st) == 0 && S_ISDIR(st.st_mode); } static unsigned int file_write_dir(const char *p) { char buf[0x200]; char *s = buf; unsigned int ok = 1; while(*p && ok) { *s++ = *p; if(*p++ == '/') { s[-1] = '\0'; ok = is_dir_real(buf) || mkdir(buf, 0755) == 0; s[-1] = '/'; } } return ok; } static void file_reset(unsigned int id) { if(ufs[id].f != NULL) fclose(ufs[id].f), ufs[id].f = NULL; if(ufs[id].dir != NULL) closedir(ufs[id].dir), ufs[id].dir = NULL; ufs[id].state = IDLE; } static unsigned int file_init(unsigned int id, Uint16 addr) { file_reset(id); ufs[id].filepath = (char *)&ram[addr]; return 0; } static unsigned int file_read(unsigned int id, Uint16 addr, unsigned int len) { void *dest = &ram[addr]; struct stat st; unsigned int n; if(ufs[id].filepath == 0) return 0; if(addr + len > 0x10000) len = 0x10000 - addr; if(ufs[id].state != FILE_READ && ufs[id].state != DIR_READ) { file_reset(id); if(stat(ufs[id].filepath, &st) == 0 && S_ISDIR(st.st_mode)) { if((ufs[id].dir = opendir(ufs[id].filepath)) != NULL) ufs[id].state = DIR_READ; } else if((ufs[id].f = fopen(ufs[id].filepath, "rb")) != NULL) { ufs[id].state = FILE_READ; } } if(ufs[id].state == FILE_READ) { n = fread(dest, 1, len, ufs[id].f); if(n == 0) file_reset(id); return n; } if(ufs[id].state == DIR_READ) { n = put_fdir(dest, len, ufs[id].filepath, ufs[id].dir); if(n == 0) file_reset(id); return n; } return 0; } static unsigned int file_write(unsigned int id, Uint16 addr, unsigned int len, Uint8 flags) { unsigned int ret = 0; if(ufs[id].filepath == 0) return 0; if(addr + len > 0x10000) len = 0x10000 - addr; file_write_dir(ufs[id].filepath); if(ufs[id].state != FILE_WRITE && ufs[id].state != DIR_WRITE) { file_reset(id); if(is_dir_path(ufs[id].filepath)) ufs[id].state = DIR_WRITE; else if((ufs[id].f = fopen(ufs[id].filepath, (flags & 0x01) ? "ab" : "wb")) != NULL) ufs[id].state = FILE_WRITE; } if(ufs[id].state == FILE_WRITE) if((ret = fwrite(&ram[addr], 1, len, ufs[id].f)) > 0 && fflush(ufs[id].f) != 0) ret = 0; if(ufs[id].state == DIR_WRITE) ret = is_dir_real(ufs[id].filepath); return ret; } static unsigned int file_stat(unsigned int id, Uint16 addr, unsigned int len) { unsigned int err, dir; struct stat st; if(ufs[id].filepath == 0) return 0; if(addr + len > 0x10000) len = 0x10000 - addr; err = stat(ufs[id].filepath, &st); dir = !err && S_ISDIR(st.st_mode); return put_stat(&ram[addr], len, st.st_size, err, dir, 0); } static unsigned int file_delete(unsigned int id) { if(ufs[id].filepath == 0) return 0; return !unlink(ufs[id].filepath); } /* clang-format off */ static void filea_deo_stat(void) { poke2(&dev[0xa2], file_stat(0, peek2(&dev[0xa4]), rL1)); } static void filea_deo_delete(void) { poke2(&dev[0xa2], file_delete(0)); } static void filea_deo_name(void) { poke2(&dev[0xa2], file_init(0, peek2(&dev[0xa8]))); } static void filea_deo_length(void) { rL1 = peek2(&dev[0xaa]); } static void filea_deo_read(void) { poke2(&dev[0xa2], file_read(0, peek2(&dev[0xac]), rL1)); } static void filea_deo_write(void) { poke2(&dev[0xa2], file_write(0, peek2(&dev[0xae]), rL1, dev[0xa7])); } static void fileb_deo_stat(void) { poke2(&dev[0xb2], file_stat(1, peek2(&dev[0xb4]), rL2)); } static void fileb_deo_delete(void) { poke2(&dev[0xb2], file_delete(1)); } static void fileb_deo_name(void) { poke2(&dev[0xb2], file_init(1, peek2(&dev[0xb8]))); } static void fileb_deo_length(void) { rL2 = peek2(&dev[0xba]); } static void fileb_deo_read(void) { poke2(&dev[0xb2], file_read(1, peek2(&dev[0xbc]), rL2)); } static void fileb_deo_write(void) { poke2(&dev[0xb2], file_write(1, peek2(&dev[0xbe]), rL2, dev[0xb7])); } /* clang-format on */ /* @|Datetime ---------------------------------------------------------- */ #include static int datetime_busy; static time_t datetime_seconds; static struct tm *datetime_t, datetime_zt = {0}; static void datetime_update(void) { if(!datetime_busy) { datetime_seconds = time(NULL); datetime_t = localtime(&datetime_seconds); if(datetime_t == NULL) datetime_t = &datetime_zt; datetime_busy = 1; } } /* clang-format off */ static Uint8 datetime_dei_yhb(void) { datetime_update(); return (datetime_t->tm_year + 1900) >> 8; } static Uint8 datetime_dei_ylb(void) { datetime_update(); return (datetime_t->tm_year + 1900); } static Uint8 datetime_dei_mon(void) { datetime_update(); return datetime_t->tm_mon; } static Uint8 datetime_dei_day(void) { datetime_update(); return datetime_t->tm_mday; } static Uint8 datetime_dei_hou(void) { datetime_update(); return datetime_t->tm_hour; } static Uint8 datetime_dei_min(void) { datetime_update(); return datetime_t->tm_min; } static Uint8 datetime_dei_sec(void) { datetime_update(); return datetime_t->tm_sec; } static Uint8 datetime_dei_wday(void) { datetime_update(); return datetime_t->tm_wday; } static Uint8 datetime_dei_ydayhb(void) { datetime_update(); return datetime_t->tm_yday >> 8; } static Uint8 datetime_dei_ydaylb(void) { datetime_update(); return datetime_t->tm_yday; } static Uint8 datetime_dei_dst(void) { datetime_update(); return datetime_t->tm_isdst; } /* clang-format on */ /* @|Core -------------------------------------------------------------- */ static const dei_handler dei_handlers[256] = { [0x04] = system_dei_wst, [0x05] = system_dei_rst, [0xc0] = datetime_dei_yhb, [0xc1] = datetime_dei_ylb, [0xc2] = datetime_dei_mon, [0xc3] = datetime_dei_day, [0xc4] = datetime_dei_hou, [0xc5] = datetime_dei_min, [0xc6] = datetime_dei_sec, [0xc7] = datetime_dei_wday, [0xc8] = datetime_dei_ydayhb, [0xc9] = datetime_dei_ydaylb, [0xca] = datetime_dei_dst, }; static const deo_handler deo_handlers[256] = { [0x03] = system_deo_expansion, [0x04] = system_deo_wst, [0x05] = system_deo_rst, [0x0e] = system_deo_print, [0x11] = console_deo_vector, [0x18] = console_deo_stdout, [0x19] = console_deo_stderr, [0x1a] = console_deo_hb, [0x1b] = console_deo_lb, [0xa5] = filea_deo_stat, [0xa6] = filea_deo_delete, [0xa9] = filea_deo_name, [0xab] = filea_deo_length, [0xad] = filea_deo_read, [0xaf] = filea_deo_write, [0xb5] = fileb_deo_stat, [0xb6] = fileb_deo_delete, [0xb9] = fileb_deo_name, [0xbb] = fileb_deo_length, [0xbd] = fileb_deo_read, [0xbf] = fileb_deo_write}; static inline Uint8 emu_dei(const Uint8 port) { dei_handler h = dei_handlers[port]; return h ? h() : dev[port]; } static inline void emu_deo(const Uint8 port, const Uint8 value) { deo_handler h = deo_handlers[port]; dev[port] = value; if(h) h(); } /* clang-format off */ /* @|Uxn --------------------------------------------------------------- */ #define OPC(opc, A, B) {\ case 0x00|opc: {const Uint8 d=0,r=0;A B} break;\ case 0x20|opc: {const Uint8 d=1,r=0;A B} break;\ case 0x40|opc: {const Uint8 d=0,r=1;A B} break;\ case 0x60|opc: {const Uint8 d=1,r=1;A B} break;\ case 0x80|opc: {const Uint8 d=0,r=0,k=ptr[0];A ptr[0]=k;B} break;\ case 0xa0|opc: {const Uint8 d=1,r=0,k=ptr[0];A ptr[0]=k;B} break;\ case 0xc0|opc: {const Uint8 d=0,r=1,k=ptr[1];A ptr[1]=k;B} break;\ case 0xe0|opc: {const Uint8 d=1,r=1,k=ptr[1];A ptr[1]=k;B} break;} #define DEC(m) stk[m][--ptr[m]] #define INC(m) stk[m][ptr[m]++] #define IMM a = ram[pc++] << 8, a |= ram[pc++]; #define MOV pc = d ? (Uint16)a : pc + (Sint8)a; #define POx(o,m) o = DEC(r); if(m) o |= DEC(r) << 8; #define PUx(i,m,s) if(m) c = (i), INC(s) = c >> 8, INC(s) = c; else INC(s) = i; #define GOT(o) if(d) o[1] = DEC(r); o[0] = DEC(r); #define PUT(i,s) INC(s) = i[0]; if(d) INC(s) = i[1]; #define DEO(o,v) emu_deo(o, v[0]); if(d) emu_deo(o + 1, v[1]); #define DEI(i,v) v[0] = emu_dei(i); if(d) v[1] = emu_dei(i + 1); PUT(v,r) #define POK(o,v,m) ram[o] = v[0]; if(d) ram[(o + 1) & m] = v[1]; #define PEK(i,v,m) v[0] = ram[i]; if(d) v[1] = ram[(i + 1) & m]; PUT(v,r) static unsigned int uxn_eval(Uint16 pc) { unsigned int a, b, c, x[2], y[2], z[2]; for(;;) switch(ram[pc++]) { /* BRK */ case 0x00: return 1; /* JCI */ case 0x20: if(DEC(0)) { IMM pc += a; } else pc += 2; break; /* JMI */ case 0x40: IMM pc += a; break; /* JSI */ case 0x60: IMM PUx(pc, 1, 1) pc += a; break; /* LI2 */ case 0xa0: INC(0) = ram[pc++]; /* fall-through */ /* LIT */ case 0x80: INC(0) = ram[pc++]; break; /* L2r */ case 0xe0: INC(1) = ram[pc++]; /* fall-through */ /* LIr */ case 0xc0: INC(1) = ram[pc++]; break; /* INC */ OPC(0x01,POx(a,d),PUx(a + 1,d,r)) /* POP */ OPC(0x02,ptr[r] -= 1 + d;,{}) /* NIP */ OPC(0x03,GOT(x) ptr[r] -= 1 + d;,PUT(x,r)) /* SWP */ OPC(0x04,GOT(x) GOT(y),PUT(x,r) PUT(y,r)) /* ROT */ OPC(0x05,GOT(x) GOT(y) GOT(z),PUT(y,r) PUT(x,r) PUT(z,r)) /* DUP */ OPC(0x06,GOT(x),PUT(x,r) PUT(x,r)) /* OVR */ OPC(0x07,GOT(x) GOT(y),PUT(y,r) PUT(x,r) PUT(y,r)) /* EQU */ OPC(0x08,POx(a,d) POx(b,d),PUx(b == a,0,r)) /* NEQ */ OPC(0x09,POx(a,d) POx(b,d),PUx(b != a,0,r)) /* GTH */ OPC(0x0a,POx(a,d) POx(b,d),PUx(b > a,0,r)) /* LTH */ OPC(0x0b,POx(a,d) POx(b,d),PUx(b < a,0,r)) /* JMP */ OPC(0x0c,POx(a,d),MOV) /* JCN */ OPC(0x0d,POx(a,d) POx(b,0),if(b) MOV) /* JSR */ OPC(0x0e,POx(a,d),PUx(pc,1,!r) MOV) /* STH */ OPC(0x0f,GOT(x),PUT(x,!r)) /* LDZ */ OPC(0x10,POx(a,0),PEK(a, x, 0xff)) /* STZ */ OPC(0x11,POx(a,0) GOT(y),POK(a, y, 0xff)) /* LDR */ OPC(0x12,POx(a,0),PEK(pc + (Sint8)a, x, 0xffff)) /* STR */ OPC(0x13,POx(a,0) GOT(y),POK(pc + (Sint8)a, y, 0xffff)) /* LDA */ OPC(0x14,POx(a,1),PEK(a, x, 0xffff)) /* STA */ OPC(0x15,POx(a,1) GOT(y),POK(a, y, 0xffff)) /* DEI */ OPC(0x16,POx(a,0),DEI(a, x)) /* DEO */ OPC(0x17,POx(a,0) GOT(y),DEO(a, y)) /* ADD */ OPC(0x18,POx(a,d) POx(b,d),PUx(b + a, d,r)) /* SUB */ OPC(0x19,POx(a,d) POx(b,d),PUx(b - a, d,r)) /* MUL */ OPC(0x1a,POx(a,d) POx(b,d),PUx(b * a, d,r)) /* DIV */ OPC(0x1b,POx(a,d) POx(b,d),PUx(a ? b / a : 0, d,r)) /* AND */ OPC(0x1c,POx(a,d) POx(b,d),PUx(b & a, d,r)) /* ORA */ OPC(0x1d,POx(a,d) POx(b,d),PUx(b | a, d,r)) /* EOR */ OPC(0x1e,POx(a,d) POx(b,d),PUx(b ^ a, d,r)) /* SFT */ OPC(0x1f,POx(a,0) POx(b,d),PUx(b >> (a & 0xf) << (a >> 4), d,r)) } return 0; } /* clang-format on */ int main(int argc, char **argv) { int i = 1; if(argc == 2 && argv[1][0] == '-' && argv[1][1] == 'v') return !fprintf(stdout, "Uxncli - Varvara Emulator(cli), 10 May 2026.\n"); else if(argc == 1) return !fprintf(stdout, "usage: %s [-v] file.rom [args..]\n", argv[0]); else if(!system_boot(argv[i++], argc > 2)) return !fprintf(stdout, "Could not load %s.\n", argv[i - 1]); if(uxn_eval(0x100) && console_vector) { for(; i < argc; i++) { int c; char *p = argv[i]; while(!dev[0x0f] && (c = *p++)) console_input(c, CONSOLE_ARG); console_input('\n', i == argc - 1 ? CONSOLE_END : CONSOLE_EOA); } while(!dev[0x0f]) { int c = fgetc(stdin); if(c == EOF) break; console_input(c, CONSOLE_STD); datetime_busy = 0; } console_input('\n', CONSOLE_END); } return dev[0x0f] & 0x7f; }