shithub: lola

Download patch

ref: 3ca0009a7450e86fed00995d8e296c41619160ba
parent: 95fa0cb9868e932653e84533fe41691ad4e1579e
author: aap <aap@papnet.eu>
date: Tue Aug 20 06:59:59 EDT 2024

first implementation of tabbed windows (WIP)

--- a/README
+++ b/README
@@ -30,6 +30,15 @@
 	- screenoffset wctl message
 	- (no)sticky wctl message
 - select window with mouse by reading from `pick' file
+- tabbed windows (WIP)
+	- new tab: click window instead of dragging out rectangle
+	- close tab: button 2 on tab
+	- move tab: 1-2 and 1-3 chords
+	- move tab across window: button 3 on tab or title bar,
+		click new window or background
+	- TODO:
+		- no fs interface yet
+		- only implemented for simple theme so far
 - misc
 	- 1-3 chord on desktop opens secondary menu
 
@@ -38,4 +47,4 @@
 The window decoration style is set by linking in the relevant file,
 so edit the `mkfile' to try different styles.
 The options are: flat.c, simple.c, win3.c, win95.c
-
+	BUG: only simple.c currently working because of tabs
--- a/TODO
+++ /dev/null
@@ -1,20 +1,0 @@
-rethink resizing and repainting
-rethink hiding/unhiding
-check for bad rects (what's left here?)
-top/bottom/current seems to work a bit different in rio
-make sure there are no deadlocks
-...
-
-ideas:
-	case-insensitive 'look'
-	virtual screen (like fvwm)
-	cursor movement
-	decorations (at least make them possible)
-	tabbed window
-
-problems:
-	Borderwidth hardcoded in gengetwindow
-	originwindow doesn't work with gengetwindow
-	non-origin screen breaks samterm scrollbars
-	raw mode where?
-	initkeyboard with /dev/kbd support (also fix leaks in old code)
--- a/data.c
+++ b/data.c
@@ -168,7 +168,8 @@
 Image*
 mkcolor(ulong col)
 {
-	return allocimage(display, Rect(0,0,1,1), screen->chan, 1, col);
+//	return allocimage(display, Rect(0,0,1,1), screen->chan, 1, col);
+	return allocimage(display, Rect(0,0,1,1), RGBA32, 1, col);
 }
 
 Image*
--- a/flat.c
+++ b/flat.c
@@ -19,7 +19,7 @@
 Image *wincolors[NumWinColors];
 
 void
-wdecor(Window *w)
+wdecor(WinTab *w)
 {
 	if(w->frame == nil)
 		return;
@@ -44,7 +44,7 @@
 }
 
 void
-wtitlectl(Window *w)
+wtitlectl(WinTab *w)
 {
 	if(mctl->buttons & 7){
 		wraise(w);
--- a/fs.c
+++ b/fs.c
@@ -95,7 +95,7 @@
 typedef struct Xfid Xfid;
 struct Xfid
 {
-	Window *w;
+	WinTab *w;
 	RuneConvBuf cnv;
 };
 #define XF(fid) ((Xfid*)(fid)->aux)
@@ -156,7 +156,7 @@
 }
 
 static Xfid*
-getxfid(Window *w)
+getxfid(WinTab *w)
 {
 	Xfid *xf;
 	xf = emalloc(sizeof(Xfid));
@@ -175,7 +175,7 @@
 static void
 fsattach(Req *r)
 {
-	Window *w;
+	WinTab *w;
 	char *end;
 	int id;
 	Wctlcmd cmd;
@@ -240,7 +240,7 @@
 	int i;
 	Dirent *d;
 	Xfid *xf;
-	Window *w;
+	WinTab *w;
 	int dir;
 
 	xf = fid->aux;
@@ -288,7 +288,7 @@
 static int
 genrootdir(int n, Dir *d, void *a)
 {
-	Window *w = a;
+	WinTab *w = a;
 	int i;
 
 	n++;	/* -1 is root dir */
@@ -320,7 +320,7 @@
 static int
 genwsysdir(int n, Dir *d, void*)
 {
-	Window *w;
+	WinTab *w;
 
 	if(n == -1){
 		genrootdir(0, d, nil);
@@ -328,8 +328,8 @@
 		d->name = estrdup9p("wsys");
 		return 0;
 	}
-	if(n < nwindows){
-		w = windows[n];
+	if(n < nwintabs){
+		w = wintabs[n];
 		genrootdir(-1, d, w);
 		free(d->name);
 		d->name = smprint("%d", w->id);
@@ -345,7 +345,7 @@
 fsopen(Req *r)
 {
 	Xfid *xf;
-	Window *w;
+	WinTab *w;
 	int rd, wr;
 
 	xf = XF(r->fid);
@@ -421,7 +421,7 @@
 		/* pick window from main thread.
 		 * TODO: this may not be optimal because
 		 *       it might block this thread. */
-		Channel *wc = chancreate(sizeof(Window*), 0);
+		Channel *wc = chancreate(sizeof(WinTab*), 0);
 		sendp(pickchan, wc);
 		xf->w = w = recvp(wc);
 		if(w)
@@ -452,7 +452,7 @@
 fsclose(Fid *fid)
 {
 	Xfid *xf;
-	Window *w;
+	WinTab *w;
 	Text *x;
 	int rd, wr;
 
@@ -578,7 +578,7 @@
 static char*
 readblocking(Req *r, Channel *readchan)
 {
-	Window *w;
+	WinTab *w;
 	Channel *chan;
 	Stringpair pair;
 	enum { Adata, Agone, Aflush, NALT };
@@ -612,7 +612,7 @@
 static void
 xread(Req *r)
 {
-	Window *w;
+	WinTab *w;
 	char *data;
 
 	w = XF(r->fid)->w;
@@ -660,7 +660,7 @@
 		respond(r, readimg(r, screen));
 		return;
 	case Qwindow:
-		respond(r, readimg(r, w->frame));
+		respond(r, readimg(r, w->w->frame));
 		return;
 	case Qwctl:
 /* TODO: what's with the Etooshort conditions?? */
@@ -704,7 +704,7 @@
 xwrite(Req *r)
 {
 	Xfid *xf;
-	Window *w;
+	WinTab *w;
 	Text *x;
 	vlong offset;
 	u32int count;
@@ -793,7 +793,7 @@
 			return;
 		}
 		pt.y = strtoul(p, nil, 0);
-		wmovemouse(w, pt);
+		wmovemouse(w->w, pt);
 		break;
 
 	case Qcursor:
--- a/inc.h
+++ b/inc.h
@@ -187,33 +187,48 @@
 extern int titlesz;
 
 typedef struct Window Window;
+typedef struct WinTab WinTab;
+
 struct Window
 {
 	Ref;
-	bool deleted;
-	bool hidden;
 	Window *lower;
 	Window *higher;
+	bool hidden;
 	Image *frame;
+	Screen *screen;
+	int noborder;
+	bool notitle;
+	bool maximized;
+	bool sticky;
+	Rectangle rect;
+	Rectangle titlerect;
+	Rectangle tabrect;
+	Rectangle contrect;
+	Rectangle scrollr;
+	Rectangle textr;
+	Rectangle origrect;
+
+	// tmp
+	WinTab *tab;
+	WinTab *cur;
+};
+
+struct WinTab
+{
+	Ref;
+	bool deleted;
+	Window *w;
+	WinTab *next;
 	Image *content;
 	int id;
 	char name[32];
 	int namecount;
 	char *label;
-	int noborder;
 	int notefd;
 	char *dir;
 
-	bool notitle;
-	bool maximized;
-	bool sticky;
-	Rectangle origrect;
-	Rectangle titlerect;
-	Rectangle contrect;
-
 	Text text;
-	Rectangle scrollr;
-	Rectangle textr;
 	int holdmode;
 	bool scrolling;
 	bool wctlready;
@@ -248,6 +263,8 @@
 extern Window *bottomwin, *topwin;
 extern Window *windows[MAXWINDOWS];
 extern int nwindows;
+extern WinTab *wintabs[MAXWINDOWS];
+extern int nwintabs;
 extern Window *focused, *cursorwin;
 extern Point screenoff;
 
@@ -256,28 +273,34 @@
 void wmaximize(Window *w);
 void wrestore(Window *w);
 void wresize(Window *w, Rectangle r);
-Window *wcreate(Rectangle r, bool hidden, bool scrolling);
-void wrelease(Window *w);
-void wsendmsg(Window *w, int type);
-Window *wfind(int id);
+void wrecreate(Window *w);
 Window *wpointto(Point pt);
-void wsetcursor(Window *w);
-void wsetlabel(Window *w, char *label);
-void wsetname(Window *w);
-void wsetpid(Window *w, int pid, int dolabel);
 void wdelete(Window *w);
 void wmove(Window *w, Point pos);
 void wraise(Window *w);
 void wlower(Window *w);
 void wfocus(Window *w);
-void wremove(Window *w);
+void wunfocus(Window *w);
 int whide(Window *w);
 int wunhide(Window *w);
-void wsethold(Window *w, int hold);
 void wmovemouse(Window *w, Point pt);
-void wtype(Window *w, Rune r);
-int wincmd(Window *w, int pid, char *dir, char **argv);
 
+WinTab *wcreate(Rectangle r, bool hidden, bool scrolling);
+void wrelease(WinTab *w);
+void wsendmsg(WinTab *w, int type);
+WinTab *wfind(int id);
+void wsetcursor(WinTab *w);
+void wsetlabel(WinTab *w, char *label);
+void wsetname(WinTab *w);
+void wsetpid(WinTab *w, int pid, int dolabel);
+void wsethold(WinTab *w, int hold);
+void wtype(WinTab *w, Rune r);
+int wincmd(WinTab *w, int pid, char *dir, char **argv);
+
+WinTab *tcreate(Window *w, bool scrolling);
+void tfocus(WinTab *t);
+void tdelete(WinTab *t);
+
 void screenoffset(int offx, int offy);
 
 typedef struct Wctlcmd Wctlcmd;
@@ -295,7 +318,7 @@
 };
 
 Wctlcmd parsewctl(char *s, Rectangle r);
-char *writewctl(Window *w, char *data);
+char *writewctl(WinTab *w, char *data);
 
 
 extern Cursor *cursor;
@@ -311,7 +334,7 @@
 
 void refresh(void);
 Point dmenuhit(int but, Mousectl *mc, int nx, int ny, Point last);
-void drainmouse(Mousectl *mc, Window *w);
+void drainmouse(Mousectl *mc, WinTab *w);
 void grab(Window *w, int btn);
 void btn3menu(void);
 
--- a/main.c
+++ b/main.c
@@ -22,9 +22,9 @@
 {
 	int i;
 
-	for(i = 0; i < nwindows; i++)
-		if(windows[i]->notefd >= 0)
-			write(windows[i]->notefd, "hangup", 6);
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i]->notefd >= 0)
+			write(wintabs[i]->notefd, "hangup", 6);
 }
 
 static char *oknotes[] ={
@@ -143,10 +143,10 @@
 
 char *rcargv[] = { "rc", "-i", nil };
 
-Window*
+WinTab*
 new(Rectangle r)
 {
-	Window *w;
+	WinTab *w;
 
 	w = wcreate(r, FALSE, scrolling);
 	assert(w);
@@ -155,8 +155,21 @@
 	return w;
 }
 
+WinTab*
+newtab(Window *ww)
+{
+	WinTab *w;
+
+	w = tcreate(ww, scrolling);
+	assert(w);
+	if(wincmd(w, 0, nil, rcargv) == 0)
+		return nil;
+	return w;
+}
+
+
 void
-drainmouse(Mousectl *mc, Window *w)
+drainmouse(Mousectl *mc, WinTab *w)
 {
 	if(w) send(w->mc.c, &mc->Mouse);
 	while(mc->buttons){
@@ -163,7 +176,7 @@
 		readmouse(mc);
 		/* stop sending once focus changes.
 		 * buttons released in wfocus() */
-		if(w != focused) w = nil;
+		if(w && w->w != focused) w = nil;
 		if(w) send(w->mc.c, &mc->Mouse);
 	}
 }
@@ -365,6 +378,13 @@
 		}
 		flushimage(display, 1);
 	}
+
+//TODO(tab): temp hack
+	else{
+		Window *ww = wpointto(r.min);
+		if(w == nil && ww)
+			newtab(ww);
+	}
 }
 
 void
@@ -452,7 +472,7 @@
 
 
 void
-btn2menu(Window *w)
+btn2menu(WinTab *w)
 {
 	enum {
 		Cut,
@@ -549,7 +569,7 @@
 			continue;
 		if(t->hidden || obscured(t, t->frame->r, t->higher)){
 			hidden[nhidden] = windows[i];
-			str[nhidden+Hidden] = windows[i]->label;
+			str[nhidden+Hidden] = windows[i]->cur->label;
 			nhidden++;	
 		}
 	}
@@ -685,11 +705,13 @@
 					setcursornormal(nil);
 					wtitlectl(w);
 				}
+			}else if(w->cur == nil){
+				/* no tab in window */
 			}else if(w != focused){
 				/* inactive window */
-				wsetcursor(w);
+				wsetcursor(w->cur);
 				if(mctl->buttons & 7 ||
-				   mctl->buttons & (8|16) && focused->mouseopen){
+				   mctl->buttons & (8|16) && focused && focused->cur->mouseopen){
 					wraise(w);
 					wfocus(w);
 					if(mctl->buttons & 1)
@@ -697,24 +719,24 @@
 					else
 						goto again;
 				}
-			}else if(!w->mouseopen){
+			}else if(!w->cur->mouseopen){
 				/* active text window */
-				wsetcursor(w);
+				wsetcursor(w->cur);
 				if(mctl->buttons && topwin != w)
 					wraise(w);
-				if(mctl->buttons & (1|8|16) || ptinrect(mctl->xy, w->text.scrollr))
-					drainmouse(mctl, w);
+				if(mctl->buttons & (1|8|16) || ptinrect(mctl->xy, w->cur->text.scrollr))
+					drainmouse(mctl, w->cur);
 				if(mctl->buttons & 2){
-					incref(w);
-					btn2menu(w);
-					wrelease(w);
+					incref(w->cur);
+					btn2menu(w->cur);
+					wrelease(w->cur);
 				}
 				if(mctl->buttons & 4)
 					btn3menu();
 			}else{
 				/* active graphics window */
-				wsetcursor(w);
-				drainmouse(mctl, w);
+				wsetcursor(w->cur);
+				drainmouse(mctl, w->cur);
 			}
 		}
 	}
@@ -790,7 +812,7 @@
 	Channel *fschan, *chan;
 	int n;
 	Stringpair pair;
-	Window *cur, *prev;
+	WinTab *cur, *prev;
 	Queue tapq;
 
 	threadsetname("keyboardtap");
@@ -813,6 +835,7 @@
 			alts[Atotap].op = CHANSND;
 		else
 			alts[Atotap].op = CHANNOP;
+WinTab *ftab = focused ? focused->cur : nil;
 		switch(alt(alts)){
 		case Akbd:
 			/* from keyboard to tap or to window */
@@ -821,7 +844,7 @@
 				ctldown = utfrune(s+1, Kctl) != nil;
 			}
 			prev = cur;
-			cur = focused;
+			cur = ftab;
 			if(totap){
 				if(cur != prev && cur){
 					/* notify tap of focus change */
@@ -842,7 +865,7 @@
 
 		case Afromtap:
 			/* from tap to window */
-			if(cur && cur == focused)
+			if(cur && cur == ftab)
 				sendp(cur->kbd, s);
 			else
 				free(s);
--- a/simple.c
+++ b/simple.c
@@ -21,6 +21,8 @@
 Image *wincolors[NumWinColors];
 Image *icons[4];
 
+Image *testcol;
+
 void
 btn(Image *img, Rectangle r, Image *col, Image *icon, int down)
 {
@@ -50,6 +52,13 @@
 	return ptinrect(mctl->xy, r);
 }
 
+int
+colsel(Window *w)
+{
+	return w->cur && w->cur->holdmode ?
+		w == focused ? 2 : 3 :
+		w == focused ? 0 : 1;
+}
 
 void
 wdecor(Window *w)
@@ -56,9 +65,7 @@
 {
 	if(w->frame == nil)
 		return;
-	int c = w->holdmode ?
-		w == focused ? TITLEHOLD : LTITLEHOLD :
-		w == focused ? TITLE : LTITLE;
+	int c = TITLE + colsel(w);
 	int c2 = w == focused ? FRAME : LFRAME;
 
 	Rectangle r, b1, b2, b3;
@@ -88,20 +95,101 @@
 r.min.x += bordersz/2;
 r.min.y -= 2;
 		Point pt = Pt(r.min.x, r.min.y);
-		string(w->frame, pt, wincolors[c+4], pt, font, w->label);
+//if(w->cur && w->ref == 1)
+if(w->cur)
+		string(w->frame, pt, wincolors[c+4], pt, font, w->cur->label);
 	}
+
+	r = rectsubpt(w->tabrect, Pt(0,1));
+//	r.min.x--;
+//	r.max.x++;
+	draw(w->frame, r, wincolors[c], nil, ZP);
+
+	int n = w->ref;
+	if(n > 1){
+		int wd = Dx(r)/n;
+		int xxx = r.max.x;
+		r.max.x = r.min.x + wd;
+		for(WinTab *t = w->tab; t; t = t->next){
+			if(t->next == nil)
+				r.max.x = xxx;
+	if(t != w->cur)
+	draw(w->frame, r, testcol, nil, ZP);
+			Point pt = Pt(r.min.x+bordersz/2, r.min.y);
+			string(w->frame, pt, wincolors[c+4], pt, font, t->label);
+			r = rectaddpt(r, Pt(wd,0));
+		}
+	}
 }
 
-void
-wtitlectl(Window *w)
+Window *pick(void);
+Window *wcreate_(Rectangle r, bool hidden);
+void tmigrate(WinTab *t, Window *w);
+void tmoveleft(WinTab *t);
+void tmoveright(WinTab *t);
+
+static void
+tabctl(Window *w)
 {
+	Rectangle r;
+
 	if(mctl->buttons & 7){
 		wraise(w);
 		wfocus(w);
+		r = rectsubpt(w->tabrect, Pt(0,1));
+		int n = w->ref;
+		if(n > 1){
+			int wd = Dx(r)/n;
+			r.max.x = r.min.x + wd;
+			for(WinTab *t = w->tab; t; t = t->next){
+				if(ptinrect(mctl->xy, r)){
+					if(mctl->buttons & 1){
+						tfocus(t);
+						/* chording */
+						while(mctl->buttons){
+							int b = mctl->buttons;
+							if(b & 6){
+								if(b & 2)
+									tmoveleft(t);
+								else
+									tmoveright(t);
+							}
+							while(mctl->buttons == b)
+								readmouse(mctl);
+						}
+					}else if(mctl->buttons & 2)
+						tdelete(t);
+					else if(mctl->buttons & 4){
+Point pt = mctl->xy;
+						Window *ww = pick();
+						if(ww){
+							tmigrate(t, ww);
+							wraise(ww);
+							wfocus(ww);
+							return;
+						}else{
+							ww = wpointto(mctl->xy);
+							if(ww == nil){
+	tmigrate(t, wcreate_(rectaddpt(w->rect, subpt(mctl->xy, pt)), 0));
+							}
+						}
+					}
+					break;
+				}
+				r = rectaddpt(r, Pt(wd,0));
+			}
+		}
+	}
+}
+
+static void
+titlectl(Window *w)
+{
+	if(mctl->buttons & 7){
+		wraise(w);
+		wfocus(w);
 		if(mctl->buttons & 1) {
-			int c = w->holdmode ?
-				w == focused ? TITLEHOLD : LTITLEHOLD :
-				w == focused ? TITLE : LTITLE;
+			int c = TITLE + colsel(w);
 
 			Rectangle r = w->titlerect;
 			Rectangle br = Rect(0,0,titlesz-bordersz,titlesz-bordersz);
@@ -129,12 +217,29 @@
 			}else if(!w->maximized)
 				grab(w, 1);
 		}
-		if(mctl->buttons & 4)
-			btn3menu();
+		if(mctl->buttons & 4){
+//			btn3menu();
+			Window *ww = pick();
+			if(ww){
+				tmigrate(w->cur, ww);
+				wraise(ww);
+				wfocus(ww);
+				return;
+			}
+		}
 	}
 }
 
+void
+wtitlectl(Window *w)
+{
+	if(ptinrect(mctl->xy, w->titlerect))
+		titlectl(w);
+	else
+		tabctl(w);
+}
 
+
 static char minbtn[] = {
 	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
 	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -230,4 +335,6 @@
 	icons[1] = mkicon(maxbtn, 16, 13);
 	icons[2] = mkicon(rstbtn, 16, 13);
 	icons[3] = mkicon(closebtn, 16, 13);
+
+	testcol = getcolor("asdfasdfasdf", 0x00000020);
 }
--- a/wctl.c
+++ b/wctl.c
@@ -139,6 +139,8 @@
 Rectangle
 rectonscreen(Rectangle r)
 {
+//TODO(vdesk) this changes
+return r;
 	shift(&r.min.x, &r.max.x, screen->r.min.x, screen->r.max.x);
 	shift(&r.min.y, &r.max.y, screen->r.min.y, screen->r.max.y);
 	return r;
@@ -288,49 +290,50 @@
 }
 
 char*
-wctlcmd(Window *w, Rectangle r, int cmd)
+wctlcmd(WinTab *w, Rectangle r, int cmd)
 {
+	Window *ww = w->w;
 	switch(cmd){
 	case Move:
-		r = rectaddpt(Rect(0,0,Dx(w->frame->r),Dy(w->frame->r)), r.min);
+		r = rectaddpt(Rect(0,0,Dx(ww->frame->r),Dy(ww->frame->r)), r.min);
 		if(!goodrect(r))
 			return Ebadwr;
-		if(!eqpt(r.min, w->frame->r.min))
-			wmove(w, r.min);
+		if(!eqpt(r.min, ww->frame->r.min))
+			wmove(ww, r.min);
 		break;
 	case Resize:
 		if(!goodrect(r))
 			return Ebadwr;
-		if(!eqrect(r, w->frame->r))
-			wresize(w, r);
+		if(!eqrect(r, ww->frame->r))
+			wresize(ww, r);
 		break;
 // TODO: these three work somewhat differently in rio
 	case Top:
-		wraise(w);
+		wraise(ww);
 		break;
 	case Bottom:
-		wlower(w);
+		wlower(ww);
 		break;
 	case Current:
-		if(w->hidden)
+		if(ww->hidden)
 			return "window is hidden";
-		wfocus(w);
-		wraise(w);
+		wfocus(ww);
+		wraise(ww);
 		break;
 	case Hide:
-		switch(whide(w)){
+		switch(whide(ww)){
 		case -1: return "window already hidden";
 		case 0: return "hide failed";
 		}
 		break;
 	case Unhide:
-		switch(wunhide(w)){
+		switch(wunhide(ww)){
 		case -1: return "window not hidden";
 		case 0: return "hide failed";
 		}
 		break;
 	case Delete:
-		wdelete(w);
+		wdelete(ww);
 		break;
 	case Scroll:
 		w->scrolling = TRUE;
@@ -342,26 +345,26 @@
 		wsendmsg(w, Wakeup);
 		break;
 	case Border:
-		w->noborder &= ~1;
-		wresize(w, w->frame->r);
+		ww->noborder &= ~1;
+		wrecreate(ww);
 		break;
 	case Noborder:
-		w->noborder |= 1;
-		wresize(w, w->frame->r);
+		ww->noborder |= 1;
+		wrecreate(ww);
 		break;
 	case Title:
-		w->notitle = FALSE;
-		wresize(w, w->frame->r);
+		ww->notitle = FALSE;
+		wrecreate(ww);
 		break;
 	case Notitle:
-		w->notitle = TRUE;
-		wresize(w, w->frame->r);
+		ww->notitle = TRUE;
+		wrecreate(ww);
 		break;
 	case Sticky:
-		w->sticky = TRUE;
+		ww->sticky = TRUE;
 		break;
 	case Nosticky:
-		w->sticky = FALSE;
+		ww->sticky = FALSE;
 		break;
 	default:
 		return "invalid wctl message";
@@ -372,7 +375,7 @@
 char*
 wctlnew(Wctlcmd cmd)
 {
-	Window *w;
+	WinTab *w;
 	char *argv[4], **args;
 
 	w = wcreate(cmd.r, cmd.hidden, cmd.scrolling);
@@ -399,7 +402,7 @@
 }
 
 char*
-writewctl(Window *w, char *data)
+writewctl(WinTab *w, char *data)
 {
 	Rectangle r;
 	Wctlcmd cmd;
@@ -407,7 +410,7 @@
 	if(w == nil)
 		r = ZR;
 	else
-		r = rectsubpt(w->frame->r, screen->r.min);
+		r = rectsubpt(w->w->frame->r, screen->r.min);
 	cmd = parsewctl(data, r);
 	if(cmd.error)
 		return cmd.error;
--- a/win3.c
+++ b/win3.c
@@ -97,7 +97,7 @@
 
 
 void
-wdecor(Window *w)
+wdecor(WinTab *w)
 {
 	if(w->frame == nil)
 		return;
@@ -155,7 +155,7 @@
 }
 
 void
-wtitlectl(Window *w)
+wtitlectl(WinTab *w)
 {
 	if(mctl->buttons & 7){
 		wraise(w);
--- a/win95.c
+++ b/win95.c
@@ -69,7 +69,7 @@
 }
 
 void
-wdecor(Window *w)
+wdecor(WinTab *w)
 {
 	if(w->frame == nil)
 		return;
@@ -105,7 +105,7 @@
 }
 
 void
-wtitlectl(Window *w)
+wtitlectl(WinTab *w)
 {
 	if(mctl->buttons & 7){
 		wraise(w);
--- a/wind.c
+++ b/wind.c
@@ -3,6 +3,8 @@
 Window *bottomwin, *topwin;
 Window *windows[MAXWINDOWS];
 int nwindows;
+WinTab *wintabs[MAXWINDOWS];
+int nwintabs;
 Window *focused, *cursorwin;
 
 Point screenoff;
@@ -62,9 +64,10 @@
 	wresize(w, w->origrect);
 }
 
-void
+static void
 wcalcrects(Window *w, Rectangle r)
 {
+	w->rect = r;
 	w->contrect = r;
 	if(!w->noborder)
 		w->contrect = insetrect(w->contrect, bordersz);
@@ -76,6 +79,13 @@
 		w->contrect.min.y += titlesz;
 	}
 
+	w->tabrect = ZR;
+	if(w->ref > 1){
+		w->tabrect = w->contrect;
+		w->tabrect.max.y = w->tabrect.min.y + 18;
+		w->contrect.min.y += 18;
+	}
+
 	r = insetrect(w->contrect, 1);
 	w->scrollr = r;
 	w->scrollr.max.x = w->scrollr.min.x + 12;
@@ -83,100 +93,191 @@
 	w->textr.min.x = w->scrollr.max.x + 4;
 }
 
-void
-wsetcolors(Window *w)
+static void
+wsetcolors(WinTab *w)
 {
 	int c = w->holdmode ?
-		w == focused ? HOLDTEXT : PALEHOLDTEXT :
-		w == focused ? TEXT : PALETEXT;
+		w->w == focused ? HOLDTEXT : PALEHOLDTEXT :
+		w->w == focused ? TEXT : PALETEXT;
 	w->text.cols[TEXT] = colors[c];
 }
 
 static void
-wfreeimages(Window *w)
+tinit(WinTab *t)
 {
-	if(w->frame == nil)
+	Window *w;
+
+	if(t->deleted)
 		return;
+	w = t->w;
+	t->mc.image = w->frame;
+	t->content = allocwindow(w->screen, w->contrect, Refbackup, DNofill);
+	assert(t->content);
+	draw(t->content, t->content->r, colors[BACK], nil, ZP);
+	xinit(&t->text, w->textr, w->scrollr, font, t->content, colors);
+}
 
-	Screen *s = w->content->screen;
-	freeimage(w->content);
-	w->content = nil;
+static void
+tdeinit(WinTab *t)
+{
+	/* have to move image out of the way
+	 * because client program can hold a ref
+	 * and mess up our drawing. */
+	if(t->content)
+		originwindow(t->content, t->content->r.min, screen->r.max);
+	freeimage(t->content);
+	t->content = nil;
+	t->mc.image = nil;
+}
 
-	freescreen(s);
+/* get rid of window visually */
+static void
+wremove(Window *w)
+{
+	if(w->frame)
+		originwindow(w->frame, w->frame->r.min, screen->r.max);
+}
 
+static void
+wfreeimages(Window *w)
+{
+	freescreen(w->screen);
+	w->screen = nil;
+
 	freeimage(w->frame);
 	w->frame = nil;
-	w->mc.image = nil;
 }
 
+/* create images, destroy first if they exist.
+ * either only tab images e.g. when the tab bar appears/disappears
+ * or full window when window is resized/moved. */
 static void
-wsetsize(Window *w, Rectangle r)
+wreinit(Window *w, bool all)
 {
-	Rectangle hr;
-	Screen *s;
+	Rectangle r, hr;
 
-	wfreeimages(w);
-	if(w->hidden){
-		hr = rectaddpt(r, subpt(screen->r.max, r.min));
-		w->frame = allocwindow(wscreen, hr, Refbackup, DNofill);
-		originwindow(w->frame, r.min, hr.min);
-	}else
-		w->frame = allocwindow(wscreen, r, Refbackup, DNofill);
-	w->mc.image = w->frame;
-	s = allocscreen(w->frame, colors[BACK], 0);
-	assert(s);
-	wcalcrects(w, r);
-	w->content = allocwindow(s, w->contrect, Refnone, DNofill);
-	assert(w->content);
-	draw(w->frame, w->frame->r, colors[BACK], nil, ZP);
-	xinit(&w->text, w->textr, w->scrollr, font, w->content, colors);
+	for(WinTab *t = w->tab; t; t = t->next)
+		tdeinit(t);
 
-	wrepaint(w);
+	if(all){
+		r = w->rect;
+		/* reference can be held by client program
+		 * indefinitely which would keep this on screen. */
+		wremove(w);
+		wfreeimages(w);
+		if(w->hidden){
+			hr = rectaddpt(r, subpt(screen->r.max, r.min));
+			w->frame = allocwindow(wscreen, hr, Refbackup, DNofill);
+			originwindow(w->frame, r.min, hr.min);
+		}else
+			w->frame = allocwindow(wscreen, r, Refbackup, DNofill);
+		w->screen = allocscreen(w->frame, colors[BACK], 0);
+		assert(w->screen);
+	}
+
+	for(WinTab *t = w->tab; t; t = t->next)
+		tinit(t);
+
+	tfocus(w->cur);
 }
 
+// TODO: find better name
+void
+wrecreate(Window *w)
+{
+	wcalcrects(w, w->rect);
+	wreinit(w, 0);
+	for(WinTab *t = w->tab; t; t = t->next)
+		wsendmsg(t, Resized);
+}
+
+
 static int id = 1;
 
 Window*
-wcreate(Rectangle r, bool hidden, bool scrolling)
+wcreate_(Rectangle r, bool hidden)
 {
 	Window *w;
 
 	w = emalloc(sizeof(Window));
-	incref(w);
-	w->id = id++;
-	w->notefd = -1;
-	wsetlabel(w, "<unnamed>");
-	w->dir = estrdup(startdir);
 	w->hidden = hidden;
-	w->scrolling = scrolling;
 	w->notitle = notitle;	// TODO: argument?
-	wsetsize(w, r);
+	wcalcrects(w, r);
+	wreinit(w, 1);
 	wlistpushfront(w);
 	// TODO: could be more graceful here
 	assert(nwindows < MAXWINDOWS);
 	windows[nwindows++] = w;
 
-	w->mc.c = chancreate(sizeof(Mouse), 16);
-	w->gone = chancreate(sizeof(int), 0);
-	w->kbd = chancreate(sizeof(char*), 16);
-	w->ctl = chancreate(sizeof(int), 0);
-	w->conswrite = chancreate(sizeof(Channel**), 0);
-	w->consread = chancreate(sizeof(Channel**), 0);
-	w->kbdread = chancreate(sizeof(Channel**), 0);
-	w->mouseread = chancreate(sizeof(Channel**), 0);
-	w->wctlread = chancreate(sizeof(Channel**), 0);
-	w->complete = chancreate(sizeof(Completion*), 0);
-	threadcreate(winthread, w, mainstacksize);
-
-	wsetname(w);
 	wfocus(w);
 
 	return w;
 }
 
+void
+tfocus(WinTab *t)
+{
+	if(t == nil || t->deleted)
+		return;
+	t->w->cur = t;
+	topwindow(t->content);
+	wrepaint(t->w);
+}
+
+WinTab*
+tcreate(Window *w, bool scrolling)
+{
+	WinTab *t, **tp;
+
+	/* recreate window when tab bar appears
+	 * before we attach the new tab. */
+	incref(w);
+	if(w->ref == 2)
+		wrecreate(w);
+
+	t = emalloc(sizeof(WinTab));
+	incref(t);
+	t->w = w;
+	for(tp = &w->tab; *tp; tp = &(*tp)->next);
+	*tp = t;
+	tinit(t);
+	t->id = id++;
+	t->notefd = -1;
+	wsetlabel(t, "<unnamed>");
+	t->dir = estrdup(startdir);
+	t->scrolling = scrolling;
+
+	t->mc.c = chancreate(sizeof(Mouse), 16);
+	t->gone = chancreate(sizeof(int), 0);
+	t->kbd = chancreate(sizeof(char*), 16);
+	t->ctl = chancreate(sizeof(int), 0);
+	t->conswrite = chancreate(sizeof(Channel**), 0);
+	t->consread = chancreate(sizeof(Channel**), 0);
+	t->kbdread = chancreate(sizeof(Channel**), 0);
+	t->mouseread = chancreate(sizeof(Channel**), 0);
+	t->wctlread = chancreate(sizeof(Channel**), 0);
+	t->complete = chancreate(sizeof(Completion*), 0);
+	threadcreate(winthread, t, mainstacksize);
+
+	wsetname(t);
+	// TODO: could be more graceful here
+	assert(nwintabs < MAXWINDOWS);
+	wintabs[nwintabs++] = t;
+
+	tfocus(t);
+
+	return t;
+}
+
+WinTab*
+wcreate(Rectangle r, bool hidden, bool scrolling)
+{
+	return tcreate(wcreate_(r, hidden), scrolling);
+}
+
 /* called from winthread when it exits */
 static void
-wfree(Window *w)
+wfree(WinTab *w)
 {
 	if(w->notefd >= 0)
 		close(w->notefd);
@@ -195,18 +296,13 @@
 	free(w);
 }
 
-/* logically and visually close the window.
- * struct and thread will stick around until all references are gone.
- * safe to call multiple times. */
 static void
-wclose(Window *w)
+wdestroy(Window *w)
 {
 	int i;
 
-	if(w->deleted)
-		return;
-	w->deleted = TRUE;
 	assert(w != focused);	/* this must be done elsewhere */
+	assert(w->tab == nil);
 	wlistremove(w);
 	for(i = 0; i < nwindows; i++)
 		if(windows[i] == w){
@@ -215,47 +311,212 @@
 			break;
 		}
 	wfreeimages(w);
+	free(w);
 	flushimage(display, 1);
 }
 
 int
-inwinthread(Window *w)
+inwinthread(WinTab *w)
 {
 	return w->threadname == threadgetname();
 }
 
-/* decrement reference, close window once all references gone. */
-void
-wrelease(Window *w)
+/* decrement reference, close window once all tabs gone. */
+static int
+wrelease_(Window *w)
 {
 	if(decref(w) == 0){
+		assert(w->tab == nil);
+		assert(w->cur == nil);
+		wremove(w);
+		wunfocus(w);
+		assert(w != focused);
+		wdestroy(w);
+		return 0;
+	}else{
+		assert(w->ref > 0);
+		return w->ref;
+	}
+}
+
+/* logically and visually close the tab.
+ * struct, thread and link into window will stick
+ * around until all references are gone.
+ * safe to call multiple times. */
+static void
+tclose(WinTab *w)
+{
+	int i;
+
+	if(w->deleted)
+		return;
+	w->deleted = TRUE;
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i] == w){
+			nwintabs--;
+			memmove(&wintabs[i], &wintabs[i+1], (nwintabs-i)*sizeof(WinTab*));
+			break;
+		}
+	tdeinit(w);
+}
+
+/* detach tab from window */
+void
+tdetach(WinTab *t)
+{
+	WinTab **tp;
+	Window *w = t->w;
+
+	if(w == nil)
+		return;
+
+	/* remove tab from window */
+	for(tp = &w->tab; *tp; tp = &(*tp)->next){
+		if(*tp == t){
+			(*tp) = t->next;
+			t->next = nil;
+			t->w = nil;
+			break;
+		}
+	}
+	assert(t->w == nil);
+	tdeinit(t);
+
+	/* find new focused tab */
+	if(w->cur == t){
+		w->cur = *tp;
+		if(w->cur == nil)
+			for(w->cur = w->tab;
+			    w->cur && w->cur->next;
+			    w->cur = w->cur->next);
+	}
+	if(wrelease_(w) > 0){
+		/* complete redraw if tab bar disappears */
+		if(w->ref == 1)
+			wrecreate(w);
+		else
+			tfocus(w->cur);
+	}
+}
+
+void
+tmigrate(WinTab *t, Window *w)
+{
+	WinTab **tp;
+
+	if(t->w == w)
+		return;
+	tdetach(t);
+
+	/* recreate window when tab bar appears
+	 * before we attach the new tab. */
+	incref(w);
+	if(w->ref == 2)
+		wrecreate(w);
+
+	t->w = w;
+	for(tp = &w->tab; *tp; tp = &(*tp)->next);
+	*tp = t;
+	tinit(t);
+
+	tfocus(t);
+	wsendmsg(t, Resized);
+}
+
+/* this SUCKS, want doubly linked lists */
+static WinTab**
+getprevptr(WinTab *t)
+{
+	WinTab **tp;
+	for(tp = &t->w->tab; *tp; tp = &(*tp)->next)
+		if(*tp == t)
+			return tp;
+	return nil;
+}
+static WinTab*
+getprev(WinTab *t)
+{
+	WinTab *tt;
+	for(tt = t->w->tab; tt; tt = tt->next)
+		if(tt->next == t)
+			return tt;
+	return nil;
+}
+
+static void
+tswapadjacent(WinTab *l, WinTab *r)
+{
+	WinTab **tp;
+
+	tp = getprevptr(l);
+	assert(tp);
+	l->next = r->next;
+	r->next = l;
+	*tp = r;
+	wdecor(l->w);
+}
+
+void
+tmoveleft(WinTab *r)
+{
+	WinTab *l;
+	l = getprev(r);
+	if(l == nil) return;
+	tswapadjacent(l, r);
+}
+
+void
+tmoveright(WinTab *l)
+{
+	WinTab *r;
+	r = l->next;
+	if(r == nil) return;
+	tswapadjacent(l, r);
+}
+
+/* decrement reference, close tab once all references gone. */
+void
+wrelease(WinTab *t)
+{
+	if(decref(t) == 0){
 		/* increment ref count temporarily
 		 * so win thread doesn't exit too early */
-		incref(w);
-		wremove(w);
-		wclose(w);
-		decref(w);
-		if(!inwinthread(w))
-			wsendmsg(w, Wakeup);
+		incref(t);
+		tdetach(t);
+		tclose(t);
+		decref(t);
+		if(!inwinthread(t))
+			wsendmsg(t, Wakeup);
 	}else
-		assert(w->ref > 0);
+		assert(t->ref > 0);
 }
 
 void
-wsendmsg(Window *w, int type)
+tdelete(WinTab *t)
 {
+	assert(!t->deleted);
+	tdetach(t);
+	tclose(t);
+
+	wsendmsg(t, Deleted);
+}
+
+
+void
+wsendmsg(WinTab *w, int type)
+{
 	assert(!inwinthread(w));
 	sendul(w->ctl, type);
 }
 
-Window*
+WinTab*
 wfind(int id)
 {
 	int i;
 
-	for(i = 0; i < nwindows; i++)
-		if(windows[i]->id == id)
-			return windows[i];
+	for(i = 0; i < nwintabs; i++)
+		if(wintabs[i]->id == id)
+			return wintabs[i];
 	return nil;
 }
 
@@ -271,22 +532,22 @@
 }
 
 void
-wsetcursor(Window *w)
+wsetcursor(WinTab *w)
 {
-	if(w == cursorwin)
+	if(w->w == cursorwin)
 		setcursornormal(w->holdmode ? &whitearrow : w->cursorp);
 }
 
 void
-wsetlabel(Window *w, char *label)
+wsetlabel(WinTab *w, char *label)
 {
 	free(w->label);
 	w->label = estrdup(label);
-	wdecor(w);
+	wdecor(w->w);
 }
 
 void
-wsetname(Window *w)
+wsetname(WinTab *w)
 {
 	int i, n;
 	char err[ERRMAX];
@@ -306,7 +567,7 @@
 }
 
 void
-wsetpid(Window *w, int pid, int dolabel)
+wsetpid(WinTab *w, int pid, int dolabel)
 {
 	char buf[32];
 	int ofd;
@@ -330,16 +591,18 @@
 wdelete(Window *w)
 {
 	wremove(w);
-	wsendmsg(w, Deleted);
+	wunfocus(w);
+	for(WinTab *t = w->tab; t; t = t->next)
+		wsendmsg(t, Deleted);
 }
 
 static void
 wrepaint(Window *w)
 {
-	wsetcolors(w);
+	wsetcolors(w->cur);
 	wdecor(w);
-	if(!w->mouseopen)
-		xredraw(&w->text);
+	if(!w->cur->mouseopen)
+		xredraw(&w->cur->text);
 }
 
 /* restore window order after reshaping has disturbed it */
@@ -355,10 +618,12 @@
 void
 wresize(Window *w, Rectangle r)
 {
-	wsetsize(w, r);
+	wcalcrects(w, r);
+	wreinit(w, 1);
 	if(w != topwin && !w->hidden)
 		worder();
-	wsendmsg(w, Resized);
+	for(WinTab *t = w->tab; t; t = t->next)
+		wsendmsg(t, Resized);
 }
 
 void
@@ -386,15 +651,16 @@
 	flushimage(display, 1);
 }
 
-void
+static void
 wfocuschanged(Window *w)
 {
-	if(w == nil)
+// TODO(tab):
+	if(w == nil || w->cur == nil)
 		return;
-	w->wctlready = TRUE;
+	w->cur->wctlready = TRUE;
 	wrepaint(w);
-	if(!inwinthread(w))
-		wsendmsg(w, Wakeup);
+	if(!inwinthread(w->cur))
+		wsendmsg(w->cur, Wakeup);
 }
 
 void
@@ -406,15 +672,17 @@
 		return;
 	prev = focused;
 	focused = w;
-	if(prev){
+	if(prev && prev->cur){
+// TODO(tab): check this
+		WinTab *t = prev->cur;
 		/* release keys (if possible) */
 		char *s = estrdup("K");
-		if(nbsendp(prev->kbd, s) != 1)
+		if(nbsendp(t->kbd, s) != 1)
 			free(s);
 		/* release mouse buttons */
-		if(prev->mc.buttons){
-			prev->mc.buttons = 0;
-			prev->mq.counter++;
+		if(t->mc.buttons){
+			t->mc.buttons = 0;
+			t->mq.counter++;
 		}
 	}
 	wfocuschanged(prev);
@@ -421,48 +689,47 @@
 	wfocuschanged(focused);
 }
 
-/* Take away focus but also get rid of the window visually.
- * For hiding/deleting */
 void
-wremove(Window *w)
+wunfocus(Window *w)
 {
-	if(!w->deleted)
-		originwindow(w->frame, w->frame->r.min, screen->r.max);
 	if(w == focused)
 		wfocus(nil);
 }
 
+// TODO(tab): wctl ready everyone?
 int
 whide(Window *w)
 {
 	if(w->hidden)
 		return -1;
-	incref(w);
+	incref(w->tab);
 	wremove(w);
+	wunfocus(w);
 	w->hidden = TRUE;
-	w->wctlready = TRUE;
-	wsendmsg(w, Wakeup);
-	wrelease(w);
+	w->tab->wctlready = TRUE;
+	wsendmsg(w->tab, Wakeup);
+	wrelease(w->tab);
 	return 1;
 }
 
+// TODO(tab): wctl ready everyone?
 int
 wunhide(Window *w)
 {
 	if(!w->hidden)
 		return -1;
-	incref(w);
+	incref(w->tab);
 	w->hidden = FALSE;
-	w->wctlready = TRUE;
+	w->tab->wctlready = TRUE;
 	originwindow(w->frame, w->frame->r.min, w->frame->r.min);
 	wraise(w);
 	wfocus(w);
-	wrelease(w);
+	wrelease(w->tab);
 	return 1;
 }
 
 void
-wsethold(Window *w, int hold)
+wsethold(WinTab *w, int hold)
 {
 	int switched;
 
@@ -472,7 +739,7 @@
 		switched = --w->holdmode == 0;
 	if(switched){
 		wsetcursor(w);
-		wrepaint(w);
+		wrepaint(w->w);
 	}
 }
 
@@ -508,7 +775,7 @@
 {
 	char	*dir;
 	char	*str;
-	Window	*win;
+	WinTab	*win;
 };
 
 static void
@@ -532,7 +799,7 @@
 }
 
 static int
-windfilewidth(Window *w, uint q0, int oneelement)
+windfilewidth(WinTab *w, uint q0, int oneelement)
 {
 	uint q;
 	Rune r;
@@ -550,7 +817,7 @@
 }
 
 static void
-namecomplete(Window *w)
+namecomplete(WinTab *w)
 {
 	Text *x;
 	int nstr, npath;
@@ -590,7 +857,7 @@
 }
 
 static void
-showcandidates(Window *w, Completion *c)
+showcandidates(WinTab *w, Completion *c)
 {
 	Text *x;
 	int i;
@@ -634,7 +901,7 @@
 }
 
 void
-wkeyctl(Window *w, Rune r)
+wkeyctl(WinTab *w, Rune r)
 {
 	Text *x;
 	int nlines, n;
@@ -726,7 +993,7 @@
 }
 
 void
-wmousectl(Window *w)
+wmousectl(WinTab *w)
 {
 	int but;
 
@@ -747,19 +1014,22 @@
 }
 
 void
-winctl(Window *w, int type)
+winctl(WinTab *w, int type)
 {
 	Text *x;
 	int i;
 
-	x = &w->text;
-	switch(type){
-	case Deleted:
+	if(type == Deleted){
 		if(w->notefd >= 0)
 			write(w->notefd, "hangup", 6);
-		wclose(w);
-		break;
+		tclose(w);
+		return;
+	}
+	if(w->deleted)
+		return;
 
+	x = &w->text;
+	switch(type){
 	case Resized:
 		wsetname(w);
 		w->resized = TRUE;
@@ -770,8 +1040,6 @@
 
 	case Refresh:
 		/* take over window again */
-		if(w->deleted)
-			break;
 		draw(w->content, w->content->r, x->cols[BACK], nil, ZP);
 		xfullredraw(&w->text);
 		break;
@@ -797,7 +1065,7 @@
 static void
 winthread(void *arg)
 {
-	Window *w;
+	WinTab *w;
 	Text *x;
 	Rune r, *rp;
 	char *s;
@@ -965,11 +1233,13 @@
 		case AWctlRead:
 			w->wctlready = FALSE;
 			recv(fsc, &pair);
+Window *ww = w->w;
 			pair.ns = snprint(pair.s, pair.ns, "%11d %11d %11d %11d %11s %11s ",
-				w->frame->r.min.x, w->frame->r.min.y,
-				w->frame->r.max.x, w->frame->r.max.y,
-				w == focused ? "current" : "notcurrent",
-				w->hidden ? "hidden" : "visible");
+				ww->frame->r.min.x, ww->frame->r.min.y,
+				ww->frame->r.max.x, ww->frame->r.max.y,
+				ww->cur == w ? ww == focused ? "current" : "notcurrent"
+					: "tab",
+				ww->hidden ? "hidden" : "visible");
 			send(fsc, &pair);
 			break;
 
@@ -978,7 +1248,7 @@
 			break;
 
 		case AComplete:
-			if(w->frame){
+			if(w->content){
 				if(!comp->advance)
 					showcandidates(w, comp);
 				if(comp->advance){
@@ -1010,7 +1280,7 @@
 static void
 shellproc(void *args)
 {
-	Window *w;
+	WinTab *w;
 	Channel *pidc;
 	void **arg;
 	char *cmd, *dir;
@@ -1054,7 +1324,7 @@
 }
 
 int
-wincmd(Window *w, int pid, char *dir, char **argv)
+wincmd(WinTab *w, int pid, char *dir, char **argv)
 {
 	Channel *cpid;
 	void *args[5];
@@ -1071,7 +1341,7 @@
 		pid = recvul(cpid);
 		chanfree(cpid);
 		if(pid == 0){
-			wdelete(w);
+			wdelete(w->w);
 			return 0;
 		}
 	}