ref: 5518b554dd7848acd7839e1de01cfa485ba32bd8
dir: /plan9.c/
#include <npe.h> #include <thread.h> #include <draw.h> #include <mouse.h> #include <keyboard.h> #include <control.h> /******************** usage and controls: n - new game u - undo r - redo s - solve Everything else is forwarded to the game. If the game doesn't redraw properly, try pressing the "game" button. *********************/ #undef PI #include "puzzles.h" #ifdef COMBINED #error Plan 9 should not be COMBINED #endif //#define PROFILE int dolog = 0; int logfd = -1; int logpipe[2]; static void Log(char *fmt, ...) { va_list arg; if (!dolog) return; va_start(arg, fmt); vfprint(logpipe[1], fmt, arg); va_end(arg); } typedef struct Ft Ft; struct Ft { int type; int size; Font *font; }; struct frontend { midend *me; Image *background; Image *img; Image **colors; int ncolors; Point ZP; Rectangle rect; Controlset *cs; Channel *c; Channel *settingschan; int showframe; int timeractive; Ft *fonts; int nfonts; }; struct blitter { Image *blimg; Point pos; }; frontend *fe = nil; struct proftimes { long draw; } ptimes; enum { GAME = 0, SETTINGS = 1, }; static Font* findfontfile(int type, int size) { Font *f; char buf[128]; char *fixed = "/lib/font/bit/lucidasans/typeunicode.%d.font"; char *variable = "/lib/font/bit/lucidasans/unicode.%d.font"; /* find font based on size */ while (size > 0) { snprint(buf, sizeof(buf), type == FONT_FIXED ? fixed : variable, size); if (access(buf, OREAD)) { f = openfont(display, buf); if (f == nil) { Log("error opening font file %s\n", buf); return font; } return f; } } Log("Unable to find proper font. Falling back to default font\n"); return font; } static Font* findfont(frontend *fe, int type, int size) { Ft *n; for (int i = 0; i < fe->nfonts; i++) { if (fe->fonts[i].type == type && fe->fonts[i].size == size) return fe->fonts[i].font; } fe->nfonts++; fe->fonts = realloc(fe->fonts, sizeof(Ft) * fe->nfonts); assert(fe->fonts); n = &fe->fonts[fe->nfonts-1]; n->type = type; n->size = size; n->font = findfontfile(type, size); return n->font; } void frontend_default_colour(frontend *, float *output) { output[0] = .9; output[1] = .9; output[2] = .9; } void get_random_seed(void **randseed, int *randseedsize) { long *t = malloc(sizeof(long)); assert(t); time(t); *randseed = (void*)t; *randseedsize = sizeof(long); } void deactivate_timer(frontend *fe) { fe->timeractive = 0; } void activate_timer(frontend *fe) { fe->timeractive = 1; } void fatal(const char *fmt, ...) { va_list ap; fprint(2, "fatal error: "); va_start(ap, fmt); vfprint(2, fmt, ap); va_end(ap); fprint(2, "\n"); sysfatal("error"); } #ifdef DEBUGGING void debug_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vfprint(1, fmt, ap); va_end(ap); } #endif static void usage(void) { fprint(2, "usage: %s [-hl]\n", argv0); exits(nil); } static void p9_draw_text(void *handle, int x, int y, int fonttype, int fontsize, int align, int color, const char *text) { Font *f; Point p, size; frontend *fe = (frontend*)handle; f = findfont(fe, fonttype, fontsize); p.x = x; p.y = y; size = stringsize(f, text); if (align & ALIGN_VCENTRE) p.y -= size.y / 2; else p.y -= size.y; if (align & ALIGN_HCENTRE) p.x -= size.x / 2; else if (align & ALIGN_HRIGHT) p.x -= size.x; string(fe->img, p, fe->colors[color], ZP, f, text); } static void p9_draw_rect(void *handle, int x, int y, int w, int h, int color) { frontend *fe = (frontend*)handle; draw(fe->img, Rect(x, y, x+w, y+h), fe->colors[color], nil, ZP); } static void p9_draw_line(void *handle, int x1, int y1, int x2, int y2, int color) { frontend *fe = (frontend*)handle; line(fe->img, Pt(x1, y1), Pt(x2, y2), Endsquare, Endsquare, 0, fe->colors[color], ZP); } static void p9_draw_thick_line(void *handle, float thickness, float x1, float y1, float x2, float y2, int color) { frontend *fe = (frontend*)handle; line(fe->img, Pt(x1, y1), Pt(x2, y2), Endsquare, Endsquare, thickness-1, fe->colors[color], ZP); } static void p9_draw_poly(void *handle, const int *coords, int npoints, int fillcolor, int outlinecolor) { Point *points = (Point*)coords; frontend *fe = (frontend*)handle; if (fillcolor >= 0) fillpoly(fe->img, (Point*)coords, npoints, 0, fe->colors[fillcolor], ZP); if (outlinecolor < 0) return; /* outline from first to last point */ poly(fe->img, points, npoints, Endsquare, Endsquare, 0, fe->colors[outlinecolor], ZP); /* close the outline shape by drawing from the last to the first point */ line(fe->img, points[npoints-1], points[0], Endsquare, Endsquare, 0, fe->colors[outlinecolor], ZP); } static void p9_draw_circle(void *handle, int cx, int cy, int radius, int fillcolor, int outlinecolor) { frontend *fe = (frontend*)handle; Point c = Pt(cx, cy); fillellipse(fe->img, c, radius, radius, fe->colors[fillcolor], ZP); ellipse(fe->img, c, radius, radius, 0, fe->colors[outlinecolor], ZP); Log("ellipse\n"); } static void p9_draw_update(void *handle, int x, int y, int w, int h) { frontend *fe = (frontend*)handle; draw(screen, rectaddpt(Rect(x, y, x+w, y+h), fe->ZP), fe->img, nil, fe->ZP); Log("draw_update\n"); } static void p9_clip(void *handle, int x, int y, int w, int h) { frontend *fe = (frontend*)handle; Rectangle r = Rect(x, y, x + w, y + h); replclipr(fe->img, 0, r); } static void p9_unclip(void *handle) { USED(handle); replclipr(fe->img, 0, fe->img->r); } long drawtime; static void p9_start_draw(void *handle) { USED(handle); Log("start_draw\n"); #ifdef PROFILE drawtime = times(nil); #endif } static void p9_end_draw(void *handle) { frontend *fe = (frontend*)handle; draw(screen, fe->rect, fe->img, nil, ZP); flushimage(display, 1); Log("end_draw\n"); #ifdef PROFILE ptimes.draw = times(nil) - drawtime; #endif } static void p9_status_bar(void *handle, const char *text) { frontend *fe = (frontend*)handle; chanprint(fe->cs->ctl, "l_status value %q", text); } static blitter* p9_blitter_new(void *handle, int w, int h) { USED(handle); blitter *bl; bl = malloc(sizeof(blitter)); bl->blimg = allocimage(display, Rect(0, 0, w, h), screen->chan, 0, 0); return bl; } static void p9_blitter_free(void *handle, blitter *bl) { USED(handle); freeimage(bl->blimg); free(bl); } static void p9_blitter_save(void *handle, blitter *bl, int x, int y) { frontend *fe = (frontend*)handle; bl->pos = Pt(x, y); flushimage(display, 0); draw(bl->blimg, bl->blimg->r, fe->img, nil, Pt(x, y)); } static void p9_blitter_load(void *handle, blitter *bl, int x, int y) { frontend *fe = (frontend*)handle; if (x == BLITTER_FROMSAVED) x = bl->pos.x; if (y == BLITTER_FROMSAVED) y = bl->pos.y; draw(fe->img, rectaddpt(bl->blimg->r, Pt(x, y)), bl->blimg, nil, ZP); } static const drawing_api p9_drawing = { p9_draw_text, p9_draw_rect, p9_draw_line, p9_draw_poly, p9_draw_circle, p9_draw_update, p9_clip, p9_unclip, p9_start_draw, p9_end_draw, p9_status_bar, p9_blitter_new, p9_blitter_free, p9_blitter_save, p9_blitter_load, nil, nil, nil, nil, nil, nil, nil, nil, /* {begin,end}_{doc,page,puzzle}, line_width, line_dotted */ nil, /* text_fallback */ #ifdef NO_THICK_LINE nil, #else p9_draw_thick_line, #endif }; static int rgb2col(int r, int g, int b) { return (r<<24) | (g<<16) | (b<<8) | 0xFF; } static frontend* new_window(void) { frontend *fe; fe = mallocz(sizeof(frontend), 1); if (!fe) sysfatal("error: out of memory!"); fe->me = midend_new(fe, &thegame, &p9_drawing, fe); return fe; } static void initui(Controlset *cs, Channel *c) { Control *b_game, *b_settings; Point p; createrow(cs, "rowmain"); b_game = createtextbutton(cs, "b_game"); p = stringsize(font, "game"); chanprint(cs->ctl, "b_game border 1"); chanprint(cs->ctl, "b_game align center"); chanprint(cs->ctl, "b_game text game"); chanprint(cs->ctl, "b_game size %d %d 500 %d", p.x, p.y, p.y); controlwire(b_game, "event", c); b_settings = createtextbutton(cs, "b_settings"); p = stringsize(font, "settings"); chanprint(cs->ctl, "b_settings border 1"); chanprint(cs->ctl, "b_settings align center"); chanprint(cs->ctl, "b_settings text settings"); chanprint(cs->ctl, "b_settings size %d %d 500 %d", p.x, p.y, p.y); createcolumn(cs, "c_settings"); chanprint(cs->ctl, "c_settings hide"); controlwire(b_settings, "event", c); createlabel(cs, "l_status"); chanprint(cs->ctl, "rowmain add b_game\nrowmain add b_settings"); activate(b_game); activate(b_settings); } static void initfe(frontend *fe, Mousectl *mousectl) { float *colors; int ncolors; int r, g, b; float bgcol[3]; Channel *c, *d; frontend_default_colour(fe, bgcol); fe->background = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, rgb2col(bgcol[0]*255., bgcol[1]*255., bgcol[2]*255.)); colors = midend_colours(fe->me, &ncolors); fe->colors = mallocz(ncolors * sizeof(Image*), 1); for (int i = 0; i < ncolors; i++) { r = colors[i*3+0] * 255.; g = colors[i*3+1] * 255.; b = colors[i*3+2] * 255.; fe->colors[i] = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, rgb2col(r, g, b)); } free(colors); fe->settingschan = chancreate(sizeof(char*), 0); c = chancreate(sizeof(Mouse), 0); d = chancreate(sizeof(Rune), 0); fe->cs = newcontrolset(screen, d, c, mousectl->resizec); fe->c = chancreate(sizeof(char*), 0); ctldeletequits = 1; namectlimage(display->black, "i_black"); namectlimage(display->white, "i_white"); namectlimage(fe->background, "background"); initui(fe->cs, fe->c); } int windowset = 0; static Point resize(int *resizenop) { int x, y, fd; x = Dx(screen->r); y = Dy(screen->r) - 2*font->height; midend_size(fe->me, &x, &y, 1, 1.); // to test if (0 && !windowset) { fd = open("/dev/wctl", OWRITE); if (fd >= 0) { fprint(fd, "resize -dx %d -dy %d\n", x+2, y + 2*font->height); close(fd); windowset = 1; } } else windowset = 0; /* do not resize if we're waiting for the window resize */ *resizenop = windowset; return Pt(x, y); } void resizecontrolset(Controlset *cs) { Rectangle rmenu, rarea, sarea; int resizenop; Point newsize; Point offset; if (getwindow(display, Refnone) < 0) { sysfatal("resize failed: %r"); } rmenu = screen->r; rmenu.max.y = rmenu.min.y + font->height; rarea = screen->r; rarea.min.y = rmenu.min.y + font->height; rarea.max.y = rarea.max.y - font->height; sarea = screen->r; sarea.min.y = sarea.max.y - font->height; newsize = resize(&resizenop); if (0 && resizenop) return; draw(screen, screen->r, fe->background, nil, ZP); if (fe->img) freeimage(fe->img); fe->img = allocimage(display, Rpt(ZP, newsize), screen->chan, 0, 0); if (!fe->img) sysfatal("cannot allocate new frame: %r"); /* calculate offset to place game area in the center */ offset = Pt(Dx(rarea), Dy(rarea)); // size of total game area offset = subpt(divpt(addpt(offset, newsize), 2), newsize); fe->rect = rectaddpt(rarea, offset); fe->ZP = fe->rect.min; chanprint(cs->ctl, "rowmain rect %R\nrowmain show", rmenu); chanprint(cs->ctl, "l_status rect %R\nl_status show", sarea); switch (fe->showframe) { case GAME: chanprint(cs->ctl, "c_settings hide"); midend_force_redraw(fe->me); flushimage(display, 1); break; case SETTINGS: chanprint(cs->ctl, "c_settings rect %R\nc_settings reveal\nc_settings show", rarea); break; } Log("resizecontrolset\n"); } typedef struct Option Option; struct Option { char *name; Control *kctl; Control *vctl; }; Option options[32]; static Option* findoption(char *name) { Option *o; for (o = options; o->name; o++) if (strcmp(o->name, name) == 0) return o; return nil; } static Option* findoptionname(char *ctl) { Option *o; for (o = options; o->name; o++) if (strcmp(o->vctl->name, ctl) == 0) return o; return nil; } static void clearoptions(void) { for (Option *o = options; o->name; o++) { if (o->name == nil) return; free(o->name); chanprint(fe->cs->ctl, "c_settings remove %q", o->kctl->name); chanprint(fe->cs->ctl, "c_settings remove %q", o->vctl->name); deactivate(o->vctl); closecontrol(o->kctl); closecontrol(o->vctl); o->kctl = o->vctl = nil; o->name = nil; } } static void addoption(config_item *cfg) { char buf[8]; Control *label, *entry; int last = 0; Option *o; for (o = options; o->name; o++) { last++; } o->name = strdup(cfg->name); snprint(buf, sizeof(buf), "l_%d", last); label = createlabel(fe->cs, buf); o->kctl = label; chanprint(fe->cs->ctl, "%q value %q", buf, cfg->name); chanprint(fe->cs->ctl, "%q align center", buf); chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height*2, font->height*2); chanprint(fe->cs->ctl, "c_settings add %q", buf); snprint(buf, sizeof(buf), "e_%d", last); entry = nil; switch (cfg->type) { case C_STRING: entry = createentry(fe->cs, buf); chanprint(fe->cs->ctl, "%q border 1", buf); chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height, font->height); chanprint(fe->cs->ctl, "%q value %q", buf, cfg->u.string.sval); chanprint(fe->cs->ctl, "%q align center", buf); controlwire(entry, "data", fe->settingschan); break; case C_BOOLEAN: if (!entry) entry = createtextbutton(fe->cs, buf); chanprint(fe->cs->ctl, "%q border 1", buf); chanprint(fe->cs->ctl, "%q textcolor black\n%q pressedtextcolor black", buf, buf); chanprint(fe->cs->ctl, "%q size 100 %d 500 %d", buf, font->height, font->height); chanprint(fe->cs->ctl, "%q value %d", buf, cfg->u.boolean.bval); chanprint(fe->cs->ctl, "%q text %s", buf, cfg->u.boolean.bval ? "yes" : "no"); chanprint(fe->cs->ctl, "%q align center", buf); chanprint(fe->cs->ctl, "%q mask transparent", buf); controlwire(entry, "event", fe->settingschan); break; case C_CHOICES: // todo break; } o->vctl = entry; if (entry) { activate(entry); chanprint(fe->cs->ctl, "c_settings add %q", buf); } } int configcats[] = { CFG_SETTINGS, /* CFG_SEED, */ CFG_PREFS }; config_item *configs[] = { nil, /* nil, */ nil }; static void loadoptions(void) { char *t; config_item *cfg; Control *c; /* assumptions: if b_savecfg exists, then l_cfginfo also exists */ c = controlcalled("b_savecfg"); /* if already set up, remove from widget to add back later */ if (c) { chanprint(fe->cs->ctl, "c_settings remove l_cfginfo"); chanprint(fe->cs->ctl, "c_settings remove b_savecfg"); } clearoptions(); for (int i = 0; i < nelem(configcats); i++) { cfg = midend_get_config(fe->me, configcats[i], &t); if (configs[i]) { //free_cfg(configs[i]); configs[i] = nil; } configs[i] = cfg; for (; cfg && cfg->type != C_END; cfg++) { addoption(cfg); } } /* if already set up, early return */ if (c) { goto Addbuttons; } createlabel(fe->cs, "l_cfginfo"); chanprint(fe->cs->ctl, "l_cfginfo align centerleft"); chanprint(fe->cs->ctl, "l_cfginfo size 50 %d 500 %d", font->height, font->height); c = createtextbutton(fe->cs, "b_savecfg"); chanprint(fe->cs->ctl, "b_savecfg text Save"); chanprint(fe->cs->ctl, "b_savecfg border 1"); chanprint(fe->cs->ctl, "b_savecfg align center"); chanprint(fe->cs->ctl, "b_savecfg size 50 %d 100 %d", font->height, font->height); controlwire(c, "event", fe->c); activate(c); Addbuttons: chanprint(fe->cs->ctl, "c_settings add l_cfginfo"); chanprint(fe->cs->ctl, "c_settings add b_savecfg"); } static void setoption(char *name, char *value, config_item *conf); static void readoptions(void) { char *val; config_item *cfg; Option *o; for (int i = 0; i < nelem(configcats); i++) { for (cfg = configs[i]; cfg && cfg->type != C_END; cfg++) { if (cfg->type != C_STRING) continue; o = findoption(cfg->name); if (!o || !o->vctl) continue; chanprint(fe->cs->ctl, "%q data", o->vctl->name); val = recvp(fe->settingschan); setoption(cfg->name, val, cfg); } } } static int saveoptions(void) { char *s; int r = 1; readoptions(); for (int i = 0; i < nelem(configcats); i++) { s = midend_set_config(fe->me, configcats[i], configs[i]); if (s) { chanprint(fe->cs->ctl, "l_cfginfo value %q", s); r = 0; } free_cfg(configs[i]); configs[i] = nil; } loadoptions(); Log("saved options\n"); return r; } static void setoption(char *name, char *value, config_item *conf) { int n; config_item *cfg; Option *o; if (conf) { cfg = conf; goto Found; } for (int i = 0; i < nelem(configs); i++) { for (cfg = configs[i]; cfg && cfg->type != C_END; cfg++) { if (strcmp(cfg->name, name) == 0) goto Found; } } return; Found: Log("Set %q to %q\n", cfg->name, value); switch (cfg->type) { case C_STRING: cfg->u.string.sval = value; break; case C_BOOLEAN: n = atoi(value) ? 1 : 0; cfg->u.boolean.bval = n; o = findoption(name); chanprint(fe->cs->ctl, "%q value %d", o->vctl->name, n); chanprint(fe->cs->ctl, "%q text %q", o->vctl->name, n ? "yes" : "no"); break; case C_CHOICES: // todo break; } } static void showframe(int frame) { fe->showframe = frame; resizecontrolset(fe->cs); Log("showframe\n"); } static int keyev(Rune k) { if (fe->showframe != GAME) { send(fe->cs->kbdc, &k); return 0; } switch (k) { case 'q': case 127: return 1; /* return 1 to quit */ case 'n': midend_process_key(fe->me, 0, 0, UI_NEWGAME); break; case 'u': midend_process_key(fe->me, 0, 0, UI_UNDO); break; case 'r': midend_process_key(fe->me, 0, 0, UI_REDO); break; case 's': midend_process_key(fe->me, 0, 0, UI_SOLVE); break; default: if (midend_process_key(fe->me, 0, 0, k) == PKR_QUIT) return 1; } return 0; } static void tick(float delta) { #ifdef PROFILE char msg[128]; Point p; #endif if (fe->timeractive) { midend_timer(fe->me, delta); } #ifdef PROFILE snprint(msg, 128, "draw: %ld", ptimes.draw); p = stringsize(font, msg); draw(screen, rectaddpt(Rect(0, 0, p.x, p.y), Pt(10, 30)), fe->background, nil, ZP); string(screen, Pt(10, 30), display->black, ZP, font, msg); #endif } typedef struct Tickinfo Tickinfo; struct Tickinfo { long totaltime; long lasttick; float delta; }; static void timerproc(void *v) { Channel *c; Tickinfo ti; long newtime; c = v; ti.totaltime = times(nil); for (;;) { sleep(20); newtime = times(nil); ti.delta = (newtime - ti.totaltime) / 1000.; ti.totaltime = newtime; send(c, &ti.delta); } } static void processmouse(Mouse *m, int *lm) { int x, y, r; if (fe->showframe != GAME) goto Ctrl; if (!ptinrect(m->xy, fe->rect)) goto Ctrl; x = m->xy.x - fe->rect.min.x; y = m->xy.y - fe->rect.min.y; r = -1; if ( ((*lm)&1) && !(m->buttons&1)) r = midend_process_key(fe->me, x, y, LEFT_RELEASE); if ( ((*lm)&1) && (m->buttons&1)) r = midend_process_key(fe->me, x, y, LEFT_DRAG); if (!((*lm)&1) && (m->buttons&1)) r = midend_process_key(fe->me, x, y, LEFT_BUTTON); if ( ((*lm)&2) && !(m->buttons&2)) r = midend_process_key(fe->me, x, y, MIDDLE_RELEASE); if ( ((*lm)&2) && (m->buttons&2)) r = midend_process_key(fe->me, x, y, MIDDLE_DRAG); if (!((*lm)&2) && (m->buttons&2)) r = midend_process_key(fe->me, x, y, MIDDLE_BUTTON); if ( ((*lm)&4) && !(m->buttons&4)) r = midend_process_key(fe->me, x, y, RIGHT_RELEASE); if ( ((*lm)&4) && (m->buttons&4)) r = midend_process_key(fe->me, x, y, RIGHT_DRAG); if (!((*lm)&4) && (m->buttons&4)) r = midend_process_key(fe->me, x, y, RIGHT_BUTTON); if (r >= 0) { } *lm = m->buttons; return; Ctrl: send(fe->cs->mousec, m); } static void doexit(void) { if (dolog) { close(logfd); } } void threadmain(int argc, char **argv) { int lastmouse; char *s, *args[6]; char *wintitle; float delta; Mousectl *mousectl; Mouse m; Keyboardctl *keyboardctl; Rune rune; Alt a[] = { { nil, &s, CHANRCV }, { nil, &delta, CHANRCV }, { nil, &m, CHANRCV }, { nil, &rune, CHANRCV }, { nil, &s, CHANRCV }, { nil, nil, CHANEND }, }; fe = new_window(); wintitle = nil; midend_get_config(fe->me, CFG_SETTINGS, &wintitle); ARGBEGIN{ case 'h': usage(); break; case 'l': dolog++; break; }ARGEND; atexit(doexit); if (dolog) { pipe(logpipe); logfd = create("/srv/puzzles", OWRITE|ORCLOSE, 0666); if (logfd < 0) sysfatal("error opening log file /srv/puzzles: %r"); fprint(logfd, "%d", logpipe[0]); close(logpipe[0]); Log("%s\n", thegame.name); } if (initdraw(nil, nil, thegame.name) < 0) { sysfatal("initdraw failed: %r"); } mousectl = initmouse(nil, screen); keyboardctl = initkeyboard(nil); initcontrols(); initfe(fe, mousectl); loadoptions(); midend_new_game(fe->me); resizecontrolset(fe->cs); a[0].c = fe->c; a[1].c = chancreate(sizeof(float), 0); a[2].c = mousectl->c; a[3].c = keyboardctl->c; a[4].c = fe->settingschan; proccreate(timerproc, a[1].c, 4096); for (;;) { switch (alt(a)) { case 0: /* libcontrol event channel */ Log("Control: %s\n", s); tokenize(s, args, nelem(args)); if (strcmp(args[0], "b_game:") == 0) { showframe(GAME); chanprint(fe->cs->ctl, "b_game value 0"); } else if (strcmp(args[0], "b_settings:") == 0) { showframe(SETTINGS); chanprint(fe->cs->ctl, "b_settings value 0"); } else if (strcmp(args[0], "b_savecfg:") == 0) { chanprint(fe->cs->ctl, "b_savecfg value 0"); if (saveoptions()) { midend_new_game(fe->me); showframe(GAME); } } else { Log("unknown command: %s\n", args[0]); } break; case 1: /* timer */ tick(delta); break; case 2: /* mouse */ processmouse(&m, &lastmouse); break; case 3: /* keyboard */ if (keyev(rune)) goto Out; break; case 4: /* settings */ Log("Setting: %s\n", s); tokenize(s, args, nelem(args)); args[0][strlen(args[0])-1] = 0; /* get rid of sender ":" */ setoption(findoptionname(args[0])->name, args[2], nil); break; } } Out: threadexitsall(nil); }