ref: 1aed09b328127929cdb95608c393bab90b9f03b9
dir: /zuke.c/
#include "theme.c" #include <mouse.h> #include <keyboard.h> #include <ctype.h> #include "plist.h" typedef struct Player Player; enum { Cstart = 1, Cstop, Ctoggle, Cforward, Cbackward, Cforwardfast, Cbackwardfast, Everror = 1, Evready, Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */ Seekbytes = Bps*10, /* 10 seconds */ Seekbytesfast = Bps*60, /* 1 minute */ Scrollwidth = 14, Scrollheight = 16, Relbufsz = Bps/5, /* 0.2 second */ }; struct Player { Channel *ctl; Channel *ev; Channel *img; int pcur; }; int mainstacksize = 32768; static Meta *pl; static int plnum; static char *plraw; static int plrawsize; static int volume; static Player *playernext; static Player *playercurr; static int audio; static u64int byteswritten; static int pcur, pcurplaying; static int scroll, scrollsz; static Font *f; static Image *cover; static Channel *ev; static Mousectl *mctl; static Keyboardctl *kctl; static int colwidth[3]; static int mincolwidth[3]; static int *shuffle; static char *covers[] = {"folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front"}; static char *menu3i[] = { "theme", "exit", nil }; static Menu menu3 = { .item = menu3i, }; #pragma varargck type "P" int static int positionfmt(Fmt *f) { char *s, tmp[16]; u64int sec; s = tmp; sec = va_arg(f->args, int); if(sec >= 3600){ s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600); sec %= 3600; } s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60); sec %= 60; seprint(s, tmp+sizeof(tmp), "%02lld", sec); return fmtstrcpy(f, tmp); } static void adjustcolumns(void) { int i, x, total; if(mincolwidth[0] == 0){ mincolwidth[0] = mincolwidth[1] = mincolwidth[2] = 1; for(i = 0; i < plnum; i++){ if((x = stringwidth(f, pl[i].artist[0])) > mincolwidth[0]) mincolwidth[0] = x; if((x = stringwidth(f, pl[i].album)) > mincolwidth[1]) mincolwidth[1] = x; if((x = stringwidth(f, pl[i].title)) > mincolwidth[2]) mincolwidth[2] = x; } } total = mincolwidth[0] + mincolwidth[1] + mincolwidth[2]; for(i = 0; i < nelem(mincolwidth); i++) colwidth[i] = (Dx(screen->r) - 8) * mincolwidth[i] / total; } static Meta * getmeta(int i) { return &pl[shuffle != nil ? shuffle[i] : i]; } static void redraw(int full) { Image *col; Point p, sp; Rectangle sel, r; int i, left, scrollcenter; char tmp[32]; lockdisplay(display); if(full){ draw(screen, screen->r, colors[Dback].im, nil, ZP); scrollsz = Dy(screen->r) / f->height - 1; adjustcolumns(); left = screen->r.min.x; if(scrollsz < plnum){ /* add a scrollbar */ p.x = sp.x = screen->r.min.x + Scrollwidth; p.y = screen->r.min.y; sp.y = screen->r.max.y; line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dfmed].im, ZP); r = screen->r; r.max.x = r.min.x + Scrollwidth - 1; r.min.x += 1; if(scroll < 1) scrollcenter = 0; else scrollcenter = (Dy(screen->r)-Scrollheight*5/4)*scroll / (plnum - scrollsz); r.min.y += scrollcenter + Scrollheight/4; r.max.y = r.min.y + Scrollheight; draw(screen, r, colors[Dfmed].im, nil, ZP); left += Scrollwidth + 4; } p.x = sp.x = left + colwidth[0] + 4; p.y = 0; sp.y = screen->r.max.y; line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dfmed].im, ZP); p.x = sp.x = left + colwidth[0] + 8 + colwidth[1] + 4; p.y = 0; sp.y = screen->r.max.y; line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dfmed].im, ZP); sp.x = sp.y = 0; p.x = left + 2; p.y = screen->r.min.y + 2; for(i = scroll; i < plnum; i++, p.y += f->height){ if(i < 0) continue; if(p.y > screen->r.max.y) break; if(pcur == i){ sel.min.x = left; sel.min.y = p.y; sel.max.x = screen->r.max.x; sel.max.y = p.y + f->height; draw(screen, sel, colors[Dbinv].im, nil, ZP); col = colors[Dfinv].im; }else{ col = colors[pcurplaying == i ? Dfhigh : Dfmed].im; } sel = screen->r; r = screen->r; p.x = left + 2; sel.max.x = p.x + colwidth[0]; replclipr(screen, 0, sel); string(screen, p, col, sp, f, getmeta(i)->artist[0]); p.x += colwidth[0] + 8; sel.min.x = p.x; sel.max.x = p.x + colwidth[1]; replclipr(screen, 0, sel); string(screen, p, col, sp, f, getmeta(i)->album); p.x += colwidth[1] + 8; sel.min.x = p.x; sel.max.x = p.x + colwidth[2]; replclipr(screen, 0, sel); string(screen, p, col, sp, f, getmeta(i)->title); replclipr(screen, 0, r); if(pcurplaying == i){ Point rightp, leftp; leftp.y = rightp.y = p.y - 1; leftp.x = left; rightp.x = screen->r.max.x; line(screen, leftp, rightp, 0, 0, 0, colors[Dfmed].im, sp); leftp.y = rightp.y = p.y + f->height; line(screen, leftp, rightp, 0, 0, 0, colors[Dfmed].im, sp); } } } if(pcurplaying >= 0) snprint(tmp, sizeof(tmp), "%s%P/%P %d%%", shuffle != nil ? "∫ " : "", (int)(byteswritten/Bps), getmeta(pcurplaying)->duration/1000, volume); else snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", volume); r = screen->r; r.min.x = r.max.x - stringwidth(f, tmp) - 4; r.min.y = r.max.y - f->height - 4; draw(screen, r, colors[Dblow].im, nil, ZP); string(screen, addpt(r.min, Pt(2, 2)), colors[Dfhigh].im, sp, f, tmp); if(cover != nil && full){ r.max.x = r.min.x; r.min.x = screen->r.max.x - cover->r.max.x - 8; draw(screen, r, colors[Dblow].im, nil, ZP); r = screen->r; r.min.x = r.max.x - cover->r.max.x - 8; r.min.y = r.max.y - cover->r.max.y - 8 - f->height - 4; r.max.y = r.min.y + cover->r.max.y + 8; draw(screen, r, colors[Dblow].im, nil, ZP); draw(screen, insetrect(r, 4), cover, nil, ZP); } flushimage(display, 1); unlockdisplay(display); } void themechanged(void) { redraw(1); } static void coverload(void *player_) { int p[2], pid, fd, i; char *prog, *path, *s, tmp[32]; Meta *m; Channel *ch; Player *player; Image *newcover; threadsetname("cover"); player = player_; m = getmeta(player->pcur); pid = -1; ch = player->img; fd = -1; prog = nil; if(m->imagefmt != nil && m->imagereader == 0){ if(strcmp(m->imagefmt, "image/png") == 0) prog = "png"; else if(strcmp(m->imagefmt, "image/jpeg") == 0) prog = "jpg"; } if(prog == nil){ path = strdup(m->path); if(path != nil && (s = utfrrune(path, '/')) != nil){ *s = 0; for(i = 0; i < nelem(covers) && prog == nil; i++){ if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0) prog = "jpg"; free(s); s = nil; if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0) prog = "png"; free(s); } } free(path); } if(prog == nil) goto done; if(fd < 0){ fd = open(m->path, OREAD); seek(fd, m->imageoffset, 0); } pipe(p); if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){ dup(fd, 0); close(fd); dup(p[1], 1); close(p[1]); dup(open("/dev/null", OWRITE), 2); snprint(tmp, sizeof(tmp), "%s -9t | resample -x128", prog); execl("/bin/rc", "rc", "-c", tmp, nil); sysfatal("execl: %r"); } close(fd); close(p[1]); if(pid > 0){ newcover = readimage(display, p[0], 1); sendp(ch, newcover); } close(p[0]); done: if(pid < 0) sendp(ch, nil); chanclose(ch); chanfree(ch); if(pid >= 0) postnote(PNGROUP, pid, "interrupt"); threadexits(nil); } static int playerret(Player *player) { return recvul(player->ev) == Everror ? -1 : 0; } static void stop(Player *player) { if(player == nil) return; if(player == playernext) playernext = nil; sendul(player->ctl, Cstop); } static void playerthread(void *player_); static Player * newplayer(int pcur, int loadnext) { Player *player; if(playernext != nil && loadnext){ if(pcur == playernext->pcur){ player = playernext; playernext = nil; goto done; } stop(playernext); playernext = nil; } player = mallocz(sizeof(*player), 1); player->ctl = chancreate(sizeof(ulong), 0); player->ev = chancreate(sizeof(ulong), 0); player->pcur = pcur; threadcreate(playerthread, player, mainstacksize); if(playerret(player) < 0) return nil; done: if(pcur < plnum-1 && playernext == nil && loadnext) playernext = newplayer(pcur+1, 0); return player; } static int start(Player *player) { if(player != nil) sendul(player->ctl, Cstart); return -1; } static void playerthread(void *player_) { char *buf, cmd[32]; Player *player; Ioproc *io; Image *thiscover; ulong c; int p[2], fd, pid, n, got, noinit, trycoverload; u64int bytesfrom, bf; threadsetname("player"); player = player_; noinit = 0; bytesfrom = 0; c = 0; buf = nil; trycoverload = 1; io = nil; pid = -1; restart: if((fd = open(getmeta(player->pcur)->path, OREAD)) < 0){ fprint(2, "%r\n"); sendul(player->ev, Everror); goto freeplayer; } pipe(p); if((pid = rfork(RFPROC|RFFDG|RFREND|RFNOTEG)) == 0){ dup(p[0], 1); close(p[0]); dup(fd, 0); close(fd); dup(open("/dev/null", OWRITE), 2); close(p[1]); snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", getmeta(player->pcur)->filefmt); execl(cmd, cmd, nil); sysfatal("execl: %r"); } if(pid < 0) sysfatal("rfork: %r"); close(fd); close(p[0]); byteswritten = 0; if(!noinit){ sendul(player->ev, Evready); buf = malloc(Relbufsz); io = ioproc(); for(c = 0, got = 0; got < Relbufsz; got += n){ if((c = nbrecvul(player->ctl)) != 0) break; n = ioread(io, p[1], buf+got, Relbufsz-got); if(n < 1) break; } if(c == 0) c = recvul(player->ctl); if(c != Cstart) goto freeplayer; iowrite(io, audio, buf, got); byteswritten = got; bytesfrom = 0; c = 0; noinit = 1; } pcurplaying = player->pcur; if(c != Cbackward && c != Cbackwardfast) redraw(1); while(1){ n = Relbufsz; if(bytesfrom > byteswritten && n > bytesfrom-byteswritten) n = bytesfrom-byteswritten; n = ioread(io, p[1], buf, n); if(n < 1) break; thiscover = nil; if(player->img != nil && nbrecv(player->img, &thiscover) != 0){ freeimage(cover); cover = thiscover; redraw(1); player->img = nil; } c = nbrecvul(player->ctl); bf = bytesfrom != 0 ? bytesfrom : byteswritten; if(c == Cstop || c == -1) goto stop; if(c == Ctoggle){ c = recvul(player->ctl); if(c == Cstop) goto stop; }else if(c == Cforward){ bytesfrom = bf + Seekbytes; }else if(c == Cforwardfast){ bytesfrom = bf + Seekbytesfast; }else if(c == Cbackward){ /* to seek backwards we need to restart playback */ bytesfrom = bf >= Seekbytes ? bf - Seekbytes : 0; n = 0; /* not an error */ break; }else if(c == Cbackwardfast){ bytesfrom = bf >= Seekbytesfast ? bf - Seekbytesfast : 0; n = 0; /* not an error */ break; } c = 0; if(bytesfrom <= byteswritten){ if(bytesfrom == byteswritten) bytesfrom = 0; if(iowrite(io, audio, buf, n) != n){ fprint(2, "failed to write %d bytes: %r\n", n); break; } if(trycoverload && byteswritten >= Bps){ player->img = chancreate(sizeof(Image*), 0); proccreate(coverload, player, 4096); trycoverload = 0; } } byteswritten += n; if(bytesfrom == byteswritten || (byteswritten/Bps > (byteswritten-n)/Bps)) redraw(0); } if(n == 0){ /* seeking backwards or end of the song */ close(p[1]); if(c != Cbackward && c != Cbackwardfast){ playercurr = nil; playercurr = newplayer((player->pcur+1) % plnum, 1); start(playercurr); goto stop; } goto restart; } stop: if(player->img != nil) freeimage(recvp(player->img)); freeplayer: if(player == playercurr) playercurr = nil; if(player == playernext) playernext = nil; chanclose(player->ctl); chanclose(player->ev); chanfree(player->ctl); chanfree(player->ev); close(p[1]); closeioproc(io); free(buf); free(player); if(pid >= 0) postnote(PNGROUP, pid, "interrupt"); threadexits(nil); } static void toggle(Player *player) { if(player != nil) sendul(player->ctl, Ctoggle); } static void backward(Player *player) { if(player != nil) sendul(player->ctl, Cbackward); } static void forward(Player *player) { if(player != nil) sendul(player->ctl, Cforward); } static void backwardfast(Player *player) { if(player != nil) sendul(player->ctl, Cbackwardfast); } static void forwardfast(Player *player) { if(player != nil) sendul(player->ctl, Cforwardfast); } static void readplist(void) { Meta *m; char *s, *e, *endrec; int i, n, sz, alloc, tagsz, intval; s = nil; for(alloc = sz = 0;;){ alloc += 65536; if((s = realloc(s, alloc)) == nil) sysfatal("no memory"); for(n = 0; sz < alloc; sz += n){ n = read(0, s+sz, alloc-sz); if(n < 0) sysfatal("%r"); if(n == 0) break; } if(n == 0) break; } plraw = s; plrawsize = sz; plraw[plrawsize-1] = 0; if(sz < 4 || s[0] != '#' || s[1] != ' ' || !isdigit(s[2]) || (s = memchr(plraw, '\n', sz)) == nil) sysfatal("invalid playlist"); s++; /* at the start of the first record */ plnum = atoi(plraw+2); pl = calloc(plnum, sizeof(Meta)); for(i = 0; i < plnum; i++, s = endrec){ if(plraw+plrawsize < s+10) sysfatal("truncated playlist"); if(s[0] != '#' || s[1] != ' ' || !isdigit(s[2])) sysfatal("invalid record"); if((n = strtol(s+2, &e, 10)) < 0 || n > plnum) sysfatal("invalid track index"); if(pl[n].path != nil) sysfatal("duplicate track index"); s[-1] = 0; sz = strtol(e, &s, 10); *s++ = 0; /* skip '\n' */ if(s+sz > plraw+plrawsize) sysfatal("truncated playlist"); s[sz-1] = 0; /* '\n'→'\0' to mark the end of the record */ endrec = s+sz; m = &pl[n]; for(;;){ if(s[0] == Pimage){ m->imageoffset = strtol(s+2, &e, 10); m->imagesize = strtol(e+1, &s, 10); m->imagereader = strtol(s+1, &e, 10); m->imagefmt = e + 1; s = strchr(e+2, '\n') + 1; }else if(s[0] == Pchannels || s[0] == Pduration || s[0] == Psamplerate){ intval = strtol(s+2, &e, 10); if(s[0] == Pduration) m->duration = intval; s = e + 1; }else if(s[0] == Ppath){ m->path = s+2; break; /* always the last one */ }else if(s[0] == Pfilefmt){ m->filefmt = s+2; s = strchr(s+2, '\n') + 1; }else{ tagsz = strtol(s+1, &e, 10); if(e+tagsz >= plraw+plrawsize) sysfatal("truncated playlist"); e++; /* point to tag value */ e[tagsz] = 0; /* '\n'→'\0' to mark the end of the tag value */ if(s[0] == Palbum) m->album = e; else if(s[0] == Partist && m->numartist < Maxartist) m->artist[m->numartist++] = e; else if(s[0] == Pdate) m->date = e; else if(s[0] == Ptitle) m->title = e; else if(s[0] == Pdate) m->date = e; else if(s[0] == Ptrack) m->track = e; else sysfatal("unknown tag type %c", s[0]); s = e + tagsz + 1; } s[-1] = 0; } if(m->filefmt == nil) sysfatal("old playlist format?\nplease re-run audio/mkplist"); } } static void search(char d) { Meta *m; static char buf[64]; static int sz; int inc, i, a; inc = (d == '/' || d == 'n') ? 1 : -1; if(d == '/' || d == '?') sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, kctl, nil); if(sz < 1) return; for(i = pcur+inc; i >= 0 && i < plnum; i += inc){ m = getmeta(i); for(a = 0; a < m->numartist; a++){ if(cistrstr(m->artist[a], buf) != nil) break; } if(m->album != nil && cistrstr(m->album, buf) != nil) break; if(m->title != nil && cistrstr(m->title, buf) != nil) break; if(cistrstr(m->path, buf) != nil) break; } if(i >= 0 && i < plnum){ pcur = i; redraw(1); } } static void chvolume(int d) { int f, l, r, ol, or; Biobuf b; char *s, *a[4]; if((f = open("/dev/volume", ORDWR)) < 0) return; Binit(&b, f, OREAD); l = r = 0; for(; (s = Brdline(&b, '\n')) != nil;) { if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){ l = ol = atoi(a[1]); r = or = atoi(a[2]); for(;;){ l += d; r += d; fprint(f, "master %d %d\n", l, r); Bseek(&b, 0, 0); for(; (s = Brdline(&b, '\n')) != nil;){ if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){ if(atoi(a[1]) == l && atoi(a[2]) == r) goto end; if(atoi(a[1]) != ol && atoi(a[2]) != or) goto end; if (l < 0 || r < 0 || l > 100 || r > 100) goto end; break; } } } } } end: volume = (l+r)/2; if(volume > 100) volume = 100; else if(volume < 0) volume = 0; Bterm(&b); close(f); } static void toggleshuffle(void) { int i, m, xi, a, c, pcurnew, pcurplayingnew; if(shuffle == nil){ if(plnum < 2) return; m = plnum; if(plnum < 4){ a = 1; c = 3; m = 7; }else{ m += 1; m |= m >> 1; m |= m >> 2; m |= m >> 4; m |= m >> 8; m |= m >> 16; a = 1 + nrand(m/4)*4; /* 1 ≤ a < m && a mod 4 = 1 */ c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */ } shuffle = malloc(plnum*sizeof(*shuffle)); xi = pcurplaying < 0 ? pcur : pcurplaying; pcurplayingnew = -1; pcurnew = 0; for(i = 0; i < plnum;){ if(xi < plnum){ if(pcur == xi) pcurnew = i; if(pcurplaying == xi) pcurplayingnew = i; shuffle[i++] = xi; } xi = (a*xi + c) & m; } pcur = pcurnew; pcurplaying = pcurplayingnew; }else{ pcur = shuffle[pcur]; if(pcurplaying >= 0) pcurplaying = shuffle[pcurplaying]; free(shuffle); shuffle = nil; } } void threadmain(int argc, char **argv) { char tmp[256]; Rune key; Mouse m; Alt a[] = { { nil, &m, CHANRCV }, { nil, nil, CHANRCV }, { nil, &key, CHANRCV }, { nil, nil, CHANEND }, }; int fd, n, scrolling, oldpcur, oldbuttons, pnew; USED(argc); USED(argv); audio = open("/dev/audio", OWRITE); if(audio < 0) sysfatal("audio: %r"); readplist(); if(plnum < 1){ fprint(2, "empty playlist\n"); sysfatal("empty"); } if(initdraw(nil, nil, "zuke") < 0) sysfatal("initdraw: %r"); unlockdisplay(display); if((mctl = initmouse(nil, screen)) == nil) sysfatal("initmouse: %r"); if((kctl = initkeyboard(nil)) == nil) sysfatal("initkeyboard: %r"); a[0].c = mctl->c; a[1].c = mctl->resizec; a[2].c = kctl->c; f = display->defaultfont; srand(time(0)); pcurplaying = -1; chvolume(0); fmtinstall('P', positionfmt); threadsetname("zuke"); snprint(tmp, sizeof(tmp), "/proc/%d/ctl", getpid()); if((fd = open(tmp, OWRITE)) >= 0){ fprint(fd, "pri 13\n"); close(fd); } themeinit(); redraw(1); oldbuttons = 0; scrolling = 0; for(;;){ ev: oldpcur = pcur; switch(alt(a)){ case 0: if(m.buttons == 4){ n = menuhit(3, mctl, &menu3, nil); if(n == 0) proccreate(themeproc, nil, 4096); else if(n == 1) goto end; goto ev; } if(m.buttons != 2) scrolling = 0; else if(oldbuttons == 0 && m.xy.x <= screen->r.min.x+Scrollwidth) scrolling = 1; oldbuttons = m.buttons; if(m.buttons == 0) break; if(scrolling){ if(scrollsz >= plnum) break; scroll = (m.xy.y - screen->r.min.y - Scrollheight/4)*(plnum-scrollsz) / (Dy(screen->r)-Scrollheight/2); if(scroll > plnum-scrollsz-1) scroll = plnum-scrollsz-1; if(scroll < 0) scroll = 0; redraw(1); }else if(m.buttons == 1 || m.buttons == 2){ pcur = scroll + (m.xy.y - screen->r.min.y)/f->height; if(m.buttons == 2){ stop(playercurr); playercurr = newplayer(pcur, 1); start(playercurr); } } break; case 1: /* resize */ if(getwindow(display, Refnone) < 0) sysfatal("getwindow: %r"); redraw(1); break; case 2: switch(key){ case Kleft: backward(playercurr); break; case Kright: forward(playercurr); break; case ',': backwardfast(playercurr); break; case '.': forwardfast(playercurr); break; case Kup: pcur--; break; case Kpgup: pcur -= scrollsz; break; case Kdown: pcur++; break; case Kpgdown: pcur += scrollsz; break; case Kend: pcur = plnum-1; break; case Khome: pcur = 0; break; case 10: stop(playercurr); playercurr = newplayer(pcur, 1); start(playercurr); break; case 'q': case Kdel: stop(playercurr); goto end; case 'i': case 'o': if(pcur == pcurplaying) oldpcur = -1; pcur = pcurplaying; break; case 'b': case '>': if(playercurr == nil) break; pnew = pcurplaying; if(++pnew >= plnum) pnew = 0; stop(playercurr); playercurr = newplayer(pnew, 1); start(playercurr); redraw(1); break; case 'z': case '<': if(playercurr == nil) break; pnew = pcurplaying; if(--pnew < 0) pnew = plnum-1; stop(playercurr); playercurr = newplayer(pnew, 1); start(playercurr); redraw(1); break; case '-': chvolume(-1); redraw(0); break; case '+': chvolume(+1); redraw(0); break; case 'v': stop(playercurr); playercurr = nil; pcurplaying = -1; freeimage(cover); cover = nil; redraw(1); break; case 's': toggleshuffle(); redraw(1); break; case 'c': case 'p': toggle(playercurr); break; case '/': case '?': case 'n': case 'N': search(key); break; } } if(pcur != oldpcur){ if(pcur < 0) pcur = 0; else if(pcur >= plnum) pcur = plnum - 1; if(pcur < scroll) scroll = pcur; else if(pcur > scroll + scrollsz) scroll = pcur - scrollsz; if(scroll > plnum - scrollsz) scroll = plnum - scrollsz; if(scroll < 0) scroll = 0; if(pcur != oldpcur) redraw(1); } } end: threadexitsall(nil); }