From 07109bb861018f4de7785c082262c1e0ff3fede0 Mon Sep 17 00:00:00 2001 From: Sam Chudnick Date: Thu, 16 Feb 2023 20:53:20 -0500 Subject: initial commit --- LICENSE | 23 + Makefile | 69 ++ README.md | 24 + arg.h | 48 + config.h | 79 ++ patches/alpha.diff | 122 ++ patches/tabbed-autohide-0.6.diff | 50 + patches/tabbed-bar-height-0.6.diff | 24 + patches/tabbed-clientnumber-0.6.diff | 23 + patches/tabbed-xresources-20210317-dabf6a2.diff | 178 +++ tabbed.1 | 171 +++ tabbed.c | 1503 +++++++++++++++++++++++ xembed.1 | 35 + xembed.c | 45 + 14 files changed, 2394 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 arg.h create mode 100644 config.h create mode 100644 patches/alpha.diff create mode 100644 patches/tabbed-autohide-0.6.diff create mode 100644 patches/tabbed-bar-height-0.6.diff create mode 100644 patches/tabbed-clientnumber-0.6.diff create mode 100644 patches/tabbed-xresources-20210317-dabf6a2.diff create mode 100644 tabbed.1 create mode 100644 tabbed.c create mode 100644 xembed.1 create mode 100644 xembed.c diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8e9678 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT/X Consortium License + +© 2009-2011 Enno Boland +© 2011,2015 Connor Lane Smith +© 2012-2015 Christoph Lohmann <20h@r-36.net> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f9a5d9e --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +.POSIX: + +NAME = tabbed +VERSION = 0.7 + +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man +DOCPREFIX = ${PREFIX}/share/doc/${NAME} + +# use system flags. +TABBED_CFLAGS = -I/usr/X11R6/include -I/usr/include/freetype2 ${CFLAGS} +TABBED_LDFLAGS = -L/usr/X11R6/lib -lX11 -lfontconfig -lXft -lXrender ${LDFLAGS} +TABBED_CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE + +# OpenBSD (uncomment) +#TABBED_CFLAGS = -I/usr/X11R6/include -I/usr/X11R6/include/freetype2 ${CFLAGS} + +SRC = tabbed.c xembed.c +OBJ = ${SRC:.c=.o} +BIN = ${OBJ:.o=} +MAN1 = ${BIN:=.1} +HDR = arg.h config.def.h +DOC = LICENSE README + +all: ${BIN} + +.c.o: + ${CC} -o $@ -c $< ${TABBED_CFLAGS} ${TABBED_CPPFLAGS} + +${OBJ}: config.h + +config.h: + cp config.def.h $@ + +.o: + ${CC} -o $@ $< ${TABBED_LDFLAGS} + +clean: + rm -f ${BIN} ${OBJ} "${NAME}-${VERSION}.tar.gz" + +dist: clean + mkdir -p "${NAME}-${VERSION}" + cp -fR Makefile ${MAN1} ${DOC} ${HDR} ${SRC} "${NAME}-${VERSION}" + tar -cf - "${NAME}-${VERSION}" | gzip -c > "${NAME}-${VERSION}.tar.gz" + rm -rf ${NAME}-${VERSION} + +install: all + # installing executable files. + mkdir -p "${DESTDIR}${PREFIX}/bin" + cp -f ${BIN} "${DESTDIR}${PREFIX}/bin" + for f in ${BIN}; do chmod 755 "${DESTDIR}${PREFIX}/bin/$$f"; done + # installing doc files. + mkdir -p "${DESTDIR}${DOCPREFIX}" + cp -f README "${DESTDIR}${DOCPREFIX}" + # installing manual pages for general commands: section 1. + mkdir -p "${DESTDIR}${MANPREFIX}/man1" + for m in ${MAN1}; do sed "s/VERSION/${VERSION}/g" < $$m > "${DESTDIR}${MANPREFIX}/man1/$$m"; done + +uninstall: + # removing executable files. + for f in ${BIN}; do rm -f "${DESTDIR}${PREFIX}/bin/$$f"; done + # removing doc files. + rm -f "${DESTDIR}${DOCPREFIX}/README" + # removing manual pages. + for m in ${MAN1}; do rm -f "${DESTDIR}${MANPREFIX}/man1/$$m"; done + -rmdir "${DESTDIR}${DOCPREFIX}" + +.PHONY: all clean dist install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..5620608 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# tabbed - generic tabbed interface + +tabbed is a simple tabbed X window container. + +## Installation + +``` +apt install libx11-dev +git clone https://gitea.chudnick.com/sam/tabbed +cd tabbed +sudo make install +``` + +## Running tabbed + +See the man page for details. + +## patches + +- alpha +- autohide +- bar-height +- clientnumber +- xresources diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..ba3fb3f --- /dev/null +++ b/arg.h @@ -0,0 +1,48 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.h b/config.h new file mode 100644 index 0000000..e5a5a9b --- /dev/null +++ b/config.h @@ -0,0 +1,79 @@ +/* See LICENSE file for copyright and license details. */ + +/* appearance */ +static char font[] = "monospace:size=9"; +static char* normbgcolor = "#222222"; +static char* normfgcolor = "#cccccc"; +static char* selbgcolor = "#555555"; +static char* selfgcolor = "#ffffff"; +static char* urgbgcolor = "#111111"; +static char* urgfgcolor = "#cc0000"; +static const char before[] = "<"; +static const char after[] = ">"; +static const char titletrim[] = "..."; +static const int tabwidth = 200; +static const Bool foreground = True; +static Bool urgentswitch = False; +static const int barHeight = 24; + +/* + * Where to place a new tab when it is opened. When npisrelative is True, + * then the current position is changed + newposition. If npisrelative + * is False, then newposition is an absolute position. + */ +static int newposition = 0; +static Bool npisrelative = False; + +#define SETPROP(p) { \ + .v = (char *[]){ "/bin/sh", "-c", \ + "prop=\"`xwininfo -children -id $1 | grep '^ 0x' |" \ + "sed -e's@^ *\\(0x[0-9a-f]*\\) \"\\([^\"]*\\)\".*@\\1 \\2@' |" \ + "xargs -0 printf %b | dmenu -l 10 -w $1`\" &&" \ + "xprop -id $1 -f $0 8s -set $0 \"$prop\"", \ + p, winid, NULL \ + } \ +} + +/* + * Xresources preferences to load at startup + */ +ResourcePref resources[] = { + { "font", STRING, &font }, + { "color0", STRING, &normbgcolor }, + { "color4", STRING, &normfgcolor }, + { "color4", STRING, &selbgcolor }, + { "color7", STRING, &selfgcolor }, + { "color2", STRING, &urgbgcolor }, + { "color3", STRING, &urgfgcolor }, +}; + +#define MODKEY ControlMask +static const Key keys[] = { + /* modifier key function argument */ + { MODKEY, XK_t, spawn, { 0 } }, + { MODKEY|ShiftMask, XK_t, killclient, { 0 } }, + + { MODKEY|ShiftMask, XK_l, rotate, { .i = +1 } }, + { MODKEY|ShiftMask, XK_h, rotate, { .i = -1 } }, + { MODKEY|ShiftMask, XK_j, movetab, { .i = -1 } }, + { MODKEY|ShiftMask, XK_k, movetab, { .i = +1 } }, + { MODKEY, XK_Tab, rotate, { .i = 0 } }, + + { MODKEY, XK_grave, spawn, SETPROP("_TABBED_SELECT_TAB") }, + { MODKEY, XK_1, move, { .i = 0 } }, + { MODKEY, XK_2, move, { .i = 1 } }, + { MODKEY, XK_3, move, { .i = 2 } }, + { MODKEY, XK_4, move, { .i = 3 } }, + { MODKEY, XK_5, move, { .i = 4 } }, + { MODKEY, XK_6, move, { .i = 5 } }, + { MODKEY, XK_7, move, { .i = 6 } }, + { MODKEY, XK_8, move, { .i = 7 } }, + { MODKEY, XK_9, move, { .i = 8 } }, + { MODKEY, XK_0, move, { .i = 9 } }, + + + { MODKEY, XK_u, focusurgent, { 0 } }, + { MODKEY|ShiftMask, XK_u, toggle, { .v = (void*) &urgentswitch } }, + + { 0, XK_F11, fullscreen, { 0 } }, +}; diff --git a/patches/alpha.diff b/patches/alpha.diff new file mode 100644 index 0000000..3ce77a7 --- /dev/null +++ b/patches/alpha.diff @@ -0,0 +1,122 @@ +diff --git a/config.mk b/config.mk +index 3a71529..095cead 100644 +--- a/config.mk ++++ b/config.mk +@@ -9,7 +9,7 @@ MANPREFIX = ${PREFIX}/share/man + + # includes and libs + INCS = -I. -I/usr/include -I/usr/include/freetype2 +-LIBS = -L/usr/lib -lc -lX11 -lfontconfig -lXft ++LIBS = -L/usr/lib -lc -lX11 -lfontconfig -lXft -lXrender + + # flags + CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE +diff --git a/tabbed.c b/tabbed.c +index 9a44795..b4d47d1 100644 +--- a/tabbed.c ++++ b/tabbed.c +@@ -170,6 +170,9 @@ static char **cmd; + static char *wmname = "tabbed"; + static const char *geometry; + ++static Colormap cmap; ++static Visual *visual = NULL; ++ + char *argv0; + + /* configuration, allows nested code to access above variables */ +@@ -255,8 +258,8 @@ configurenotify(const XEvent *e) + ww = ev->width; + wh = ev->height; + XFreePixmap(dpy, dc.drawable); +- dc.drawable = XCreatePixmap(dpy, root, ww, wh, +- DefaultDepth(dpy, screen)); ++ dc.drawable = XCreatePixmap(dpy, win, ww, wh, ++ 32); + if (sel > -1) + resize(sel, ww, wh - bh); + XSync(dpy, False); +@@ -399,7 +402,7 @@ drawtext(const char *text, XftColor col[ColLast]) + ; + } + +- d = XftDrawCreate(dpy, dc.drawable, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen)); ++ d = XftDrawCreate(dpy, dc.drawable, visual, cmap); + XftDrawStringUtf8(d, &col[ColFG], dc.font.xfont, x, y, (XftChar8 *) buf, len); + XftDrawDestroy(d); + } +@@ -564,7 +567,7 @@ getcolor(const char *colstr) + { + XftColor color; + +- if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), DefaultColormap(dpy, screen), colstr, &color)) ++ if (!XftColorAllocName(dpy, visual, cmap, colstr, &color)) + die("%s: cannot allocate color '%s'\n", argv0, colstr); + + return color; +@@ -1016,18 +1019,60 @@ setup(void) + wy = dh + wy - wh - 1; + } + ++ XVisualInfo *vis; ++ XRenderPictFormat *fmt; ++ int nvi; ++ int i; ++ ++ XVisualInfo tpl = { ++ .screen = screen, ++ .depth = 32, ++ .class = TrueColor ++ }; ++ ++ vis = XGetVisualInfo(dpy, VisualScreenMask | VisualDepthMask | VisualClassMask, &tpl, &nvi); ++ for(i = 0; i < nvi; i ++) { ++ fmt = XRenderFindVisualFormat(dpy, vis[i].visual); ++ if (fmt->type == PictTypeDirect && fmt->direct.alphaMask) { ++ visual = vis[i].visual; ++ break; ++ } ++ } ++ ++ XFree(vis); ++ ++ if (! visual) { ++ fprintf(stderr, "Couldn't find ARGB visual.\n"); ++ exit(1); ++ } ++ ++ cmap = XCreateColormap( dpy, root, visual, None); + dc.norm[ColBG] = getcolor(normbgcolor); + dc.norm[ColFG] = getcolor(normfgcolor); + dc.sel[ColBG] = getcolor(selbgcolor); + dc.sel[ColFG] = getcolor(selfgcolor); + dc.urg[ColBG] = getcolor(urgbgcolor); + dc.urg[ColFG] = getcolor(urgfgcolor); +- dc.drawable = XCreatePixmap(dpy, root, ww, wh, +- DefaultDepth(dpy, screen)); +- dc.gc = XCreateGC(dpy, root, 0, 0); + +- win = XCreateSimpleWindow(dpy, root, wx, wy, ww, wh, 0, +- dc.norm[ColFG].pixel, dc.norm[ColBG].pixel); ++ XSetWindowAttributes attrs; ++ attrs.background_pixel = dc.norm[ColBG].pixel; ++ attrs.border_pixel = dc.norm[ColFG].pixel; ++ attrs.bit_gravity = NorthWestGravity; ++ attrs.event_mask = FocusChangeMask | KeyPressMask ++ | ExposureMask | VisibilityChangeMask | StructureNotifyMask ++ | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; ++ attrs.background_pixmap = None ; ++ attrs.colormap = cmap; ++ ++ win = XCreateWindow(dpy, root, wx, wy, ++ ww, wh, 0, 32, InputOutput, ++ visual, CWBackPixmap | CWBorderPixel | CWBitGravity ++ | CWEventMask | CWColormap, &attrs); ++ ++ dc.drawable = XCreatePixmap(dpy, win, ww, wh, ++ 32); ++ dc.gc = XCreateGC(dpy, dc.drawable, 0, 0); ++ + XMapRaised(dpy, win); + XSelectInput(dpy, win, SubstructureNotifyMask | FocusChangeMask | + ButtonPressMask | ExposureMask | KeyPressMask | diff --git a/patches/tabbed-autohide-0.6.diff b/patches/tabbed-autohide-0.6.diff new file mode 100644 index 0000000..fd641b4 --- /dev/null +++ b/patches/tabbed-autohide-0.6.diff @@ -0,0 +1,50 @@ +diff --git a/tabbed.c b/tabbed.c +index ff3ada0..c41db0c 100644 +--- a/tabbed.c ++++ b/tabbed.c +@@ -152,7 +152,7 @@ static void (*handler[LASTEvent]) (const XEvent *) = { + [MapRequest] = maprequest, + [PropertyNotify] = propertynotify, + }; +-static int bh, wx, wy, ww, wh; ++static int bh, wx, wy, ww, wh, vbh; + static unsigned int numlockmask = 0; + static Bool running = True, nextfocus, doinitspawn = True, + fillagain = False, closelastclient = False; +@@ -307,6 +307,6 @@ void + drawbar(void) { + unsigned long *col; +- int c, fc, width, n = 0; ++ int c, fc, width, n = 0, nbh, i; + char *name = NULL; + + if (nclients == 0) { +@@ -314,10 +314,19 @@ drawbar(void) + dc.w = ww; + XFetchName(dpy, win, &name); + drawtext(name ? name : "", dc.norm); +- XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, bh, 0, 0); ++ XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, vbh, 0, 0); + XSync(dpy, False); + + return; + } + ++ nbh = nclients > 1 ? vbh : 0; ++ if (bh != nbh) { ++ bh = nbh; ++ for (i = 0; i < nclients; i++) ++ XMoveResizeWindow(dpy, clients[i]->win, 0, bh, ww, wh - bh); ++ } ++ if (bh == 0) ++ return; ++ + width = ww; +@@ -920,6 +929,6 @@ setup(void) + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + initfont(font); +- bh = dc.h = dc.font.height + 2; ++ vbh = dc.h = dc.font.height + 2; + + /* init atoms */ diff --git a/patches/tabbed-bar-height-0.6.diff b/patches/tabbed-bar-height-0.6.diff new file mode 100644 index 0000000..fddcb28 --- /dev/null +++ b/patches/tabbed-bar-height-0.6.diff @@ -0,0 +1,24 @@ +diff --color -up tabbed-0.6-clean/config.def.h tabbed-0.6-modified/config.def.h +--- tabbed-0.6-clean/config.def.h 2014-01-21 10:22:03.000000000 -0800 ++++ tabbed-0.6-modified/config.def.h 2021-03-30 20:23:45.752478278 -0700 +@@ -10,7 +10,7 @@ static const char before[] = "<"; + static const char after[] = ">"; + static const int tabwidth = 200; + static const Bool foreground = True; +- ++static const int barHeight = 24; + /* + * Where to place a new tab when it is opened. When npisrelative is True, + * then the current position is changed + newposition. If npisrelative +diff --color -up tabbed-0.6-clean/tabbed.c tabbed-0.6-modified/tabbed.c +--- tabbed-0.6-clean/tabbed.c 2014-01-21 10:22:03.000000000 -0800 ++++ tabbed-0.6-modified/tabbed.c 2021-03-30 20:24:23.712477426 -0700 +@@ -920,7 +920,7 @@ setup(void) { + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + initfont(font); +- bh = dc.h = dc.font.height + 2; ++ bh = dc.h = barHeight; + + /* init atoms */ + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); diff --git a/patches/tabbed-clientnumber-0.6.diff b/patches/tabbed-clientnumber-0.6.diff new file mode 100644 index 0000000..430245c --- /dev/null +++ b/patches/tabbed-clientnumber-0.6.diff @@ -0,0 +1,23 @@ +diff --git a/tabbed.c b/tabbed.c +index d30206b..70642cb 100644 +--- a/tabbed.c ++++ b/tabbed.c +@@ -308,6 +308,7 @@ drawbar(void) { + unsigned long *col; + int c, fc, width, n = 0; + char *name = NULL; ++ char tabtitle[256]; + + if(nclients == 0) { + dc.x = 0; +@@ -353,7 +354,9 @@ drawbar(void) { + } else { + col = dc.norm; + } +- drawtext(clients[c]->name, col); ++ snprintf(tabtitle, sizeof(tabtitle), "%d: %s", ++ c + 1, clients[c]->name); ++ drawtext(tabtitle, col); + dc.x += dc.w; + clients[c]->tabx = dc.x; + } diff --git a/patches/tabbed-xresources-20210317-dabf6a2.diff b/patches/tabbed-xresources-20210317-dabf6a2.diff new file mode 100644 index 0000000..15610a7 --- /dev/null +++ b/patches/tabbed-xresources-20210317-dabf6a2.diff @@ -0,0 +1,178 @@ +From 8c48f1564c555bbd21758a3a70a9984e61c34a35 Mon Sep 17 00:00:00 2001 +From: 6d6f7274686f6e <4648531+6d6f7274686f6e@users.noreply.github.com> +Date: Wed, 17 Mar 2021 10:59:18 +0100 +Subject: [PATCH] xresources support + +--- + config.def.h | 27 ++++++++++++++------ + tabbed.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 89 insertions(+), 7 deletions(-) + +diff --git a/config.def.h b/config.def.h +index defa426..244e288 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -1,13 +1,13 @@ + /* See LICENSE file for copyright and license details. */ + + /* appearance */ +-static const char font[] = "monospace:size=9"; +-static const char* normbgcolor = "#222222"; +-static const char* normfgcolor = "#cccccc"; +-static const char* selbgcolor = "#555555"; +-static const char* selfgcolor = "#ffffff"; +-static const char* urgbgcolor = "#111111"; +-static const char* urgfgcolor = "#cc0000"; ++static char font[] = "monospace:size=9"; ++static char* normbgcolor = "#222222"; ++static char* normfgcolor = "#cccccc"; ++static char* selbgcolor = "#555555"; ++static char* selfgcolor = "#ffffff"; ++static char* urgbgcolor = "#111111"; ++static char* urgfgcolor = "#cc0000"; + static const char before[] = "<"; + static const char after[] = ">"; + static const char titletrim[] = "..."; +@@ -33,6 +33,19 @@ static Bool npisrelative = False; + } \ + } + ++/* ++ * Xresources preferences to load at startup ++ */ ++ResourcePref resources[] = { ++ { "font", STRING, &font }, ++ { "color0", STRING, &normbgcolor }, ++ { "color4", STRING, &normfgcolor }, ++ { "color4", STRING, &selbgcolor }, ++ { "color7", STRING, &selfgcolor }, ++ { "color2", STRING, &urgbgcolor }, ++ { "color3", STRING, &urgfgcolor }, ++}; ++ + #define MODKEY ControlMask + static Key keys[] = { + /* modifier key function argument */ +diff --git a/tabbed.c b/tabbed.c +index eafe28a..c5bffc7 100644 +--- a/tabbed.c ++++ b/tabbed.c +@@ -13,6 +13,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -85,11 +86,26 @@ typedef struct { + Bool urgent; + Bool closed; + } Client; ++ ++/* Xresources preferences */ ++enum resource_type { ++ STRING = 0, ++ INTEGER = 1, ++ FLOAT = 2 ++}; ++ ++typedef struct { ++ char *name; ++ enum resource_type type; ++ void *dst; ++} ResourcePref; ++ + + /* function declarations */ + static void buttonpress(const XEvent *e); + static void cleanup(void); + static void clientmessage(const XEvent *e); ++static void config_init(void); + static void configurenotify(const XEvent *e); + static void configurerequest(const XEvent *e); + static void createnotify(const XEvent *e); +@@ -120,6 +136,7 @@ static void move(const Arg *arg); + static void movetab(const Arg *arg); + static void propertynotify(const XEvent *e); + static void resize(int c, int w, int h); ++static int resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst); + static void rotate(const Arg *arg); + static void run(void); + static void sendxembed(int c, long msg, long detail, long d1, long d2); +@@ -245,6 +262,23 @@ clientmessage(const XEvent *e) + } + } + ++void ++config_init(void) ++{ ++ char *resm; ++ XrmDatabase db; ++ ResourcePref *p; ++ ++ XrmInitialize(); ++ resm = XResourceManagerString(dpy); ++ if (!resm) ++ return; ++ ++ db = XrmGetStringDatabase(resm); ++ for (p = resources; p < resources + LENGTH(resources); p++) ++ resource_load(db, p->name, p->type, p->dst); ++} ++ + void + configurenotify(const XEvent *e) + { +@@ -897,6 +931,40 @@ resize(int c, int w, int h) + (XEvent *)&ce); + } + ++int ++resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst) ++{ ++ char **sdst = dst; ++ int *idst = dst; ++ float *fdst = dst; ++ ++ char fullname[256]; ++ char fullclass[256]; ++ char *type; ++ XrmValue ret; ++ ++ snprintf(fullname, sizeof(fullname), "%s.%s", "tabbed", name); ++ snprintf(fullclass, sizeof(fullclass), "%s.%s", "tabbed", name); ++ fullname[sizeof(fullname) - 1] = fullclass[sizeof(fullclass) - 1] = '\0'; ++ ++ XrmGetResource(db, fullname, fullclass, &type, &ret); ++ if (ret.addr == NULL || strncmp("String", type, 64)) ++ return 1; ++ ++ switch (rtype) { ++ case STRING: ++ *sdst = ret.addr; ++ break; ++ case INTEGER: ++ *idst = strtoul(ret.addr, NULL, 10); ++ break; ++ case FLOAT: ++ *fdst = strtof(ret.addr, NULL); ++ break; ++ } ++ return 0; ++} ++ + void + rotate(const Arg *arg) + { +@@ -1354,6 +1422,7 @@ main(int argc, char *argv[]) + if (!(dpy = XOpenDisplay(NULL))) + die("%s: cannot open display\n", argv0); + ++ config_init(); + setup(); + printf("0x%lx\n", win); + fflush(NULL); +-- +2.30.2 + diff --git a/tabbed.1 b/tabbed.1 new file mode 100644 index 0000000..07bdbd7 --- /dev/null +++ b/tabbed.1 @@ -0,0 +1,171 @@ +.TH TABBED 1 tabbed\-VERSION +.SH NAME +tabbed \- generic tabbed interface +.SH SYNOPSIS +.B tabbed +.RB [ \-c ] +.RB [ \-d ] +.RB [ \-k ] +.RB [ \-s ] +.RB [ \-v ] +.RB [ \-g +.IR geometry ] +.RB [ \-n +.IR name ] +.RB [ \-p +.RB [ s {+/-} ] \fIpos\fR ] +.RB [ \-o +.IR normbgcol ] +.RB [ \-O +.IR normfgcol ] +.RB [ \-t +.IR selbgcol ] +.RB [ \-T +.IR selfgcol ] +.RB [ \-u +.IR urgbgcol ] +.RB [ \-U +.IR urgfgcol ] +.RB [ \-r +.IR narg ] +.RI [ "command ..." ] +.SH DESCRIPTION +.B tabbed +is a simple tabbed container for applications which support XEmbed. Tabbed +will then run the provided command with the xid of tabbed as appended +argument. (See EXAMPLES.) The automatic spawning of the command can be +disabled by providing the -s parameter. If no command is provided +tabbed will just print its xid and run no command. +.SH OPTIONS +.TP +.B \-c +close tabbed when the last tab is closed. Mutually exclusive with -f. +.TP +.B \-d +detaches tabbed from the terminal and prints its XID to stdout. +.TP +.B \-f +fill up tabbed again by spawning the provided command, when the last tab is +closed. Mutually exclusive with -c. +.TP +.BI \-g " geometry" +defines the X11 geometry string, which will fixate the height and width of +tabbed. +The syntax is +.RI [=][ width {xX} height ][{+-} xoffset {+-} yoffset ]. +See +.BR XParseGeometry (3) +for further details. +.TP +.B \-k +close foreground tabbed client (instead of tabbed and all clients) when +WM_DELETE_WINDOW is sent. +.TP +.BI \-n " name" +will set the WM_CLASS attribute to +.I name. +.TP +.BR \-p " [" s {+-}] \fIpos\fR +will set the absolute or relative position of where to start a new tab. When +.I pos +is is given without 's' in front it is an absolute position. Then negative +numbers will be the position from the last tab, where -1 is the last tab. +If 's' is given, then +.I pos +is a relative position to the current selected tab. If this reaches the limits +of the tabs; those limits then apply. +.TP +.BI \-r " narg" +will replace the +.I narg +th argument in +.I command +with the window id, rather than appending it to the end. +.TP +.B \-s +will disable automatic spawning of the command. +.TP +.BI \-o " normbgcol" +defines the normal background color. +.RI # RGB , +.RI # RRGGBB , +and X color names are supported. +.TP +.BI \-O " normfgcol" +defines the normal foreground color. +.TP +.BI \-t " selbgcol" +defines the selected background color. +.TP +.BI \-T " selfgbcol" +defines the selected foreground color. +.TP +.BI \-u " urgbgcol" +defines the urgent background color. +.TP +.BI \-U " urgfgbcol" +defines the urgent foreground color. +.TP +.B \-v +prints version information to stderr, then exits. +.SH USAGE +.TP +.B Ctrl\-Shift\-Return +open new tab +.TP +.B Ctrl\-Shift\-h +previous tab +.TP +.B Ctrl\-Shift\-l +next tab +.TP +.B Ctrl\-Shift\-j +move selected tab one to the left +.TP +.B Ctrl\-Shift\-k +move selected tab one to the right +.TP +.B Ctrl\-Shift\-u +toggle autofocus of urgent tabs +.TP +.B Ctrl\-Tab +toggle between the selected and last selected tab +.TP +.B Ctrl\-` +open dmenu to either create a new tab appending the entered string or select +an already existing tab. +.TP +.B Ctrl\-q +close tab +.TP +.B Ctrl\-u +focus next urgent tab +.TP +.B Ctrl\-[0..9] +jumps to nth tab +.TP +.B F11 +Toggle fullscreen mode. +.SH EXAMPLES +$ tabbed surf -e +.TP +$ tabbed urxvt -embed +.TP +$ tabbed xterm -into +.TP +$ $(tabbed -d >/tmp/tabbed.xid); urxvt -embed $( +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "arg.h" + +/* XEMBED messages */ +#define XEMBED_EMBEDDED_NOTIFY 0 +#define XEMBED_WINDOW_ACTIVATE 1 +#define XEMBED_WINDOW_DEACTIVATE 2 +#define XEMBED_REQUEST_FOCUS 3 +#define XEMBED_FOCUS_IN 4 +#define XEMBED_FOCUS_OUT 5 +#define XEMBED_FOCUS_NEXT 6 +#define XEMBED_FOCUS_PREV 7 +/* 8-9 were used for XEMBED_GRAB_KEY/XEMBED_UNGRAB_KEY */ +#define XEMBED_MODALITY_ON 10 +#define XEMBED_MODALITY_OFF 11 +#define XEMBED_REGISTER_ACCELERATOR 12 +#define XEMBED_UNREGISTER_ACCELERATOR 13 +#define XEMBED_ACTIVATE_ACCELERATOR 14 + +/* Details for XEMBED_FOCUS_IN: */ +#define XEMBED_FOCUS_CURRENT 0 +#define XEMBED_FOCUS_FIRST 1 +#define XEMBED_FOCUS_LAST 2 + +/* Macros */ +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define MIN(a, b) ((a) < (b) ? (a) : (b)) +#define LENGTH(x) (sizeof((x)) / sizeof(*(x))) +#define CLEANMASK(mask) (mask & ~(numlockmask | LockMask)) +#define TEXTW(x) (textnw(x, strlen(x)) + dc.font.height) + +enum { ColFG, ColBG, ColLast }; /* color */ +enum { WMProtocols, WMDelete, WMName, WMState, WMFullscreen, + XEmbed, WMSelectTab, WMLast }; /* default atoms */ + +typedef union { + int i; + const void *v; +} Arg; + +typedef struct { + unsigned int mod; + KeySym keysym; + void (*func)(const Arg *); + const Arg arg; +} Key; + +typedef struct { + int x, y, w, h; + XftColor norm[ColLast]; + XftColor sel[ColLast]; + XftColor urg[ColLast]; + Drawable drawable; + GC gc; + struct { + int ascent; + int descent; + int height; + XftFont *xfont; + } font; +} DC; /* draw context */ + +typedef struct { + char name[256]; + Window win; + int tabx; + Bool urgent; + Bool closed; +} Client; + +/* Xresources preferences */ +enum resource_type { + STRING = 0, + INTEGER = 1, + FLOAT = 2 +}; + +typedef struct { + char *name; + enum resource_type type; + void *dst; +} ResourcePref; + + +/* function declarations */ +static void buttonpress(const XEvent *e); +static void cleanup(void); +static void clientmessage(const XEvent *e); +static void config_init(void); +static void configurenotify(const XEvent *e); +static void configurerequest(const XEvent *e); +static void createnotify(const XEvent *e); +static void destroynotify(const XEvent *e); +static void die(const char *errstr, ...); +static void drawbar(void); +static void drawtext(const char *text, XftColor col[ColLast]); +static void *ecalloc(size_t n, size_t size); +static void *erealloc(void *o, size_t size); +static void expose(const XEvent *e); +static void focus(int c); +static void focusin(const XEvent *e); +static void focusonce(const Arg *arg); +static void focusurgent(const Arg *arg); +static void fullscreen(const Arg *arg); +static char *getatom(int a); +static int getclient(Window w); +static XftColor getcolor(const char *colstr); +static int getfirsttab(void); +static Bool gettextprop(Window w, Atom atom, char *text, unsigned int size); +static void initfont(const char *fontstr); +static Bool isprotodel(int c); +static void keypress(const XEvent *e); +static void killclient(const Arg *arg); +static void manage(Window win); +static void maprequest(const XEvent *e); +static void move(const Arg *arg); +static void movetab(const Arg *arg); +static void propertynotify(const XEvent *e); +static void resize(int c, int w, int h); +static int resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst); +static void rotate(const Arg *arg); +static void run(void); +static void sendxembed(int c, long msg, long detail, long d1, long d2); +static void setcmd(int argc, char *argv[], int); +static void setup(void); +static void sigchld(int unused); +static void spawn(const Arg *arg); +static int textnw(const char *text, unsigned int len); +static void toggle(const Arg *arg); +static void unmanage(int c); +static void unmapnotify(const XEvent *e); +static void updatenumlockmask(void); +static void updatetitle(int c); +static int xerror(Display *dpy, XErrorEvent *ee); +static void xsettitle(Window w, const char *str); + +/* variables */ +static int screen; +static void (*handler[LASTEvent]) (const XEvent *) = { + [ButtonPress] = buttonpress, + [ClientMessage] = clientmessage, + [ConfigureNotify] = configurenotify, + [ConfigureRequest] = configurerequest, + [CreateNotify] = createnotify, + [UnmapNotify] = unmapnotify, + [DestroyNotify] = destroynotify, + [Expose] = expose, + [FocusIn] = focusin, + [KeyPress] = keypress, + [MapRequest] = maprequest, + [PropertyNotify] = propertynotify, +}; +static int bh, wx, wy, ww, wh, vbh; +static unsigned int numlockmask; +static Bool running = True, nextfocus, doinitspawn = True, + fillagain = False, closelastclient = False, + killclientsfirst = False; +static Display *dpy; +static DC dc; +static Atom wmatom[WMLast]; +static Window root, win; +static Client **clients; +static int nclients, sel = -1, lastsel = -1; +static int (*xerrorxlib)(Display *, XErrorEvent *); +static int cmd_append_pos; +static char winid[64]; +static char **cmd; +static char *wmname = "tabbed"; +static const char *geometry; + +static Colormap cmap; +static Visual *visual = NULL; + +char *argv0; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +void +buttonpress(const XEvent *e) +{ + const XButtonPressedEvent *ev = &e->xbutton; + int i, fc; + Arg arg; + + if (ev->y < 0 || ev->y > bh) + return; + + if (((fc = getfirsttab()) > 0 && ev->x < TEXTW(before)) || ev->x < 0) + return; + + for (i = fc; i < nclients; i++) { + if (clients[i]->tabx > ev->x) { + switch (ev->button) { + case Button1: + focus(i); + break; + case Button2: + focus(i); + killclient(NULL); + break; + case Button4: /* FALLTHROUGH */ + case Button5: + arg.i = ev->button == Button4 ? -1 : 1; + rotate(&arg); + break; + } + break; + } + } +} + +void +cleanup(void) +{ + int i; + + for (i = 0; i < nclients; i++) { + focus(i); + killclient(NULL); + XReparentWindow(dpy, clients[i]->win, root, 0, 0); + unmanage(i); + } + free(clients); + clients = NULL; + + XFreePixmap(dpy, dc.drawable); + XFreeGC(dpy, dc.gc); + XDestroyWindow(dpy, win); + XSync(dpy, False); + free(cmd); +} + +void +clientmessage(const XEvent *e) +{ + const XClientMessageEvent *ev = &e->xclient; + + if (ev->message_type == wmatom[WMProtocols] && + ev->data.l[0] == wmatom[WMDelete]) { + if (nclients > 1 && killclientsfirst) { + killclient(0); + return; + } + running = False; + } +} + +void +config_init(void) +{ + char *resm; + XrmDatabase db; + ResourcePref *p; + + XrmInitialize(); + resm = XResourceManagerString(dpy); + if (!resm) + return; + + db = XrmGetStringDatabase(resm); + for (p = resources; p < resources + LENGTH(resources); p++) + resource_load(db, p->name, p->type, p->dst); +} + +void +configurenotify(const XEvent *e) +{ + int obh; + const XConfigureEvent *ev = &e->xconfigure; + + if (ev->window == win && (ev->width != ww || ev->height != wh)) { + ww = ev->width; + wh = ev->height; + XFreePixmap(dpy, dc.drawable); + dc.drawable = XCreatePixmap(dpy, win, ww, wh, + 32); + + if (!obh && (wh <= bh)) { + obh = bh; + bh = 0; + } else if (!bh && (wh > obh)) { + bh = obh; + obh = 0; + } + + if (sel > -1) + resize(sel, ww, wh - bh); + XSync(dpy, False); + } +} + +void +configurerequest(const XEvent *e) +{ + const XConfigureRequestEvent *ev = &e->xconfigurerequest; + XWindowChanges wc; + int c; + + if ((c = getclient(ev->window)) > -1) { + wc.x = 0; + wc.y = bh; + wc.width = ww; + wc.height = wh - bh; + wc.border_width = 0; + wc.sibling = ev->above; + wc.stack_mode = ev->detail; + XConfigureWindow(dpy, clients[c]->win, ev->value_mask, &wc); + } +} + +void +createnotify(const XEvent *e) +{ + const XCreateWindowEvent *ev = &e->xcreatewindow; + + if (ev->window != win && getclient(ev->window) < 0) + manage(ev->window); +} + +void +destroynotify(const XEvent *e) +{ + const XDestroyWindowEvent *ev = &e->xdestroywindow; + int c; + + if ((c = getclient(ev->window)) > -1) + unmanage(c); +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +void +drawbar(void) +{ + XftColor *col; + int c, cc, fc, width, n = 0, nbh, i; + char *name = NULL; + char tabtitle[256]; + + if (nclients == 0) { + dc.x = 0; + dc.w = ww; + XFetchName(dpy, win, &name); + drawtext(name ? name : "", dc.norm); + XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, vbh, 0, 0); + XSync(dpy, False); + + return; + } + + nbh = nclients > 1 ? vbh : 0; + if (bh != nbh) { + bh = nbh; + for (i = 0; i < nclients; i++) + XMoveResizeWindow(dpy, clients[i]->win, 0, bh, ww, wh - bh); + } + if (bh == 0) + return; + + width = ww; + cc = ww / tabwidth; + if (nclients > cc) + cc = (ww - TEXTW(before) - TEXTW(after)) / tabwidth; + + if ((fc = getfirsttab()) + cc < nclients) { + dc.w = TEXTW(after); + dc.x = width - dc.w; + drawtext(after, dc.sel); + width -= dc.w; + } + dc.x = 0; + + if (fc > 0) { + dc.w = TEXTW(before); + drawtext(before, dc.sel); + dc.x += dc.w; + width -= dc.w; + } + + cc = MIN(cc, nclients); + for (c = fc; c < fc + cc; c++) { + dc.w = width / cc; + if (c == sel) { + col = dc.sel; + dc.w += width % cc; + } else { + col = clients[c]->urgent ? dc.urg : dc.norm; + } + snprintf(tabtitle, sizeof(tabtitle), "%d: %s", + c + 1, clients[c]->name); + drawtext(tabtitle, col); + dc.x += dc.w; + clients[c]->tabx = dc.x; + } + XCopyArea(dpy, dc.drawable, win, dc.gc, 0, 0, ww, bh, 0, 0); + XSync(dpy, False); +} + +void +drawtext(const char *text, XftColor col[ColLast]) +{ + int i, j, x, y, h, len, olen; + char buf[256]; + XftDraw *d; + XRectangle r = { dc.x, dc.y, dc.w, dc.h }; + + XSetForeground(dpy, dc.gc, col[ColBG].pixel); + XFillRectangles(dpy, dc.drawable, dc.gc, &r, 1); + if (!text) + return; + + olen = strlen(text); + h = dc.font.ascent + dc.font.descent; + y = dc.y + (dc.h / 2) - (h / 2) + dc.font.ascent; + x = dc.x + (h / 2); + + /* shorten text if necessary */ + for (len = MIN(olen, sizeof(buf)); + len && textnw(text, len) > dc.w - h; len--); + + if (!len) + return; + + memcpy(buf, text, len); + if (len < olen) { + for (i = len, j = strlen(titletrim); j && i; + buf[--i] = titletrim[--j]) + ; + } + + d = XftDrawCreate(dpy, dc.drawable, visual, cmap); + XftDrawStringUtf8(d, &col[ColFG], dc.font.xfont, x, y, (XftChar8 *) buf, len); + XftDrawDestroy(d); +} + +void * +ecalloc(size_t n, size_t size) +{ + void *p; + + if (!(p = calloc(n, size))) + die("%s: cannot calloc\n", argv0); + return p; +} + +void * +erealloc(void *o, size_t size) +{ + void *p; + + if (!(p = realloc(o, size))) + die("%s: cannot realloc\n", argv0); + return p; +} + +void +expose(const XEvent *e) +{ + const XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && win == ev->window) + drawbar(); +} + +void +focus(int c) +{ + char buf[BUFSIZ] = "tabbed-"VERSION" ::"; + size_t i, n; + XWMHints* wmh; + + /* If c, sel and clients are -1, raise tabbed-win itself */ + if (nclients == 0) { + cmd[cmd_append_pos] = NULL; + for(i = 0, n = strlen(buf); cmd[i] && n < sizeof(buf); i++) + n += snprintf(&buf[n], sizeof(buf) - n, " %s", cmd[i]); + + xsettitle(win, buf); + XRaiseWindow(dpy, win); + + return; + } + + if (c < 0 || c >= nclients) + return; + + resize(c, ww, wh - bh); + XRaiseWindow(dpy, clients[c]->win); + XSetInputFocus(dpy, clients[c]->win, RevertToParent, CurrentTime); + sendxembed(c, XEMBED_FOCUS_IN, XEMBED_FOCUS_CURRENT, 0, 0); + sendxembed(c, XEMBED_WINDOW_ACTIVATE, 0, 0, 0); + xsettitle(win, clients[c]->name); + + if (sel != c) { + lastsel = sel; + sel = c; + } + + if (clients[c]->urgent && (wmh = XGetWMHints(dpy, clients[c]->win))) { + wmh->flags &= ~XUrgencyHint; + XSetWMHints(dpy, clients[c]->win, wmh); + clients[c]->urgent = False; + XFree(wmh); + } + + drawbar(); + XSync(dpy, False); +} + +void +focusin(const XEvent *e) +{ + const XFocusChangeEvent *ev = &e->xfocus; + int dummy; + Window focused; + + if (ev->mode != NotifyUngrab) { + XGetInputFocus(dpy, &focused, &dummy); + if (focused == win) + focus(sel); + } +} + +void +focusonce(const Arg *arg) +{ + nextfocus = True; +} + +void +focusurgent(const Arg *arg) +{ + int c; + + if (sel < 0) + return; + + for (c = (sel + 1) % nclients; c != sel; c = (c + 1) % nclients) { + if (clients[c]->urgent) { + focus(c); + return; + } + } +} + +void +fullscreen(const Arg *arg) +{ + XEvent e; + + e.type = ClientMessage; + e.xclient.window = win; + e.xclient.message_type = wmatom[WMState]; + e.xclient.format = 32; + e.xclient.data.l[0] = 2; + e.xclient.data.l[1] = wmatom[WMFullscreen]; + e.xclient.data.l[2] = 0; + XSendEvent(dpy, root, False, SubstructureNotifyMask, &e); +} + +char * +getatom(int a) +{ + static char buf[BUFSIZ]; + Atom adummy; + int idummy; + unsigned long ldummy; + unsigned char *p = NULL; + + XGetWindowProperty(dpy, win, wmatom[a], 0L, BUFSIZ, False, XA_STRING, + &adummy, &idummy, &ldummy, &ldummy, &p); + if (p) + strncpy(buf, (char *)p, LENGTH(buf)-1); + else + buf[0] = '\0'; + XFree(p); + + return buf; +} + +int +getclient(Window w) +{ + int i; + + for (i = 0; i < nclients; i++) { + if (clients[i]->win == w) + return i; + } + + return -1; +} + +XftColor +getcolor(const char *colstr) +{ + XftColor color; + + if (!XftColorAllocName(dpy, visual, cmap, colstr, &color)) + die("%s: cannot allocate color '%s'\n", argv0, colstr); + + return color; +} + +int +getfirsttab(void) +{ + int cc, ret; + + if (sel < 0) + return 0; + + cc = ww / tabwidth; + if (nclients > cc) + cc = (ww - TEXTW(before) - TEXTW(after)) / tabwidth; + + ret = sel - cc / 2 + (cc + 1) % 2; + return ret < 0 ? 0 : + ret + cc > nclients ? MAX(0, nclients - cc) : + ret; +} + +Bool +gettextprop(Window w, Atom atom, char *text, unsigned int size) +{ + char **list = NULL; + int n; + XTextProperty name; + + if (!text || size == 0) + return False; + + text[0] = '\0'; + XGetTextProperty(dpy, w, &name, atom); + if (!name.nitems) + return False; + + if (name.encoding == XA_STRING) { + strncpy(text, (char *)name.value, size - 1); + } else if (XmbTextPropertyToTextList(dpy, &name, &list, &n) >= Success + && n > 0 && *list) { + strncpy(text, *list, size - 1); + XFreeStringList(list); + } + text[size - 1] = '\0'; + XFree(name.value); + + return True; +} + +void +initfont(const char *fontstr) +{ + if (!(dc.font.xfont = XftFontOpenName(dpy, screen, fontstr)) + && !(dc.font.xfont = XftFontOpenName(dpy, screen, "fixed"))) + die("error, cannot load font: '%s'\n", fontstr); + + dc.font.ascent = dc.font.xfont->ascent; + dc.font.descent = dc.font.xfont->descent; + dc.font.height = dc.font.ascent + dc.font.descent; +} + +Bool +isprotodel(int c) +{ + int i, n; + Atom *protocols; + Bool ret = False; + + if (XGetWMProtocols(dpy, clients[c]->win, &protocols, &n)) { + for (i = 0; !ret && i < n; i++) { + if (protocols[i] == wmatom[WMDelete]) + ret = True; + } + XFree(protocols); + } + + return ret; +} + +void +keypress(const XEvent *e) +{ + const XKeyEvent *ev = &e->xkey; + unsigned int i; + KeySym keysym; + + keysym = XkbKeycodeToKeysym(dpy, (KeyCode)ev->keycode, 0, 0); + for (i = 0; i < LENGTH(keys); i++) { + if (keysym == keys[i].keysym && + CLEANMASK(keys[i].mod) == CLEANMASK(ev->state) && + keys[i].func) + keys[i].func(&(keys[i].arg)); + } +} + +void +killclient(const Arg *arg) +{ + XEvent ev; + + if (sel < 0) + return; + + if (isprotodel(sel) && !clients[sel]->closed) { + ev.type = ClientMessage; + ev.xclient.window = clients[sel]->win; + ev.xclient.message_type = wmatom[WMProtocols]; + ev.xclient.format = 32; + ev.xclient.data.l[0] = wmatom[WMDelete]; + ev.xclient.data.l[1] = CurrentTime; + XSendEvent(dpy, clients[sel]->win, False, NoEventMask, &ev); + clients[sel]->closed = True; + } else { + XKillClient(dpy, clients[sel]->win); + } +} + +void +manage(Window w) +{ + updatenumlockmask(); + { + int i, j, nextpos; + unsigned int modifiers[] = { 0, LockMask, numlockmask, + numlockmask | LockMask }; + KeyCode code; + Client *c; + XEvent e; + + XWithdrawWindow(dpy, w, 0); + XReparentWindow(dpy, w, win, 0, bh); + XSelectInput(dpy, w, PropertyChangeMask | + StructureNotifyMask | EnterWindowMask); + XSync(dpy, False); + + for (i = 0; i < LENGTH(keys); i++) { + if ((code = XKeysymToKeycode(dpy, keys[i].keysym))) { + for (j = 0; j < LENGTH(modifiers); j++) { + XGrabKey(dpy, code, keys[i].mod | + modifiers[j], w, True, + GrabModeAsync, GrabModeAsync); + } + } + } + + c = ecalloc(1, sizeof *c); + c->win = w; + + nclients++; + clients = erealloc(clients, sizeof(Client *) * nclients); + + if(npisrelative) { + nextpos = sel + newposition; + } else { + if (newposition < 0) + nextpos = nclients - newposition; + else + nextpos = newposition; + } + if (nextpos >= nclients) + nextpos = nclients - 1; + if (nextpos < 0) + nextpos = 0; + + if (nclients > 1 && nextpos < nclients - 1) + memmove(&clients[nextpos + 1], &clients[nextpos], + sizeof(Client *) * (nclients - nextpos - 1)); + + clients[nextpos] = c; + updatetitle(nextpos); + + XLowerWindow(dpy, w); + XMapWindow(dpy, w); + + e.xclient.window = w; + e.xclient.type = ClientMessage; + e.xclient.message_type = wmatom[XEmbed]; + e.xclient.format = 32; + e.xclient.data.l[0] = CurrentTime; + e.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY; + e.xclient.data.l[2] = 0; + e.xclient.data.l[3] = win; + e.xclient.data.l[4] = 0; + XSendEvent(dpy, root, False, NoEventMask, &e); + + XSync(dpy, False); + + /* Adjust sel before focus does set it to lastsel. */ + if (sel >= nextpos) + sel++; + focus(nextfocus ? nextpos : + sel < 0 ? 0 : + sel); + nextfocus = foreground; + } +} + +void +maprequest(const XEvent *e) +{ + const XMapRequestEvent *ev = &e->xmaprequest; + + if (getclient(ev->window) < 0) + manage(ev->window); +} + +void +move(const Arg *arg) +{ + if (arg->i >= 0 && arg->i < nclients) + focus(arg->i); +} + +void +movetab(const Arg *arg) +{ + int c; + Client *new; + + if (sel < 0) + return; + + c = (sel + arg->i) % nclients; + if (c < 0) + c += nclients; + + if (c == sel) + return; + + new = clients[sel]; + if (sel < c) + memmove(&clients[sel], &clients[sel+1], + sizeof(Client *) * (c - sel)); + else + memmove(&clients[c+1], &clients[c], + sizeof(Client *) * (sel - c)); + clients[c] = new; + sel = c; + + drawbar(); +} + +void +propertynotify(const XEvent *e) +{ + const XPropertyEvent *ev = &e->xproperty; + XWMHints *wmh; + int c; + char* selection = NULL; + Arg arg; + + if (ev->state == PropertyNewValue && ev->atom == wmatom[WMSelectTab]) { + selection = getatom(WMSelectTab); + if (!strncmp(selection, "0x", 2)) { + arg.i = getclient(strtoul(selection, NULL, 0)); + move(&arg); + } else { + cmd[cmd_append_pos] = selection; + arg.v = cmd; + spawn(&arg); + } + } else if (ev->state == PropertyNewValue && ev->atom == XA_WM_HINTS && + (c = getclient(ev->window)) > -1 && + (wmh = XGetWMHints(dpy, clients[c]->win))) { + if (wmh->flags & XUrgencyHint) { + XFree(wmh); + wmh = XGetWMHints(dpy, win); + if (c != sel) { + if (urgentswitch && wmh && + !(wmh->flags & XUrgencyHint)) { + /* only switch, if tabbed was focused + * since last urgency hint if WMHints + * could not be received, + * default to no switch */ + focus(c); + } else { + /* if no switch should be performed, + * mark tab as urgent */ + clients[c]->urgent = True; + drawbar(); + } + } + if (wmh && !(wmh->flags & XUrgencyHint)) { + /* update tabbed urgency hint + * if not set already */ + wmh->flags |= XUrgencyHint; + XSetWMHints(dpy, win, wmh); + } + } + XFree(wmh); + } else if (ev->state != PropertyDelete && ev->atom == XA_WM_NAME && + (c = getclient(ev->window)) > -1) { + updatetitle(c); + } +} + +void +resize(int c, int w, int h) +{ + XConfigureEvent ce; + XWindowChanges wc; + + ce.x = 0; + ce.y = wc.y = bh; + ce.width = wc.width = w; + ce.height = wc.height = h; + ce.type = ConfigureNotify; + ce.display = dpy; + ce.event = clients[c]->win; + ce.window = clients[c]->win; + ce.above = None; + ce.override_redirect = False; + ce.border_width = 0; + + XConfigureWindow(dpy, clients[c]->win, CWY | CWWidth | CWHeight, &wc); + XSendEvent(dpy, clients[c]->win, False, StructureNotifyMask, + (XEvent *)&ce); +} + +int +resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst) +{ + char **sdst = dst; + int *idst = dst; + float *fdst = dst; + + char fullname[256]; + char fullclass[256]; + char *type; + XrmValue ret; + + snprintf(fullname, sizeof(fullname), "%s.%s", "tabbed", name); + snprintf(fullclass, sizeof(fullclass), "%s.%s", "tabbed", name); + fullname[sizeof(fullname) - 1] = fullclass[sizeof(fullclass) - 1] = '\0'; + + XrmGetResource(db, fullname, fullclass, &type, &ret); + if (ret.addr == NULL || strncmp("String", type, 64)) + return 1; + + switch (rtype) { + case STRING: + *sdst = ret.addr; + break; + case INTEGER: + *idst = strtoul(ret.addr, NULL, 10); + break; + case FLOAT: + *fdst = strtof(ret.addr, NULL); + break; + } + return 0; +} + +void +rotate(const Arg *arg) +{ + int nsel = -1; + + if (sel < 0) + return; + + if (arg->i == 0) { + if (lastsel > -1) + focus(lastsel); + } else if (sel > -1) { + /* Rotating in an arg->i step around the clients. */ + nsel = sel + arg->i; + while (nsel >= nclients) + nsel -= nclients; + while (nsel < 0) + nsel += nclients; + focus(nsel); + } +} + +void +run(void) +{ + XEvent ev; + + /* main event loop */ + XSync(dpy, False); + drawbar(); + if (doinitspawn == True) + spawn(NULL); + + while (running) { + XNextEvent(dpy, &ev); + if (handler[ev.type]) + (handler[ev.type])(&ev); /* call handler */ + } +} + +void +sendxembed(int c, long msg, long detail, long d1, long d2) +{ + XEvent e = { 0 }; + + e.xclient.window = clients[c]->win; + e.xclient.type = ClientMessage; + e.xclient.message_type = wmatom[XEmbed]; + e.xclient.format = 32; + e.xclient.data.l[0] = CurrentTime; + e.xclient.data.l[1] = msg; + e.xclient.data.l[2] = detail; + e.xclient.data.l[3] = d1; + e.xclient.data.l[4] = d2; + XSendEvent(dpy, clients[c]->win, False, NoEventMask, &e); +} + +void +setcmd(int argc, char *argv[], int replace) +{ + int i; + + cmd = ecalloc(argc + 3, sizeof(*cmd)); + if (argc == 0) + return; + for (i = 0; i < argc; i++) + cmd[i] = argv[i]; + cmd[replace > 0 ? replace : argc] = winid; + cmd_append_pos = argc + !replace; + cmd[cmd_append_pos] = cmd[cmd_append_pos + 1] = NULL; +} + +void +setup(void) +{ + int bitm, tx, ty, tw, th, dh, dw, isfixed; + XWMHints *wmh; + XClassHint class_hint; + XSizeHints *size_hint; + + /* clean up any zombies immediately */ + sigchld(0); + + /* init screen */ + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + initfont(font); + vbh = dc.h = dc.font.height + 2; + + /* init atoms */ + wmatom[WMDelete] = XInternAtom(dpy, "WM_DELETE_WINDOW", False); + wmatom[WMFullscreen] = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", + False); + wmatom[WMName] = XInternAtom(dpy, "_NET_WM_NAME", False); + wmatom[WMProtocols] = XInternAtom(dpy, "WM_PROTOCOLS", False); + wmatom[WMSelectTab] = XInternAtom(dpy, "_TABBED_SELECT_TAB", False); + wmatom[WMState] = XInternAtom(dpy, "_NET_WM_STATE", False); + wmatom[XEmbed] = XInternAtom(dpy, "_XEMBED", False); + + /* init appearance */ + wx = 0; + wy = 0; + ww = 800; + wh = 600; + isfixed = 0; + + if (geometry) { + tx = ty = tw = th = 0; + bitm = XParseGeometry(geometry, &tx, &ty, (unsigned *)&tw, + (unsigned *)&th); + if (bitm & XValue) + wx = tx; + if (bitm & YValue) + wy = ty; + if (bitm & WidthValue) + ww = tw; + if (bitm & HeightValue) + wh = th; + if (bitm & XNegative && wx == 0) + wx = -1; + if (bitm & YNegative && wy == 0) + wy = -1; + if (bitm & (HeightValue | WidthValue)) + isfixed = 1; + + dw = DisplayWidth(dpy, screen); + dh = DisplayHeight(dpy, screen); + if (wx < 0) + wx = dw + wx - ww - 1; + if (wy < 0) + wy = dh + wy - wh - 1; + } + + XVisualInfo *vis; + XRenderPictFormat *fmt; + int nvi; + int i; + + XVisualInfo tpl = { + .screen = screen, + .depth = 32, + .class = TrueColor + }; + + vis = XGetVisualInfo(dpy, VisualScreenMask | VisualDepthMask | VisualClassMask, &tpl, &nvi); + for(i = 0; i < nvi; i ++) { + fmt = XRenderFindVisualFormat(dpy, vis[i].visual); + if (fmt->type == PictTypeDirect && fmt->direct.alphaMask) { + visual = vis[i].visual; + break; + } + } + + XFree(vis); + + if (! visual) { + fprintf(stderr, "Couldn't find ARGB visual.\n"); + exit(1); + } + + cmap = XCreateColormap( dpy, root, visual, None); + dc.norm[ColBG] = getcolor(normbgcolor); + dc.norm[ColFG] = getcolor(normfgcolor); + dc.sel[ColBG] = getcolor(selbgcolor); + dc.sel[ColFG] = getcolor(selfgcolor); + dc.urg[ColBG] = getcolor(urgbgcolor); + dc.urg[ColFG] = getcolor(urgfgcolor); + + XSetWindowAttributes attrs; + attrs.background_pixel = dc.norm[ColBG].pixel; + attrs.border_pixel = dc.norm[ColFG].pixel; + attrs.bit_gravity = NorthWestGravity; + attrs.event_mask = FocusChangeMask | KeyPressMask + | ExposureMask | VisibilityChangeMask | StructureNotifyMask + | ButtonMotionMask | ButtonPressMask | ButtonReleaseMask; + attrs.background_pixmap = None ; + attrs.colormap = cmap; + + win = XCreateWindow(dpy, root, wx, wy, + ww, wh, 0, 32, InputOutput, + visual, CWBackPixmap | CWBorderPixel | CWBitGravity + | CWEventMask | CWColormap, &attrs); + + dc.drawable = XCreatePixmap(dpy, win, ww, wh, + 32); + dc.gc = XCreateGC(dpy, dc.drawable, 0, 0); + + XMapRaised(dpy, win); + XSelectInput(dpy, win, SubstructureNotifyMask | FocusChangeMask | + ButtonPressMask | ExposureMask | KeyPressMask | + PropertyChangeMask | StructureNotifyMask | + SubstructureRedirectMask); + xerrorxlib = XSetErrorHandler(xerror); + + class_hint.res_name = wmname; + class_hint.res_class = "tabbed"; + XSetClassHint(dpy, win, &class_hint); + + size_hint = XAllocSizeHints(); + if (!isfixed) { + size_hint->flags = PSize | PMinSize; + size_hint->height = wh; + size_hint->width = ww; + size_hint->min_height = bh + 1; + } else { + size_hint->flags = PMaxSize | PMinSize; + size_hint->min_width = size_hint->max_width = ww; + size_hint->min_height = size_hint->max_height = wh; + } + wmh = XAllocWMHints(); + XSetWMProperties(dpy, win, NULL, NULL, NULL, 0, size_hint, wmh, NULL); + XFree(size_hint); + XFree(wmh); + + XSetWMProtocols(dpy, win, &wmatom[WMDelete], 1); + + snprintf(winid, sizeof(winid), "%lu", win); + setenv("XEMBED", winid, 1); + + nextfocus = foreground; + focus(-1); +} + +void +sigchld(int unused) +{ + if (signal(SIGCHLD, sigchld) == SIG_ERR) + die("%s: cannot install SIGCHLD handler", argv0); + + while (0 < waitpid(-1, NULL, WNOHANG)); +} + +void +spawn(const Arg *arg) +{ + if (fork() == 0) { + if(dpy) + close(ConnectionNumber(dpy)); + + setsid(); + if (arg && arg->v) { + execvp(((char **)arg->v)[0], (char **)arg->v); + fprintf(stderr, "%s: execvp %s", argv0, + ((char **)arg->v)[0]); + } else { + cmd[cmd_append_pos] = NULL; + execvp(cmd[0], cmd); + fprintf(stderr, "%s: execvp %s", argv0, cmd[0]); + } + perror(" failed"); + exit(0); + } +} + +int +textnw(const char *text, unsigned int len) +{ + XGlyphInfo ext; + XftTextExtentsUtf8(dpy, dc.font.xfont, (XftChar8 *) text, len, &ext); + return ext.xOff; +} + +void +toggle(const Arg *arg) +{ + *(Bool*) arg->v = !*(Bool*) arg->v; +} + +void +unmanage(int c) +{ + if (c < 0 || c >= nclients) { + drawbar(); + XSync(dpy, False); + return; + } + + if (!nclients) + return; + + if (c == 0) { + /* First client. */ + nclients--; + free(clients[0]); + memmove(&clients[0], &clients[1], sizeof(Client *) * nclients); + } else if (c == nclients - 1) { + /* Last client. */ + nclients--; + free(clients[c]); + clients = erealloc(clients, sizeof(Client *) * nclients); + } else { + /* Somewhere inbetween. */ + free(clients[c]); + memmove(&clients[c], &clients[c+1], + sizeof(Client *) * (nclients - (c + 1))); + nclients--; + } + + if (nclients <= 0) { + lastsel = sel = -1; + + if (closelastclient) + running = False; + else if (fillagain && running) + spawn(NULL); + } else { + if (lastsel >= nclients) + lastsel = nclients - 1; + else if (lastsel > c) + lastsel--; + + if (c == sel && lastsel >= 0) { + focus(lastsel); + } else { + if (sel > c) + sel--; + if (sel >= nclients) + sel = nclients - 1; + + focus(sel); + } + } + + drawbar(); + XSync(dpy, False); +} + +void +unmapnotify(const XEvent *e) +{ + const XUnmapEvent *ev = &e->xunmap; + int c; + + if ((c = getclient(ev->window)) > -1) + unmanage(c); +} + +void +updatenumlockmask(void) +{ + unsigned int i, j; + XModifierKeymap *modmap; + + numlockmask = 0; + modmap = XGetModifierMapping(dpy); + for (i = 0; i < 8; i++) { + for (j = 0; j < modmap->max_keypermod; j++) { + if (modmap->modifiermap[i * modmap->max_keypermod + j] + == XKeysymToKeycode(dpy, XK_Num_Lock)) + numlockmask = (1 << i); + } + } + XFreeModifiermap(modmap); +} + +void +updatetitle(int c) +{ + if (!gettextprop(clients[c]->win, wmatom[WMName], clients[c]->name, + sizeof(clients[c]->name))) + gettextprop(clients[c]->win, XA_WM_NAME, clients[c]->name, + sizeof(clients[c]->name)); + if (sel == c) + xsettitle(win, clients[c]->name); + drawbar(); +} + +/* There's no way to check accesses to destroyed windows, thus those cases are + * ignored (especially on UnmapNotify's). Other types of errors call Xlibs + * default error handler, which may call exit. */ +int +xerror(Display *dpy, XErrorEvent *ee) +{ + if (ee->error_code == BadWindow + || (ee->request_code == X_SetInputFocus && + ee->error_code == BadMatch) + || (ee->request_code == X_PolyText8 && + ee->error_code == BadDrawable) + || (ee->request_code == X_PolyFillRectangle && + ee->error_code == BadDrawable) + || (ee->request_code == X_PolySegment && + ee->error_code == BadDrawable) + || (ee->request_code == X_ConfigureWindow && + ee->error_code == BadMatch) + || (ee->request_code == X_GrabButton && + ee->error_code == BadAccess) + || (ee->request_code == X_GrabKey && + ee->error_code == BadAccess) + || (ee->request_code == X_CopyArea && + ee->error_code == BadDrawable)) + return 0; + + fprintf(stderr, "%s: fatal error: request code=%d, error code=%d\n", + argv0, ee->request_code, ee->error_code); + return xerrorxlib(dpy, ee); /* may call exit */ +} + +void +xsettitle(Window w, const char *str) +{ + XTextProperty xtp; + + if (XmbTextListToTextProperty(dpy, (char **)&str, 1, + XCompoundTextStyle, &xtp) == Success) { + XSetTextProperty(dpy, w, &xtp, wmatom[WMName]); + XSetTextProperty(dpy, w, &xtp, XA_WM_NAME); + XFree(xtp.value); + } +} + +void +usage(void) +{ + die("usage: %s [-dfksv] [-g geometry] [-n name] [-p [s+/-]pos]\n" + " [-r narg] [-o color] [-O color] [-t color] [-T color]\n" + " [-u color] [-U color] command...\n", argv0); +} + +int +main(int argc, char *argv[]) +{ + Bool detach = False; + int replace = 0; + char *pstr; + + ARGBEGIN { + case 'c': + closelastclient = True; + fillagain = False; + break; + case 'd': + detach = True; + break; + case 'f': + fillagain = True; + break; + case 'g': + geometry = EARGF(usage()); + break; + case 'k': + killclientsfirst = True; + break; + case 'n': + wmname = EARGF(usage()); + break; + case 'O': + normfgcolor = EARGF(usage()); + break; + case 'o': + normbgcolor = EARGF(usage()); + break; + case 'p': + pstr = EARGF(usage()); + if (pstr[0] == 's') { + npisrelative = True; + newposition = atoi(&pstr[1]); + } else { + newposition = atoi(pstr); + } + break; + case 'r': + replace = atoi(EARGF(usage())); + break; + case 's': + doinitspawn = False; + break; + case 'T': + selfgcolor = EARGF(usage()); + break; + case 't': + selbgcolor = EARGF(usage()); + break; + case 'U': + urgfgcolor = EARGF(usage()); + break; + case 'u': + urgbgcolor = EARGF(usage()); + break; + case 'v': + die("tabbed-"VERSION", © 2009-2016 tabbed engineers, " + "see LICENSE for details.\n"); + break; + default: + usage(); + break; + } ARGEND; + + if (argc < 1) { + doinitspawn = False; + fillagain = False; + } + + setcmd(argc, argv, replace); + + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fprintf(stderr, "%s: no locale support\n", argv0); + if (!(dpy = XOpenDisplay(NULL))) + die("%s: cannot open display\n", argv0); + + config_init(); + setup(); + printf("0x%lx\n", win); + fflush(NULL); + + if (detach) { + if (fork() == 0) { + fclose(stdout); + } else { + if (dpy) + close(ConnectionNumber(dpy)); + return EXIT_SUCCESS; + } + } + + run(); + cleanup(); + XCloseDisplay(dpy); + + return EXIT_SUCCESS; +} diff --git a/xembed.1 b/xembed.1 new file mode 100644 index 0000000..5b0c28c --- /dev/null +++ b/xembed.1 @@ -0,0 +1,35 @@ +.TH XEMBED 1 tabbed\-VERSION +.SH NAME +xembed \- XEmbed foreground process +.SH SYNOPSIS +.B xembed +.I flag command +.RI [ "argument ..." ] +.SH DESCRIPTION +If the environment variable XEMBED is set, and +.B xembed +is in the foreground of its controlling tty, it will execute +.IP +command flag $XEMBED [argument ...] +.LP +Otherwise it will execute +.IP +command [argument ...] +.LP +.SH EXAMPLE +In a terminal emulator within a +.B tabbed +session, the shell alias +.IP +$ alias surf='xembed -e surf' +.LP +will cause `surf' to open in a new tab, unless it is run in the background, +i.e. `surf &', in which case it will instead open in a new window. +.SH AUTHORS +See the LICENSE file for the authors. +.SH LICENSE +See the LICENSE file for the terms of redistribution. +.SH SEE ALSO +.BR tabbed (1) +.SH BUGS +Please report them. diff --git a/xembed.c b/xembed.c new file mode 100644 index 0000000..cbb0e97 --- /dev/null +++ b/xembed.c @@ -0,0 +1,45 @@ +/* + * See LICENSE file for copyright and license details. + */ + +#include +#include +#include +#include + +int +main(int argc, char *argv[]) +{ + char *xembed; + int tty; + pid_t pgrp, tcpgrp; + + if (argc < 3) { + fprintf(stderr, "usage: %s flag cmd ...\n", argv[0]); + return 2; + } + + if (!(xembed = getenv("XEMBED"))) + goto noembed; + + if ((tty = open("/dev/tty", O_RDONLY)) < 0) + goto noembed; + + pgrp = getpgrp(); + tcpgrp = tcgetpgrp(tty); + + close(tty); + + if (pgrp == tcpgrp) { /* in foreground of tty */ + argv[0] = argv[2]; + argv[2] = xembed; + } else { +noembed: + argv += 2; + } + + execvp(argv[0], argv); + + perror(argv[0]); /* failed to execute */ + return 1; +} -- cgit v1.2.3