ref: 5296e55cfb065247016be4632c9fb30425425908
dir: /plan9.c/
#include "plan9.h" #include "field.h" #include "gbuffer.h" #include "sim.h" #include <bio.h> #include <draw.h> #include <mouse.h> #include <keyboard.h> #include <thread.h> typedef struct Key Key; #define MIN(x,y) ((x)<(y)?(x):(y)) #define MAX(x,y) ((x)>(y)?(x):(y)) enum { Txtoff = 16, Coloff = 2, Cchar = 0, Ckey, Cmouse, Cresize, Credraw, Numchan, Sfancy = 0, Splain, Snone, Numstyles, Minsert = 0, Mappend, Mslide, Mselect, Nummodes, Menu3load = 0, Menu3save, Menu3dotstyle, Menu3rulerstyle, Menu3exit, Nummenu3, Dback = 0, Dfhigh, Dfmed, Dflow, Dfinv, Dbhigh, Dbmed, Dblow, Dbinv, Numcolors, /* this might become a bad idea in the future */ Mark_flag_selected = 1<<7, }; struct Key { int down; Rune rune; }; static Rune *linebuf; static vlong tick; static int gridw = 8, gridh = 8; static int rulerstyle = Sfancy, dotstyle = Sfancy; static int bpm = 120, apm = 120, pause, forward; static int curx, cury; static int selw = 1, selh = 1; static int charw, charh; static Field field; static Mbuf_reusable mbuf, mscr; static char filename[256]; static Channel *cchan; static Field copyfield, selfield; static int altdown; static int mode = Minsert; static long framedev; /* frame deviation >= 1ms */ static Rune *linebuf; static char *style[Numstyles] = { [Sfancy] = "fancy", [Splain] = "plain", [Snone] = "no", }; static Rune dot[Numstyles] = { [Sfancy] = L'·', [Splain] = '.', [Snone] = ' ', }; static Rune ruler[Numstyles][9] = { [Sfancy] = { L'┌', L'┬', L'┐', L'├', L'┼', L'┤', L'└', L'┴', L'┘', }, [Splain] = { '+', '+', '+', '+', '+', '+', '+', '+', '+', }, [Snone] = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', }, }; static struct { u8int u[4]; Usz at; }noteoff[16*128]; /* 16 channels, 128 notes each */ static u32int theme[Numcolors] = { [Dback] = 0x000000ff, [Dfhigh] = 0xffffffff, [Dfmed] = 0x777777ff, [Dflow] = 0x444444ff, [Dfinv] = 0x000000ff, [Dbhigh] = 0xddddddff, [Dbmed] = 0x72dec2ff, [Dblow] = 0x222222ff, [Dbinv] = 0xffb545ff, }; static Image *color[Numcolors]; static char *modes[Nummodes] = { [Minsert] = "insert", [Mappend] = "append", [Mslide] = "slide ", [Mselect] = "select", }; static char *menu3i[Nummenu3+1] = { [Menu3load] = "load", [Menu3save] = "save", [Menu3exit] = "exit", }; static Menu menu3 = { .item = menu3i, }; Usz orca_round_up_power2(Usz x) { x -= 1; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; return x + 1; } bool orca_is_valid_glyph(Glyph c) { if (c >= '0' && c <= '9') return true; if (c >= 'A' && c <= 'Z') return true; if (c >= 'a' && c <= 'z') return true; switch (c) { case '!': case '#': case '%': case '*': case '.': case ':': case ';': case '=': case '?': return true; } return false; } static void process(Oevent_list *events) { int i, off; Oevent *e; u8int u[4]; for (e = events->buffer, i = 0; i < events->count; i++, e++) { if (e->any.oevent_type == Oevent_type_midi_note) { Oevent_midi_note const *n = &e->midi_note; u[0] = 1; u[1] = 0x90 | n->channel; u[2] = (n->octave + 1)*12 + n->note; u[3] = n->velocity; write(1, u, 4); off = n->channel*128 + u[2]; noteoff[off].u[1] = 0x80 | n->channel; noteoff[off].u[2] = u[2]; noteoff[off].u[3] = 0; noteoff[off].at = tick + n->duration; } } for (i = 0; i < nelem(noteoff); i++) { if (noteoff[i].at > 0 && noteoff[i].at < tick) { write(1, noteoff[i].u, 4); noteoff[i].at = 0; } } } /* * nsec() is wallclock and can be adjusted by timesync * so need to use cycles() instead * * "fasthz" is how many ticks there are in a second * can be read from /dev/time * * perhaps using RDTSCP is even better */ static uvlong nanosec(void) { static uvlong fasthz, xstart; uvlong x, div; int f, n, i; char tmp[128], *e; if (fasthz < 1) { if ((f = open("/dev/time", OREAD)) < 0) sysfatal("failed to open /dev/time"); if ((n = read(f, tmp, sizeof(tmp)-1)) < 2) sysfatal("failed to read /dev/time"); tmp[n] = 0; e = tmp; for (i = 0; i < 3; i++) strtoll(e, &e, 10); fasthz = strtoll(e, nil, 10); if (fasthz < 1) sysfatal("failed to read fasthz"); close(f); cycles(&xstart); } cycles(&x); if (x < xstart) { /* wrap around */ xstart = 0; x += 0 - xstart; } x -= xstart; for (div = 1000000000ULL; x < 0x1999999999999999ULL && div > 1 ; div /= 10ULL, x *= 10ULL); return x / (fasthz / div); } static void orcathread(void *drawchan) { vlong start, end, n; vlong processold, processnew; Oevent_list events; int w, h; threadsetname("orca/sim"); oevent_list_init(&events); processnew = nanosec(); for (;;) { start = nanosec(); w = field.width; h = field.height; mbuffer_clear(mbuf.buffer, h, w); oevent_list_clear(&events); orca_run(field.buffer, mbuf.buffer, h, w, tick, &events, 0); processold = processnew; processnew = nanosec(); process(&events); nbsendul(drawchan, 0); forward = 0; do { end = 15000000000LL/bpm; /* 1e9*60/4 */ n = nanosec() - start; if (n >= end && !pause) break; /* unpause is not precise at all */ if (pause || end - n > 750000000LL) sleep(70); else if (end - n > 25000000LL) sleep(20); else if (end - n > 10000000LL) sleep(1); } while (!forward); framedev = (processnew - processold - 15000000000LL/bpm)/1000000LL; tick++; if (apm < bpm) bpm--; else if (apm > bpm) bpm++; } } static void redraw(int complete) { Rectangle r; Point p, top, bot; int x, y, rx, ry, i; int oldbg, oldfg, bg, fg, attr, selected, off; char s[32]; Rune c; p = addpt(screen->r.min, Pt(Txtoff, Txtoff)); top = p; bot.x = top.x; bot.y = screen->r.max.y - Txtoff - charh*2; if (complete) { r = screen->r; r.max.y = r.min.y + Txtoff; draw(screen, r, color[Dback], nil, ZP); r = screen->r; r.max.x = r.min.x + Txtoff; draw(screen, r, color[Dback], nil, ZP); r = screen->r; r.min.x += MIN(field.width*charw, Dx(r)-Txtoff) + Txtoff; draw(screen, r, color[Dback], nil, ZP); } bg = -1; fg = -1; for (y = 0; y < field.height && p.y < bot.y-charh; y++) { p.x = top.x; for (x = i = 0; x < field.width && x < screen->r.max.x-Txtoff; x++) { oldbg = bg; oldfg = fg; off = field.width*y + x; c = field.buffer[off]; attr = mbuf.buffer[off]; selected = x >= curx && y >= cury && x < curx+selw && y < cury+selh; if (selected) attr |= Mark_flag_selected; else attr &= ~Mark_flag_selected; if (!complete && c == copyfield.buffer[off] && attr == mscr.buffer[off]) { if (i > 0) { p = runestringnbg(screen, p, color[oldfg], ZP, font, linebuf, i, color[oldbg], ZP); i = 0; } p.x += charw; continue; } copyfield.buffer[off] = c; mscr.buffer[off] = attr; bg = selected ? Dbinv : Dback; fg = selected ? Dfinv : Dfhigh; if (c == '.') c = dot[dotstyle]; if (c == dot[dotstyle] && attr == 0) { if ((x % gridw) == 0 && (y % gridh) == 0) { rx = !!x + (x + 1) / field.width; ry = !!y + (y + 1) / field.height; c = rulerstyle == Snone ? dot[dotstyle] : ruler[rulerstyle][ry*3+rx]; } fg = Dflow; } else if (!selected) { if (c == '#') { fg = Dfmed; } else { if (c >= 'A' && c <= 'Z' && c != 'W' && c != 'N' && c != 'E' && c != 'S') { bg = Dbmed; fg = Dfinv; } if (attr & Mark_flag_input) { bg = Dback; fg = Dfhigh; } else if (attr & Mark_flag_lock) { bg = Dback; fg = Dfmed; } } if (attr & Mark_flag_output) { bg = Dfhigh; fg = Dfinv; } if (attr & Mark_flag_haste_input) { bg = Dback; fg = Dbmed; } } if (i > 0 && (bg != oldbg || fg != oldfg)) { p = runestringnbg(screen, p, color[oldfg], ZP, font, linebuf, i, color[oldbg], ZP); i = 0; } linebuf[i++] = c; } runestringnbg(screen, p, color[fg], ZP, font, linebuf, i, color[bg], ZP); p.y += charh; } r = screen->r; r.min.y = MIN(top.y + field.height*charh, bot.y-charh); draw(screen, r, color[Dback], nil, ZP); i = 0; sprint(s, "%udx%ud", field.width, field.height); i += runesprint(linebuf, "%-10s", s); sprint(s, "%d/%d", gridw, gridh); i += runesprint(linebuf+i, "%-9s", s); sprint(s, "%lldf%c", MAX(0, tick), pause ? '~' : 0); i += runesprint(linebuf+i, "%-9s", s); off = sprint(s, "%d", bpm); if (apm != bpm) off += sprint(s+off, "%+d", apm-bpm); sprint(s+off, "%c", (tick % 4) == 0 ? '*' : 0); i += runesprint(linebuf+i, "%-9s", s); sprint(s, "%ldms", labs(framedev)); i += runesprint(linebuf+i, "%-8s", s); runestringn(screen, bot, color[Dfhigh], ZP, font, linebuf, i); bot.y += charh; i = 0; sprint(s, "%ud,%ud", curx, cury); i += runesprint(linebuf, "%-10s", s); sprint(s, "%d:%d", selw, selh); i += runesprint(linebuf+i, "%-9s", s); i += runesprint(linebuf+i, "%-9s", modes[altdown ? Mslide : mode]); i += runesprint(linebuf+i, "%s", filename[0] ? filename : "unnamed"); runestringn(screen, bot, color[Dfhigh], ZP, font, linebuf, i); flushimage(display, 1); } static int fieldload(char *path) { Field_load_error e; if ((e = field_load_file(path, &field)) != Field_load_error_ok) { werrstr(field_load_error_string(e)); return -1; } curx = MIN(curx, field.width-1); cury = MIN(cury, field.height-1); return 0; } static int fieldsave(char *path) { FILE *f; if ((f = fopen(path, "w")) == nil) return -1; field_fput(&field, f); fclose(f); return 0; } static void selset(Rune key) { int y, commented; if (key == '#') { commented = 1; for (y = cury; y < cury+selh && y < field.height && commented; y++) { commented = field.buffer[curx + field.width*y] == key && field.buffer[MIN(field.width-1, curx + selw-1) + field.width*y] == key; } if (commented) key = '.'; } else { commented = 0; } for (y = cury; y < cury+selh && y < field.height; y++) { if (key == '#' || commented) { field.buffer[curx + field.width*y] = key; field.buffer[MIN(field.width-1, curx + selw-1) + field.width*y] = key; } else { memset(&field.buffer[curx + field.width*y], key, MIN(field.width-curx, selw)); } } } static void selcopy(void) { Biobuf *b; int y; if ((b = Bopen("/dev/snarf", OWRITE)) != nil) { for (y = cury; y < cury+selh && y < field.height; y++) { Bwrite(b, &field.buffer[curx + field.width*y], MIN(selw, field.width-curx)); Bputc(b, '\n'); } Bterm(b); } } static void selpaste(void) { Biobuf *b; char *s; int cols, rows, n; if ((b = Bopen("/dev/snarf", OREAD)) != nil) { for (cols = rows = 0; (s = Brdstr(b, '\n', 1)) != nil; rows++) { if ((n = Blinelen(b)) > cols) cols = n; if (cury+rows < field.height) memmove(&field.buffer[curx + field.width*(cury+rows)], s, MIN(n, field.width-curx)); free(s); } selw = MAX(1, cols); selh = MAX(1, rows); Bterm(b); } } static void fieldset(Rune key) { if (mode == Minsert) { selset(key); } else { field.buffer[curx + field.width*cury] = key; if (curx < field.width-1) curx++; } } static void selmove(int x, int y) { int i, n; if (curx+x < 0 || cury+y < 0) return; field_resize_raw(&selfield, selh, selw); gbuffer_copy_subrect( field.buffer, selfield.buffer, field.height, field.width, selh, selw, cury, curx, 0, 0, selh, selw ); n = MIN(selw, field.width-curx); for (i = cury; i < cury+selh && i < field.height; i++) { memset(&field.buffer[curx + field.width*i], '.', n); memset(&mbuf.buffer[curx + field.width*i], 0, n); } gbuffer_copy_subrect( selfield.buffer, field.buffer, selh, selw, field.height, field.width, 0, 0, cury+y, curx+x, selh, selw ); } static void selmap(int (*f)(int)) { int x, y; for (y = cury; y < cury+selh && y < field.height; y++) { for (x = curx; x < curx+selw && x < field.width; x++) { field.buffer[x + field.width*y] = f(field.buffer[x + field.width*y]); } } } static int snaplow(int n, int gridn) { n--; n -= (n % gridn) > 0 ? (n % gridn) : gridn; return MAX(1, n+1); } static int snaphigh(int n, int gridn) { n += gridn; n -= n % gridn - 1; return n; } static void screensize(int *w, int *h) { *w = snaplow((Dx(screen->r) - 2*Txtoff) / charw, gridw); *h = snaplow(((Dy(screen->r) - 2*Txtoff) - 3*charh) / charh, gridh); } static void kbdproc(void *k) { char buf[128], buf2[128], *s; Channel *kchan; int kfd, n; Key key; Rune r; threadsetname("kbdproc"); if ((kfd = open("/dev/kbd", OREAD)) < 0) sysfatal("can't open kbd: %r"); kchan = k; buf2[0] = 0; buf2[1] = 0; buf[0] = 0; for(;;){ if (buf[0] != 0) { n = strlen(buf)+1; memmove(buf, buf+n, sizeof(buf)-n); } if (buf[0] == 0) { n = read(kfd, buf, sizeof(buf)-1); if (n <= 0) break; buf[n-1] = 0; buf[n] = 0; } switch (buf[0]) { case 'c': if (chartorune(&r, buf+1) > 0 && r != Runeerror) nbsend(cchan, &r); /* no break */ default: continue; case 'k': s = buf+1; while (*s){ s += chartorune(&r, s); if (utfrune(buf2+1, r) == nil) { key.down = 1; key.rune = r; nbsend(kchan, &key); } } break; case 'K': s = buf2+1; while (*s) { s += chartorune(&r, s); if(utfrune(buf+1, r) == nil) { key.down = 0; key.rune = r; nbsend(kchan, &key); } } break; } strcpy(buf2, buf); } } static void command(char *s) { char *a; int x; if ((a = strchr(s, ':')) != nil) *a++ = 0; if (strcmp(s, "play") == 0) pause = 0; else if (strcmp(s, "stop") == 0) pause = 1; else if (strcmp(s, "run") == 0) forward = 1; else if (a != nil) { x = atoi(a); if (strcmp(s, "bpm") == 0) apm = bpm = MAX(1, x); else if (strcmp(s, "apm") == 0) apm = MAX(1, x); else if (strcmp(s, "frame") == 0) tick = MAX(0, x); else if (strcmp(s, "skip") == 0) tick = MAX(0, tick+x); else if (strcmp(s, "rewind") == 0) tick = MAX(0, tick-x); /* FIXME color, find, select, inject, write, time */ } } void threadmain(int argc, char **argv) { Mousectl *mctl; Key key; Keyboardctl kctl; Mouse m; char tmp[256]; Channel *kchan; int oldw, oldh, w, h, n, shiftdown, complete; int movex, movey, ctldown; Alt a[Numchan+1] = { [Cchar] = { nil, &key.rune, CHANRCV }, [Ckey] = { nil, &key, CHANRCV }, [Cmouse] = { nil, &m, CHANRCV }, [Cresize] = { nil, nil, CHANRCV }, [Credraw] = { nil, nil, CHANRCV }, { nil, nil, CHANEND }, }; USED(argc, argv); kchan = chancreate(sizeof(Key), 20); cchan = chancreate(sizeof(Rune), 20); proccreate(kbdproc, kchan, mainstacksize); srand(time(0)); threadsetname("orca/draw"); if(initdraw(nil, nil, "orca") < 0) sysfatal("initdraw: %r"); if ((mctl = initmouse(nil, screen)) == nil) sysfatal("initmouse: %r"); kctl.c = cchan; kctl.file = "/dev/null"; kctl.consfd = kctl.pid = kctl.ctlfd = -1; a[Cchar].c = cchan; a[Ckey].c = kchan; a[Cmouse].c = mctl->c; a[Cresize].c = mctl->resizec; a[Credraw].c = chancreate(sizeof(ulong), 0); for (n = 0; n < Numcolors; n++) color[n] = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, setalpha(theme[n] & ~0xff, theme[n] & 0xff)); charw = stringwidth(font, "X"); charh = font->height; screensize(&w, &h); field_init_fill(&field, h, w, '.'); field_init_fill(©field, h, w, '.'); field_init(&selfield); linebuf = malloc(sizeof(Rune)*MAX(w+1, 64)); memset(noteoff, 0, sizeof(noteoff)); mbuf_reusable_init(&mbuf); mbuf_reusable_ensure_size(&mbuf, h, w); memset(mbuf.buffer, 0, w*h); mbuf_reusable_init(&mscr); mbuf_reusable_ensure_size(&mscr, h, w); memset(mscr.buffer, 0, w*h); proccreate(orcathread, a[Credraw].c, mainstacksize); shiftdown = 0; altdown = 0; complete = 1; movex = 1; movey = 1; for (;;) { redraw(complete); complete = 0; oldw = w = field.width; oldh = h = field.height; noredraw: switch (alt(a)) { case Cmouse: if (m.buttons == 4) { menu3i[Menu3dotstyle] = tmp; menu3i[Menu3rulerstyle] = 1 + menu3i[Menu3dotstyle] + sprintf(tmp, "%s dots", style[(dotstyle+1) % Numstyles]); sprintf(menu3i[Menu3rulerstyle], "%s rulers", style[(rulerstyle+1) % Numstyles]); n = menuhit(3, mctl, &menu3, nil); if (n == Menu3load || n == Menu3save) { strncpy(tmp, filename, sizeof(tmp)); if (enter(n == Menu3load ? "load from:" : "save to:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0) { if (n == Menu3load && fieldload(tmp) == 0) { w = field.width; h = field.height; strncpy(filename, tmp, sizeof(filename)); } else if (n == Menu3save && fieldsave(tmp) == 0) { strncpy(filename, tmp, sizeof(filename)); } } } else if (n == Menu3dotstyle) { dotstyle = ++dotstyle % Numstyles; } else if (n == Menu3rulerstyle) { rulerstyle = ++rulerstyle % Numstyles; } else if (n == Menu3exit) { goto end; } complete = 1; } else { goto noredraw; } break; case Cresize: getwindow(display, Refnone); complete = 1; break; case Ckey: switch (key.rune) { case Kshift: shiftdown = key.down; break; case Kalt: altdown = key.down; break; case Kctl: ctldown = key.down; movex = ctldown ? gridw : 1; movey = ctldown ? gridh : 1; break; case Kup: case Kdown: case Kleft: case Kright: break; default: // fprint(2, "unknown key down/up 0x%x\n", key.rune); break; } break; case Credraw: break; case Cchar: switch (key.rune) { case 0x0b: /* C-k */ movey = 1; case Kup: if (shiftdown || mode == Mselect) selh = MAX(1, selh-movey); else { if (altdown || mode == Mslide) selmove(0, -movey); cury = MAX(0, cury-movey); } break; case '\n': /* C-j */ movey = 1; case Kdown: if (shiftdown || mode == Mselect) selh += movey; else { if (altdown || mode == Mslide) selmove(0, movey); cury = MIN(h-1, cury+movey); } break; case Kbs: /* C-h */ movex = 1; case Kleft: if (shiftdown || mode == Mselect) selw = MAX(1, selw-movex); else { if (altdown || mode == Mslide) selmove(-movex, 0); curx = MAX(0, curx-movex); } break; case 0x0c: /* C-l */ movex = 1; if (shiftdown) { /* FIXME this conflicts with vim keys btw */ selmap(tolower); break; } case Kright: if (shiftdown || mode == Mselect) selw += movex; else { if (altdown || mode == Mslide) selmove(movex, 0); curx = MIN(w-1, curx+movex); } break; case Ksoh: /* C-a */ if (shiftdown || mode == Mselect) selw = curx; curx = 0; break; case Kenq: /* C-e */ if (shiftdown || mode == Mselect) selw = field.width - curx; else curx = field.width-1; break; case Khome: if (shiftdown || mode == Mselect) selh = cury; cury = 0; break; case Kend: if (shiftdown || mode == Mselect) selh = field.height - cury; else cury = field.height-1; break; case 0x12: /* C-r */ tick = -1; forward = 1; break; case 0x13: /* C-s */ tmp[0] = 0; if (filename[0]) fieldsave(filename); else if (enter("file path:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0 && fieldsave(tmp) == 0) strncpy(filename, tmp, sizeof(filename)); break; case 0x18: /* C-x */ selcopy(); selset('.'); break; case Ketx: /* C-c */ selcopy(); break; case 0x16: /* C-v */ selpaste(); break; case '[': gridw = MAX(4, gridw-1); complete = 1; break; case ']': gridw = MIN(16, gridw+1); complete = 1; break; case '{': gridh = MAX(4, gridh-1); complete = 1; break; case '}': gridh = MIN(16, gridh+1); complete = 1; break; case '(': w = snaplow(w, gridw); break; case ')': w = snaphigh(w, gridw); break; case '_': h = snaplow(h, gridh); break; case '+': h = snaphigh(h, gridh); break; case '>': apm = ++bpm; break; case '<': apm = bpm = MAX(1, bpm-1); break; case 0x09: /* C-i */ case Kins: mode = mode != Mappend ? Mappend : Minsert; break; case Kstx: tmp[0] = 0; if (enter("command:", tmp, sizeof(tmp), mctl, &kctl, nil) > 0) command(tmp); break; case Kesc: if (mode == Mslide || mode != Minsert) mode = Minsert; else selw = selh = 1; break; case Kack: /* C-f */ forward = 1; break; case '`': case '~': case L'´': mode = mode != Mslide ? Mslide : Minsert; break; case '\'': mode = mode != Mselect ? Mselect : Minsert; break; case Knack: /* C-u */ if (shiftdown) selmap(toupper); break; case ' ': if (mode != Mappend) { pause = !pause; break; } default: if (key.rune == Kdel || key.rune == ' ') key.rune = '.'; if (orca_is_valid_glyph(key.rune)) { fieldset(key.rune); } else { // fprint(2, "unhandled key %04x\n", key.rune); goto noredraw; } break; } } if (w != oldw || h != oldh) { mbuf_reusable_ensure_size(&mscr, h, w); memset(mscr.buffer, 0, w*h); for (n = 0; n < oldh; n++) memmove(&mscr.buffer[n*w], &mbuf.buffer[n*oldw], MIN(w, oldw)); mbuf_reusable_ensure_size(&mbuf, h, w); memmove(mbuf.buffer, mscr.buffer, w*h); linebuf = realloc(linebuf, sizeof(Rune)*MAX(w+1, 64)); field_copy(&field, ©field); field_resize_raw(&field, h, w); memset(field.buffer, '.', w*h); gbuffer_copy_subrect( copyfield.buffer, field.buffer, copyfield.height, copyfield.width, h, w, 0, 0, 0, 0, MIN(h, copyfield.height), MIN(w, copyfield.width) ); field_resize_raw(©field, h, w); complete = 1; } } end: mbuf_reusable_deinit(&mscr); field_deinit(©field); field_deinit(&selfield); free(linebuf); threadexitsall(nil); }