#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_debug1(void) { fprintf(stderr, "%02x", dev[0x1a]); } static void console_deo_debug2(void) { fprintf(stderr, "%02x%02x", dev[0x1a], dev[0x1b]); } /* clang-format on */ /* @|File -------------------------------------------------------------- */ #include #include #include typedef struct { union { FILE *f; DIR *dir; } h; char *filepath; enum { IDLE, FILE_READ, FILE_WRITE, DIR_READ, DIR_WRITE } state; } UxnFile; static UxnFile ufs[2]; static inline unsigned int put_text(Uint8 *dest, unsigned int *length, const char *text) { Uint8 *anchor = dest; while(*text && *length) { *dest = *text++, dest++; *length = *length - 1; } *dest = 0; return dest - anchor; } static inline unsigned int put_size(Uint8 *dest, unsigned int *length, unsigned int len, unsigned int size) { unsigned int i; for(i = 0, dest += len; i < len && *length; i++) { *(--dest) = "0123456789abcdef"[(Uint8)(size & 0xf)]; size >>= 4; *length = *length - 1; } return len; } static unsigned int put_fill(Uint8 *dest, unsigned int *length, unsigned int len, char c) { unsigned int i; for(i = 0; i < len && *length; i++) { *dest++ = c; *length = *length - 1; } return len; } static unsigned int put_stat(Uint8 *dest, unsigned int *length, unsigned int len, unsigned int size, unsigned int err, unsigned int dir) { unsigned int max = 1 << (len << 2); if(dir) return put_fill(dest, length, len, '-'); else if(err) return put_fill(dest, length, len, '!'); else if(size < max) return put_size(dest, length, len, size); return put_fill(dest, length, len, '?'); } static unsigned int put_statfile(Uint8 *dest, unsigned int *length, char *pathbuf, unsigned int prefix_len, unsigned int cap, const char *basename) { unsigned int err, dir; struct stat st; Uint8 *anchor = dest; char *p = pathbuf + prefix_len, *end = pathbuf + cap - 1; const char *b = basename; while(*b && p < end) *p++ = *b++; *p = 0; err = stat(pathbuf, &st); dir = !err && S_ISDIR(st.st_mode); dest += put_stat(dest, length, 4, st.st_size, err, dir); dest += put_text(dest, length, "\t"); dest += put_text(dest, length, basename); dest += put_text(dest, length, dir ? "/\n" : "\n"); return dest - anchor; } static unsigned int put_fdir(Uint8 *dest, unsigned int *length, const char *filepath, DIR *dir) { Uint8 *p = dest, *end = dest + *length - 1; struct dirent *de; char pathbuf[0x200]; unsigned int prefix_len = 0; char c = '/'; const char *fp = filepath; while(*fp && prefix_len < sizeof pathbuf - 1) c = *fp++, pathbuf[prefix_len++] = c; if(c != '/' && prefix_len < sizeof pathbuf - 1) pathbuf[prefix_len++] = '/'; 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, length, pathbuf, prefix_len, sizeof pathbuf, 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, *end = buf + sizeof buf - 1; unsigned int ok = 1; while(*p && ok && s < end) { *s++ = *p; if(*p++ == '/') { s[-1] = '\0'; ok = is_dir_real(buf) || mkdir(buf, 0755) == 0; s[-1] = '/'; } } return ok && !*p; } static void file_reset(unsigned int id) { switch(ufs[id].state) { case FILE_READ: case FILE_WRITE: fclose(ufs[id].h.f); break; case DIR_READ: closedir(ufs[id].h.dir); break; default: break; } ufs[id].state = IDLE; } static unsigned int file_open(unsigned int id, unsigned int want_write, Uint8 flags) { UxnFile *u = &ufs[id]; struct stat st; if(want_write) { if(u->state == FILE_WRITE || u->state == DIR_WRITE) return 1; file_reset(id); file_write_dir(u->filepath); if(is_dir_path(u->filepath)) { u->state = is_dir_real(u->filepath) ? DIR_WRITE : IDLE; } else if((u->h.f = fopen(u->filepath, (flags & 0x01) ? "ab" : "wb"))) { u->state = FILE_WRITE; } } else { if(u->state == FILE_READ || u->state == DIR_READ) return 1; file_reset(id); if(stat(u->filepath, &st) == 0 && S_ISDIR(st.st_mode)) { if((u->h.dir = opendir(u->filepath))) u->state = DIR_READ; } else if((u->h.f = fopen(u->filepath, "rb"))) { u->state = FILE_READ; } } return u->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]; unsigned int n, length = len; if(ufs[id].filepath == 0) return 0; if(addr + len > 0x10000) length = 0x10000 - addr; if(!file_open(id, 0, 0)) return 0; if(ufs[id].state == FILE_READ) n = fread(dest, 1, length, ufs[id].h.f); else n = put_fdir(dest, &length, ufs[id].filepath, ufs[id].h.dir); if(len > 0 && n == 0) file_reset(id); return n; } static unsigned int file_write(unsigned int id, Uint16 addr, unsigned int len, Uint8 flags) { if(ufs[id].filepath == 0) return 0; if(addr + len > 0x10000) len = 0x10000 - addr; if(!file_open(id, 1, flags)) return 0; if(ufs[id].state == FILE_WRITE) { unsigned int ret = fwrite(&ram[addr], 1, len, ufs[id].h.f); return (ret > 0 && fflush(ufs[id].h.f) != 0) ? 0 : ret; } return is_dir_real(ufs[id].filepath); } static unsigned int file_stat(unsigned int id, Uint16 addr, unsigned int len) { unsigned int err, dir; unsigned int length = len; 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], &length, len, st.st_size, err, dir); } 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_y(void) { datetime_update(); const int v = (datetime_t->tm_year + 1900); dev[0xc1] = v; return v >> 8; } 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_yday(void) { datetime_update(); const int v = datetime_t->tm_yday; dev[0xc9] = v; return v >> 8; } 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_y, [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_yday, [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_debug1, [0x1b] = console_deo_debug2, [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 Ld1(o) stk[r][(Uint8)(ptr[r]-(o))] #define Ld2(o) (Ld1(o) << 8 | stk[r][(Uint8)(ptr[r]-(o)+1)]) #define Ldx(o8,o16) d ? Ld2(o16) : Ld1(o8) #define Re1(m) ptr[r] -= m; #define Rex(m1,m2) ptr[r] -= d ? m2 : m1; #define Pu1(s,v) stk[s][ptr[s]++] = v; #define Pu2(s,v) Pu1(s,v>>8) Pu1(s,v) #define Pux(s,v) if(d) Pu1(s,(v)>>8) Pu1(s,v) #define Lda(s,o) Pu1(r,ram[o]) if(d) Pu1(r,ram[(s)(o+1)]) #define Sta(s,o,u) if(d) ram[o]=u>>8, ram[(s)(o+1)]=u; else ram[o]=u; #define Dei Pu1(r,emu_dei(x)) if(d) Pu1(r,dev[(x+1)&0xff]) #define Deo if(d) dev[x]=y>>8, emu_deo(x+1,y); else emu_deo(x,y); #define MUTE (void)x;(void)y;(void)z;(void)r;(void)d; #define OP(opc,X,Y,Z,K,P) \ case (opc)|0x00: { const int d=0,r=0,x=X,y=Y,z=Z; K P MUTE } break; \ case (opc)|0x20: { const int d=1,r=0,x=X,y=Y,z=Z; K P MUTE } break; \ case (opc)|0x40: { const int d=0,r=1,x=X,y=Y,z=Z; K P MUTE } break; \ case (opc)|0x60: { const int d=1,r=1,x=X,y=Y,z=Z; K P MUTE } break; \ case (opc)|0x80: { const int d=0,r=0,x=X,y=Y,z=Z; P MUTE } break; \ case (opc)|0xa0: { const int d=1,r=0,x=X,y=Y,z=Z; P MUTE } break; \ case (opc)|0xc0: { const int d=0,r=1,x=X,y=Y,z=Z; P MUTE } break; \ case (opc)|0xe0: { const int d=1,r=1,x=X,y=Y,z=Z; P MUTE } break; static unsigned int uxn_eval(Uint16 start_pc) { Uint16 pc = start_pc; for(;;) switch(ram[pc++]) { /* BRK */ case 0x00: return 1; /* JCI */ case 0x20: { Uint16 a=ram[pc]<<8|ram[pc+1]; pc+=2; if(stk[0][--ptr[0]]) pc+=a; } break; /* JMI */ case 0x40: { Uint16 a=ram[pc]<<8|ram[pc+1]; pc+=2+a; } break; /* JSI */ case 0x60: { Uint16 a=ram[pc]<<8|ram[pc+1]; const Uint8 d=1; pc+=2; Pux(1,pc); pc+=a; } break; /* LIT */ case 0x80: { const Uint8 r=0,d=0; Lda(Uint16,pc); pc+=1; } break; /* LI2 */ case 0xa0: { const Uint8 r=0,d=1; Lda(Uint16,pc); pc+=2; } break; /* LIr */ case 0xc0: { const Uint8 r=1,d=0; Lda(Uint16,pc); pc+=1; } break; /* L2r */ case 0xe0: { const Uint8 r=1,d=1; Lda(Uint16,pc); pc+=2; } break; /* INC */ OP(0x01, Ldx(1,2),0, 0, Rex(1,2),Pux(r,x+1)) /* POP */ OP(0x02, 0, 0, 0, Rex(1,2),{}) /* NIP */ OP(0x03, Ldx(1,2),0, 0, Rex(2,4),Pux(r,x)) /* SWP */ OP(0x04, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pux(r,x) Pux(r,y)) /* ROT */ OP(0x05, Ldx(1,2),Ldx(2,4),Ldx(3,6),Rex(3,6),Pux(r,y) Pux(r,x) Pux(r,z)) /* DUP */ OP(0x06, Ldx(1,2),0, 0, Rex(1,2),Pux(r,x) Pux(r,x)) /* OVR */ OP(0x07, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pux(r,y) Pux(r,x) Pux(r,y)) /* EQU */ OP(0x08, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pu1(r,x==y)) /* NEQ */ OP(0x09, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pu1(r,y!=x)) /* GTH */ OP(0x0a, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pu1(r,y>x)) /* LTH */ OP(0x0b, Ldx(1,2),Ldx(2,4),0, Rex(2,4),Pu1(r,y>(x&0xf))<<(x>>4))) } 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(79K) Emulator, 5 Jul 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; }