+/*
+ * Copyright (c) 2019-2020 The DragonFly Project. All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * DragonFlyBSD X11 clock
+ *
+ * Synopsis from Matthew Dillon:
+ *
+ * I got tired of X11 clocks that can't be resized, or don't scale,
+ * don't include the day of week, day, and month, use all sorts of
+ * libraries for things that nobody needs, use horrible fonts, look
+ * cute but have no actual functionality, leave artifacts, or flicker
+ * when updating.
+ *
+ * This code is really quite straight-forward. Eat, Sleep, Repeat.
+ * There were two quirks. First, trying to use XftDrawString8()
+ * to erase text leaves an outline, even with alpha = 0xFFFF. That's
+ * a bug in the function in my view. Trying to use outlines to erase
+ * the second, minute, or hour hand 'sorta' worked, but big windows
+ * would leave artifacts. This was attempted to avoid flickering but
+ * wound up being a mess. To deal with flickering the program uses
+ * double-buffering as a solution.
+ *
+ * The program uses a trick for the seconds-hand to avoid having to
+ * double-buffer-copy every second. However, if seconds is displayed
+ * in the text, a double-buffer-copy will occur each second.
+ *
+ * There are quite a few features, run dflyclock -h for a list. The
+ * features are smart. Turning off the text below expands the clock
+ * into the space. True-type fonts are supported, you can control
+ * the strftime string (including blanking it with ""). It centers
+ * the text in a smart way so you don't have to use a monospace font,
+ * the code is very smart about using a minimal amount of CPU and GPU.
+ * And just for yuchs I added shaped window support too. Etc.
+ *
+ * DOUBLE BUFFER OPTIMIZATION - If the text does not contain a seconds
+ * counter and the second-hand is turned on, we use XOR on the second-
+ * hand directly to the window and avoid double-buffering it on a
+ * 1-second basis. The double buffer will be used every 15 seconds in
+ * order to be able to update the minute hand.
+ *
+ * BSD Copyright, replication is good
+ *
+ * Many of the old xclock derivatives use the tried-and-true method of
+ * starting with an older work and hacking it up... usually without
+ * much improvement.
+ *
+ * Please feel free to copy and modify the code. Rename your clock and
+ * add yourself to the contribution list if you like. While not strictly
+ * verboten to GNUize the source, please stick with the BSD copyright as
+ * a nod to my BSD roots.
+ *
+ * cc -I/usr/local/include -I/usr/local/include/freetype2 dflyclock.c -L/usr/local/lib -o ~/bin/dflyclock -lXft -lXext -lX11 -lm
+ */
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/file.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <time.h>
+#include <math.h>
+#include <X11/X.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <X11/Xft/Xft.h>
+#include <X11/extensions/Xrender.h>
+#include <X11/extensions/shape.h>
+
+static void process_events(int *redrawp);
+static int process_time(int *redrawp);
+static void redraw_display(void);
+static void update_display(int draw_me);
+static void update_seconds(int draw_me);
+static void gcsetup(void);
+static int fontcheck(const char *str);
+static void usage(const char *prog, const char *badopt);
+static void drawhand(Drawable d, GC gc, int draw_me,
+ double a, double len, int thick);
+static void drawtick(GC gc, double a, double len, double thick);
+static void shaped_window_setup(void);
+static double angleof(int v1, int lim);
+
+#define DEFAULT_PIX 128
+#define DEFAULT_PIX_WTEXT 140
+
+XFontStruct *Font1;
+XFontStruct *Font2;
+Pixmap AreaPixmap;
+XftFont *XftFont1;
+XftFont *XftFont2;
+Display *Dpy;
+Window Win;
+XftDraw *XftWin;
+double Scale = 0.50;
+GC GCXor;
+GC GCFg;
+GC GCBg;
+GC GCText2;
+XftColor XftGCFg;
+XftColor XftGCBg;
+XftColor XftGCText2;
+int GCAllocated;
+int WindowWidth;
+int WindowHeight;
+int CW; /* clock face drawing area */
+int CH; /* clock face drawing area (before adj) */
+
+int ShapeTextBoxX;
+int ShapeTextBoxY;
+int ShapeTextBoxW;
+int ShapeTextBoxH;
+
+int FW1; /* font width in pixels */
+int FHA1; /* font height ascent in pixels */
+int FHD1; /* font height descent in pixels */
+
+int FW2; /* t2font width in pixels */
+int FHA2; /* t2font height ascent in pixels */
+int FHD2; /* t2font height descent in pixels */
+
+int BH; /* border height */
+int BW; /* border width */
+int CW2; /* 1/2 clock race drawing area */
+int CH2; /* 1/2 clock race drawing area */
+int AddBorder = 2;
+int Military;
+int EnableText = 3;
+int IncludeSeconds = 1;
+int TextHasSeconds;
+int ShapedWindow = 1;
+int ShapeType = 1;
+int WasteCPU = 0;
+const char *Time1NoTime2FmtMil = "%a %d-%b %l:%M";
+const char *Time1NoTime2FmtNrm = "%a %d-%b %l:%M";
+const char *Time1FmtMil = "%b %H:%M";
+const char *Time1FmtNrm = "%b %l:%M";
+const char *Time1Fmt = "%b %l:%M";
+const char *Time2Fmt = "%a %d";
+long Interval = 1000000; /* microseconds */
+struct timeval LastTVSubsecs;
+struct timeval NextTVSubsecs;
+struct tm LastTM;
+struct tm LastTMSecs;
+struct tm NextTM;
+ulong BGColor;
+ulong FGColor;
+ulong T2Color;
+const char *FontName1 = "Droid Sans-%f";
+const char *FontName2 = "Droid Sans-%f";
+const char *BackupFontName1 = "-*-*-bold-r-*-*-%f-*-*-*-*-*-*-*";
+const char *BackupFontName2 = "-*-*-bold-r-*-*-%f-*-*-*-*-*-*-*";
+
+int
+main(int ac, char **av)
+{
+ Window root_win;
+ XSetWindowAttributes wattr;
+ int i;
+ int j;
+ int reverse = 0;
+ int didfont2 = 0;
+ int didtime1fmt = 0;
+ const char *bgcolor = NULL;
+ const char *fgcolor = NULL;
+ const char *t2color = NULL;
+ char *wname = strdup("dflyclock");
+ char *wclass = strdup(" DFlyClock");
+ char *geometry = NULL;
+ XSizeHints hints;
+ XTextProperty name;
+ XClassHint classHint;
+ bzero(&hints, sizeof(hints));
+
+ hints.width = DEFAULT_PIX;
+ hints.height = DEFAULT_PIX_WTEXT;
+
+ for (i = 1; i < ac; ++i) {
+ if (av[i][0] != '-')
+ usage(av[0], av[i]);
+ if (strcmp(av[i], "-geometry") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ geometry = av[++i];
+ } else if (strcmp(av[i], "-font") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ FontName1 = av[++i];
+ if (fontcheck(FontName1) < 0)
+ usage(av[0], "(illegal font specification)");
+ if (didfont2 == 0)
+ FontName2 = FontName1;
+ } else if (strcmp(av[i], "-font2") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ FontName2 = av[++i];
+ if (fontcheck(FontName2) < 0)
+ usage(av[0], "(illegal t2font specification)");
+ didfont2 = 1;
+ } else if (strcmp(av[i], "-S") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ Scale = strtod(av[++i], NULL);
+ if (Scale < 0.01)
+ Scale = 0.01;
+ if (Scale > 1000.0)
+ Scale = 1000.0;
+ } else if (strcmp(av[i], "-12") == 0) {
+ Military = 0;
+ Time1Fmt = Time1FmtNrm;
+ } else if (strcmp(av[i], "-24") == 0) {
+ Military = 1;
+ Time1Fmt = Time1FmtMil;
+ } else if (strcmp(av[i], "-shape") == 0) {
+ ShapedWindow = 2;
+ } else if (strcmp(av[i], "-noshape") == 0) {
+ ShapedWindow = 0;
+ } else if (strcmp(av[i], "-subsecond") == 0) {
+ WasteCPU = 1;
+ IncludeSeconds = 1;
+ } else if (strcmp(av[i], "-t") == 0) {
+ ShapeType = strtol(av[++i], NULL, 0);
+ if (ShapeType)
+ ShapedWindow = 2;
+ else
+ ShapedWindow = 0;
+ } else if (strcmp(av[i], "-b") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ AddBorder = strtol(av[++i], NULL, 0);
+ } else if (strcmp(av[i], "-r") == 0) {
+ reverse = 1;
+ } else if (strcmp(av[i], "-s") == 0) {
+ IncludeSeconds = 1;
+ } else if (strcmp(av[i], "-m") == 0) {
+ IncludeSeconds = 0;
+ } else if (strcmp(av[i], "-bg") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ bgcolor = av[++i];
+ } else if (strcmp(av[i], "-fg") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ fgcolor = av[++i];
+ } else if (strcmp(av[i], "-t2") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ t2color = av[++i];
+ } else if (strcmp(av[i], "-strftime") == 0 ||
+ strcmp(av[i], "-strftime1") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ Military = 1; /* don't tack on 'a' or 'p' */
+ Time1Fmt = av[++i];
+ if (Time1Fmt[0]) {
+ EnableText |= 1;
+ hints.height = DEFAULT_PIX_WTEXT;
+ } else {
+ EnableText &= ~1;
+ hints.height = DEFAULT_PIX;
+ }
+ didtime1fmt = 1;
+ } else if (strcmp(av[i], "-strftime2") == 0) {
+ if (i + 1 >= ac)
+ usage(av[0], "(missing argument)");
+ Time2Fmt = av[++i];
+ if (Time2Fmt[0]) {
+ EnableText |= 2;
+ } else {
+ EnableText &= ~2;
+ if (didtime1fmt == 0) {
+ Time1FmtMil = Time1NoTime2FmtMil;
+ Time1FmtNrm = Time1NoTime2FmtNrm;
+ Time1Fmt = Military ?
+ Time1FmtMil : Time1FmtNrm;
+ }
+ }
+ } else if (strcmp(av[i], "-dom") == 0) {
+ hints.height = DEFAULT_PIX;
+ Time2Fmt = "%a %d";
+ EnableText |= 2;
+ } else if (strcmp(av[i], "-nodom") == 0) {
+ hints.height = DEFAULT_PIX_WTEXT;
+ EnableText &= ~2;
+ Time2Fmt = "";
+ if (didtime1fmt == 0) {
+ Time1FmtMil = Time1NoTime2FmtMil;
+ Time1FmtNrm = Time1NoTime2FmtNrm;
+ Time1Fmt = Military ?
+ Time1FmtMil : Time1FmtNrm;
+ }
+ } else if (strcmp(av[i], "-h") == 0) {
+ usage(av[0], NULL);
+ } else if (strcmp(av[i], "-v") == 0) {
+ printf("%s: version 1.0.6\n", av[0]);
+ exit(0);
+ } else {
+ usage(av[0], av[i]);
+ }
+ }
+
+ /*
+ * If TextHasSeconds is non-zero we are going to have to do
+ * a double-buffer area copy every second. If not we can get
+ * away with XORing the second-hand.
+ */
+ if (strstr(Time1Fmt, "%s") || strstr(Time1Fmt, "%S") ||
+ strstr(Time1Fmt, "%T")) {
+ TextHasSeconds |= 1;
+ }
+ if (strstr(Time2Fmt, "%s") || strstr(Time2Fmt, "%S") ||
+ strstr(Time2Fmt, "%T")) {
+ TextHasSeconds |= 2;
+ }
+
+ Dpy = XOpenDisplay(NULL);
+
+ if (bgcolor)
+ BGColor = strtoul(bgcolor, NULL, 16);
+ else
+ BGColor = WhitePixel(Dpy, DefaultScreen(Dpy));
+
+ if (fgcolor)
+ FGColor = strtoul(fgcolor, NULL, 16);
+ else
+ FGColor = BlackPixel(Dpy, DefaultScreen(Dpy));
+
+ if (t2color)
+ T2Color = strtoul(t2color, NULL, 16);
+ else
+ T2Color = 0x0000FF;
+
+ if (reverse) {
+ ulong tmp = FGColor;
+ FGColor = BGColor;
+ BGColor = tmp;
+ }
+ if (geometry) {
+ int gravity = 0;
+
+ hints.flags |= USSize | USPosition;
+ XWMGeometry(Dpy, DefaultScreen(Dpy), geometry, NULL,
+ 1, &hints,
+ &hints.x, &hints.y,
+ &hints.width, &hints.height, &gravity);
+ }
+
+ WindowWidth = hints.width;
+ WindowHeight = hints.height;
+
+ root_win = DefaultRootWindow(Dpy);
+ Win = XCreateSimpleWindow(Dpy, root_win,
+ hints.x, hints.y, hints.width, hints.height,
+ 1, /* border width */
+ BGColor, /* border */
+ BGColor); /* background */
+ wattr.event_mask = ExposureMask|StructureNotifyMask;
+ XChangeWindowAttributes(Dpy, Win, CWEventMask, &wattr);
+ if (geometry)
+ XSetWMNormalHints(Dpy, Win, &hints);
+ classHint.res_name = wname;
+ classHint.res_class = wclass;
+ XSetClassHint(Dpy, Win, &classHint);
+
+ /*
+ * Shaped window setup. Only complain on failure if option was
+ * explicitly given.
+ */
+ if (ShapedWindow) {
+ int event_base = 0;
+ int error_base = 0;
+ if (!XShapeQueryExtension(Dpy, &event_base, &error_base)) {
+ if (ShapedWindow == 2) {
+ fprintf(stderr,
+ "Warning: shape extension not avail\n");
+ }
+ ShapedWindow = 0;
+ }
+ }
+
+ XMapWindow(Dpy, Win);
+
+ XStringListToTextProperty(&wname, 1, &name);
+ XSetWMName(Dpy, Win, &name);
+ XSync(Dpy, 1);
+
+ tzset();
+ gcsetup();
+
+ for (;;) {
+ int redraw;
+ int us;
+
+ redraw = 0;
+ process_events(&redraw);
+ us = process_time(&redraw);
+
+ if (redraw & 2) { /* full update */
+ redraw_display();
+ } else if (redraw & 1) { /* nominal update */
+ update_display(0);
+ LastTM = NextTM;
+ LastTMSecs = NextTM;
+ LastTVSubsecs = NextTVSubsecs;
+ update_display(1);
+ } else if (redraw & 4) { /* seconds only update */
+ update_seconds(0);
+ LastTMSecs = NextTM;
+ LastTVSubsecs = NextTVSubsecs;
+ update_seconds(1);
+ } else if (redraw & 8) { /* CPU waster */
+ update_seconds(0);
+ LastTVSubsecs = NextTVSubsecs;
+ update_seconds(1);
+ }
+ XFlush(Dpy);
+ usleep(us);
+ }
+ return 0;
+}
+
+static void
+process_events(int *redrawp)
+{
+ while (XEventsQueued(Dpy, QueuedAfterReading)) {
+ XEvent xev;
+
+ XNextEvent(Dpy, &xev);
+
+ switch(xev.type) {
+ case ConfigureNotify:
+ WindowWidth = xev.xconfigure.width;
+ WindowHeight = xev.xconfigure.height;
+ gcsetup();
+ /* fall through */
+ case Expose:
+ case VisibilityNotify:
+ case GraphicsExpose:
+ case MappingNotify:
+ *redrawp |= 2;
+ break;
+ }
+ }
+}
+
+static int
+process_time(int *redrawp)
+{
+ static int FirstTime = 1;
+ struct timeval tv;
+ int target_us = 1000000;
+
+ gettimeofday(&tv, NULL);
+ localtime_r(&tv.tv_sec, &NextTM);
+ NextTVSubsecs = tv;
+
+ /*
+ * Nominal update (1) or full redraw (2) (mask). These will
+ * cause a double-buffered copy. Mode 4 does not.
+ *
+ * every 1 min - no second-hand, no seconds in text.
+ * every 15 sec - second-hand, no seconds in text.
+ * every 1 sec - seconds displayed in text.
+ */
+ if (FirstTime) {
+ FirstTime = 0;
+ LastTM = NextTM;
+ LastTMSecs = NextTM;
+ LastTVSubsecs = NextTVSubsecs;
+ *redrawp |= 2;
+ } else if (IncludeSeconds) {
+ if (LastTM.tm_sec != NextTM.tm_sec &&
+ TextHasSeconds) {
+ *redrawp |= 1;
+ } else if (WasteCPU && LastTM.tm_sec != NextTM.tm_sec) {
+ /*
+ * Be really silly
+ */
+ *redrawp |= 1;
+ } else if (LastTM.tm_sec != NextTM.tm_sec &&
+ NextTM.tm_sec % 15 == 0) {
+ *redrawp |= 1;
+ } else if (LastTM.tm_sec != NextTM.tm_sec) {
+ *redrawp |= 4;
+ } else if (WasteCPU) {
+ /*
+ * Be really silly
+ */
+ *redrawp |= 8;
+ }
+ if (WasteCPU) {
+ target_us = tv.tv_usec + (100000 - tv.tv_usec % 100000);
+ }
+ } else {
+ if (LastTM.tm_sec != NextTM.tm_sec &&
+ TextHasSeconds) {
+ *redrawp |= 1;
+ } else if (LastTM.tm_min != NextTM.tm_min) {
+ *redrawp |= 1;
+ }
+ }
+
+ /*
+ * Full redraw once a minute to catch any artifacting that
+ * might build up (it isn't supposed to but...)
+ */
+ if (LastTM.tm_sec == 0 && NextTM.tm_sec != 0)
+ *redrawp |= 2;
+
+ return target_us - tv.tv_usec + 1000;
+}
+
+static void
+redraw_display(void)
+{
+ XFillRectangle(Dpy, AreaPixmap, GCBg, 0, 0, WindowWidth, WindowHeight);
+ /*XClearWindow(Dpy, Win);*/
+ LastTM = NextTM;
+ LastTMSecs = NextTM;
+ LastTVSubsecs = NextTVSubsecs;
+ update_display(2);
+}
+
+static void
+update_display(int draw_me)
+{
+ double min_ang;
+ double hour_ang;
+ GC gc1 = (draw_me) ? GCFg : GCBg;
+ GC gc2 = (draw_me) ? GCText2 : GCBg;
+ XftColor *xftgc1 = (draw_me) ? &XftGCFg : &XftGCBg;
+ XftColor *xftgc2 = (draw_me) ? &XftGCText2 : &XftGCBg;
+ size_t len;
+ char buf[256];
+ char nbuf[256];
+ int workaround = 0;
+ int i;
+
+ /*
+ * The seconds hand uses an XOR operation in order to erase and
+ * update itself in the Window directly (see the direct call to
+ * update_seconds in main()). In order to remain compatible with
+ * this direct call, the clearing operation must be called first
+ * and the setting operation must be called last to properly XOR
+ * the hand.
+ */
+ if (draw_me == 0)
+ update_seconds(draw_me);
+ min_ang = angleof(LastTM.tm_min * 60 + LastTM.tm_sec, 3600);
+ hour_ang = angleof(LastTM.tm_hour * 60 + LastTM.tm_min, 720);
+ drawhand(AreaPixmap, gc1, draw_me, min_ang, 0.8, 1);
+ drawhand(AreaPixmap, gc1, draw_me, hour_ang, 0.5, 1);
+
+ /*
+ * Static elements of the clock which do not get
+ * messed up by the second, minute, or hour-hand.
+ */
+ if (draw_me == 2) {
+ for (i = 0; i < 12; ++i) {
+ drawtick(gc1, angleof(i, 12), 0.10, 0.02);
+ }
+ for (i = 0; i < 60; ++i) {
+ drawtick(gc1, angleof(i, 60), 0.05, 0.01);
+ }
+ }
+
+ /*
+ * Static elements of the clock which do get messed up
+ * by hand or text movement.
+ */
+ if (draw_me) {
+ int w;
+ int h;
+
+ w = CW * 5 / 100;
+ h = CH * 5 / 100;
+
+ XFillArc(Dpy, AreaPixmap, gc1,
+ BW + CW2 - w/2, BH + CH2 - h/2, w, h,
+ 0, 360 * 64);
+ }
+
+ /*
+ * Text underneath
+ */
+ if (EnableText & 1) {
+ int w;
+ int h;
+ int x;
+ int y;
+
+ if (Military) {
+ strftime(nbuf, sizeof(nbuf), Time1Fmt, &NextTM);
+ strftime(buf, sizeof(buf), Time1Fmt, &LastTM);
+ len = strlen(buf);
+ } else {
+ strftime(nbuf, sizeof(nbuf), Time1Fmt, &NextTM);
+ len = strlen(nbuf);
+ len += snprintf(nbuf + len, sizeof(buf) - len,
+ "%s",
+ ((NextTM.tm_hour >= 12) ? "p" : "a"));
+
+ strftime(buf, sizeof(buf), Time1Fmt, &LastTM);
+ len = strlen(buf);
+ len += snprintf(buf + len, sizeof(buf) - len,
+ "%s",
+ ((LastTM.tm_hour >= 12) ? "p" : "a"));
+ }
+ if (XftFont1) {
+ XGlyphInfo ext;
+ XftTextExtents8(Dpy, XftFont1, buf, len, &ext);
+ w = ext.xOff;
+ h = ext.height;
+ } else if (Font1) {
+ w = XTextWidth(Font1, buf, len);
+ } else {
+ w = FW1 * len;
+ }
+
+ x = BW + CW2 - w / 2;
+ y = CH + FHA1 + BH + BH / 2;
+
+ if (ShapedWindow && (EnableText & 1) &&
+ (ShapeTextBoxX != x - (w/20) ||
+ ShapeTextBoxY != CH + 1 ||
+ ShapeTextBoxW != w + 2*(w/20) ||
+ ShapeTextBoxH != FHA1 + FHD1)) {
+ ShapeTextBoxX = x - (w/20);
+ ShapeTextBoxY = CH + 1;
+ ShapeTextBoxW = w + 2*(w/20);
+ ShapeTextBoxH = FHA1 + FHD1;
+ shaped_window_setup();
+ }
+
+ if (XftFont1) {
+ /*
+ * Xft fonts have an alpha channel for blending edges
+ * which we can't turn off, which means that we cannot
+ * erase text via XftDrawString8() (it will leave a
+ * light outline). Reset the background instead.
+ */
+ if (draw_me == 0) {
+ XFillRectangle(Dpy, AreaPixmap, GCBg,
+ x, y - h, w, h + FHD1);
+ } else {
+ XftDrawString8(XftWin, xftgc1, XftFont1,
+ x, y, buf, len);
+ }
+ } else {
+ XDrawString(Dpy, AreaPixmap, gc1, x, y, buf, len);
+ workaround = 1;
+ }
+ }
+
+ /*
+ * In-face time display. Displays in the lower half of the
+ * face when the hour hand is in the upper half, and vise-versa.
+ */
+ if ((EnableText & 2) && Time2Fmt[0]) {
+ int w;
+ int h;
+ int x;
+ int y;
+ double ang;
+
+ strftime(nbuf, sizeof(nbuf), Time2Fmt, &NextTM);
+ strftime(buf, sizeof(buf), Time2Fmt, &LastTM);
+ len = strlen(buf);
+
+ if (XftFont2) {
+ XGlyphInfo ext;
+ XftTextExtents8(Dpy, XftFont2, buf, len, &ext);
+ w = ext.xOff;
+ h = ext.height;
+ } else if (Font2) {
+ w = XTextWidth(Font2, buf, len);
+ h = Font2->ascent;
+ } else {
+ w = FW2 * len;
+ }
+ if (LastTM.tm_hour % 12 >= 9 ||
+ LastTM.tm_hour % 12 < 3) {
+ ang = M_PI / 2.0;
+ x = BW + CW2;
+ y = BH + CH2 + h;
+ } else {
+ ang = -M_PI / 2.0;
+ x = BW + CW2;
+ y = BH + CH2;
+ }
+
+ x += cos(ang) * (CW2 / 3) - w / 2;
+ y += sin(ang) * (CH2 / 3);
+
+ /*
+ * Because Text2 is in the face, it gets corrupted by the
+ * hands and we have to redraw it every time.
+ */
+ if (XftFont2) {
+ /*
+ * Xft fonts have an alpha channel for blending edges
+ * which we can't turn off, which means that we cannot
+ * erase text via XftDrawString8() (it will leave a
+ * light outline). Reset the background instead.
+ */
+ if (draw_me == 0) {
+ XFillRectangle(Dpy, AreaPixmap, GCBg,
+ x, y - h, w, h + FHD2);
+ } else {
+ XftDrawString8(XftWin, xftgc2, XftFont2,
+ x, y, buf, len);
+ }
+ } else {
+ XDrawString(Dpy, AreaPixmap, gc2, x, y, buf, len);
+ workaround = 1;
+ }
+ }
+
+ if (draw_me) {
+ /*
+ * The XSync() works around certain broken gpu drivers when
+ * a XDrawString() is followed by a GXxor XDrawLine().
+ */
+ if (workaround)
+ XSync(Dpy, 0);
+ update_seconds(draw_me);
+ }
+
+ if (draw_me) {
+ XCopyArea(Dpy, AreaPixmap, Win, GCFg,
+ 0, 0, WindowWidth, WindowHeight,
+ 0, 0);
+ }
+}
+
+/*
+ * This function is called as part of a general update and also called
+ * as part of a special update that avoids the double-buffer copy.
+ */
+static void
+update_seconds(int draw_me)
+{
+ double sec_ang;
+
+ if (IncludeSeconds) {
+ if (WasteCPU)
+ sec_ang = angleof(LastTMSecs.tm_sec * 10 +
+ LastTVSubsecs.tv_usec / 100000, 600);
+ else
+ sec_ang = angleof(LastTMSecs.tm_sec, 60);
+ drawhand(AreaPixmap, GCXor, draw_me, sec_ang, 0.895, 0);
+ drawhand(Win, GCXor, draw_me, sec_ang, 0.895, 0);
+ }
+}
+
+/*
+ * X Drawing support
+ */
+static void
+gcsetup(void)
+{
+ XGCValues gcinfo;
+ XRenderColor xrc;
+ Colormap cmap;
+ char fontbuf[256];
+ char fmtbuf[256];
+ double tw;
+
+ cmap = XDefaultColormap(Dpy, DefaultScreen(Dpy));
+
+ /*
+ * Make sure this stuff is deallocated in the correct order,
+ * reverse of dependencies.
+ */
+ if (XftWin)
+ XftDrawDestroy(XftWin);
+
+ if (GCAllocated) {
+ XFreeGC(Dpy, GCFg);
+ XFreeGC(Dpy, GCBg);
+ XFreeGC(Dpy, GCXor);
+ XFreeGC(Dpy, GCText2);
+
+ XftColorFree(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &XftGCFg);
+ XftColorFree(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &XftGCBg);
+ XftColorFree(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &XftGCText2);
+ }
+
+ if (AreaPixmap)
+ XFreePixmap(Dpy, AreaPixmap);
+
+ if (Font1) {
+ XUnloadFont(Dpy, Font1->fid);
+ Font1 = NULL;
+ }
+
+ if (Font2) {
+ XUnloadFont(Dpy, Font2->fid);
+ Font2 = NULL;
+ }
+
+ if (XftFont1) {
+ XftFontClose(Dpy, XftFont1);
+ XftFont1 = NULL;
+ }
+
+ if (XftFont2) {
+ XftFontClose(Dpy, XftFont2);
+ XftFont2 = NULL;
+ }
+
+ CW = WindowWidth;
+ CH = WindowHeight;
+
+ AreaPixmap = XCreatePixmap(Dpy, Win, CW, CH,
+ DefaultDepth(Dpy, DefaultScreen(Dpy)));
+ XftWin = XftDrawCreate(Dpy, AreaPixmap,
+ DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ XDefaultColormap(Dpy, DefaultScreen(Dpy)));
+
+ /*
+ * 12 characters in text string. Use smaller dimension
+ */
+ if (CW <= CH) {
+ tw = (CW - AddBorder * 2) / 12.0;
+ } else {
+ tw = (CH - AddBorder * 2) / 12.0;
+ }
+
+retry_font1:
+ snprintf(fmtbuf, sizeof(fmtbuf), "%s", FontName1);
+ if (strstr(fmtbuf, "%f"))
+ strstr(fmtbuf, "%f")[1] = 'd';
+ snprintf(fontbuf, sizeof(fontbuf), fmtbuf, (int)(tw + 0.5));
+ Font1 = XLoadQueryFont(Dpy, fontbuf);
+ if (Font1 == NULL) {
+ snprintf(fontbuf, sizeof(fontbuf),
+ FontName1, tw * Scale);
+ XftFont1 = XftFontOpenName(Dpy, DefaultScreen(Dpy), fontbuf);
+ if (XftFont1 == NULL && BackupFontName1) {
+ FontName1 = BackupFontName1;
+ BackupFontName1 = NULL;
+ goto retry_font1;
+ }
+ }
+
+ if (XftFont1) {
+ FW1 = XftFont1->max_advance_width;
+ FHA1 = XftFont1->ascent;
+ FHD1 = XftFont1->descent;
+ } else if (Font1) {
+ FW1 = Font1->max_bounds.width;
+ FHA1 = Font1->max_bounds.ascent;
+ FHD1 = Font1->max_bounds.descent + 1;
+ } else {
+ static int not_found_reported;
+
+ FW1 = (int)tw;
+ FHA1 = (int)tw;
+ FHD1 = 0;
+ if (not_found_reported == 0) {
+ not_found_reported = 1;
+ fprintf(stderr,
+ "Warning: font '%s' not found\n", fontbuf);
+ }
+ }
+
+ BH = CH / 50 + AddBorder;
+ BH = AddBorder;
+ BW = BH;
+ if (EnableText == 0) {
+ FW1 = 0;
+ FHA1 = 0;
+ FHD1 = 0;
+ }
+
+ CH -= FHA1 + FHD1;
+ CH -= BH * 2;
+ CW -= BW * 2;
+ CW2 = CW / 2;
+ CH2 = CH / 2;
+
+retry_font2:
+ snprintf(fmtbuf, sizeof(fmtbuf), "%s", FontName2);
+ if (strstr(fmtbuf, "%f"))
+ strstr(fmtbuf, "%f")[1] = 'd';
+ snprintf(fontbuf, sizeof(fontbuf), fmtbuf, (int)(tw * 2.0 + 0.5));
+ Font2 = XLoadQueryFont(Dpy, fontbuf);
+
+ if (Font2 == NULL) {
+ snprintf(fontbuf, sizeof(fontbuf),
+ FontName2, tw * Scale * 2.0);
+ XftFont2 = XftFontOpenName(Dpy, DefaultScreen(Dpy), fontbuf);
+ if (XftFont2 == NULL && BackupFontName2) {
+ FontName2 = BackupFontName2;
+ BackupFontName2 = NULL;
+ goto retry_font2;
+ }
+ }
+ if (XftFont2) {
+ FW2 = XftFont2->max_advance_width;
+ FHA2 = XftFont2->ascent;
+ FHD2 = XftFont2->descent;
+ } else if (Font2) {
+ FW2 = Font2->max_bounds.width;
+ FHA2 = Font2->max_bounds.ascent;
+ FHD2 = Font2->max_bounds.descent;
+ } else {
+ static int not_found_reported2;
+
+ FW2 = tw;
+ FHA2 = tw;
+ FHD2 = 0;
+ if (not_found_reported2 == 0) {
+ not_found_reported2 = 1;
+ fprintf(stderr,
+ "Warning: t2font '%s' not found\n", fontbuf);
+ }
+ }
+
+ gcinfo.plane_mask = AllPlanes;
+ gcinfo.fill_style = FillSolid;
+ if (Font1)
+ gcinfo.font = Font1->fid;
+
+ gcinfo.function = GXxor;
+ gcinfo.foreground = WhitePixel(Dpy, DefaultScreen(Dpy));
+ GCXor = XCreateGC(Dpy, Win,
+ GCFunction|GCPlaneMask|GCForeground|
+ GCBackground|GCFillStyle, &gcinfo);
+
+ gcinfo.function = GXcopy;
+ gcinfo.foreground = FGColor;
+ gcinfo.background = BGColor;
+ GCFg = XCreateGC(Dpy, AreaPixmap,
+ GCFunction|GCPlaneMask|GCForeground|
+ GCBackground|GCFillStyle|
+ (Font1 ? GCFont : 0),
+ &gcinfo);
+
+ gcinfo.foreground = BGColor;
+ gcinfo.background = BGColor;
+ GCBg = XCreateGC(Dpy, AreaPixmap,
+ GCFunction|GCPlaneMask|GCForeground|
+ GCBackground|GCFillStyle|
+ (Font1 ? GCFont : 0),
+ &gcinfo);
+
+ gcinfo.font = 0;
+ gcinfo.foreground = T2Color;
+ gcinfo.background = BGColor;
+ if (Font2)
+ gcinfo.font = Font2->fid;
+ GCText2 = XCreateGC(Dpy, AreaPixmap,
+ GCFunction|GCPlaneMask|GCForeground|
+ GCBackground|GCFillStyle|
+ (Font2 ? GCFont : 0),
+ &gcinfo);
+
+ bzero(&xrc, sizeof(xrc));
+
+ xrc.red = ((FGColor >> 16) & 0xFF) << 8;
+ xrc.green = ((FGColor >> 8) & 0xFF) << 8;
+ xrc.blue = (FGColor & 0xFF) << 8;
+ xrc.alpha = 0xFFFF;
+ XftColorAllocValue(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &xrc, &XftGCFg);
+
+ xrc.red = ((BGColor >> 16) & 0xFF) << 8;
+ xrc.green = ((BGColor >> 8) & 0xFF) << 8;
+ xrc.blue = (BGColor & 0xFF) << 8;
+ XftColorAllocValue(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &xrc, &XftGCBg);
+
+ xrc.red = ((T2Color >> 16) & 0xFF) << 8;
+ xrc.green = ((T2Color >> 8) & 0xFF) << 8;
+ xrc.blue = (T2Color & 0xFF) << 8;
+ XftColorAllocValue(Dpy, DefaultVisual(Dpy, DefaultScreen(Dpy)),
+ cmap, &xrc, &XftGCText2);
+ GCAllocated = 1;
+
+ /*
+ * Update our shape params when we know the text dimensions.
+ */
+ ShapeTextBoxX = 0;
+ ShapeTextBoxY = 0;
+ ShapeTextBoxW = 0;
+ ShapeTextBoxH = 0;
+ if (ShapedWindow && (EnableText & 1) == 0)
+ shaped_window_setup();
+}
+
+static void
+drawhand(Drawable d, GC gc, int draw_me, double a, double len, int thick)
+{
+ XPoint points[5];
+ double dta = M_PI / 2.0; /* 90 degrees in rad */
+ double mid;
+ double dthick = thick * CW2 / 20.0;
+
+ if (dthick < 1.0)
+ dthick = 1.0;
+
+ a = -a + M_PI / 2.0; /* direction of movement + top = 0 deg */
+
+ dta = M_PI / 2;
+ mid = len / 10.0;
+
+ points[0].x = BW+CW2;
+ points[0].y = BH+CH2;
+ points[1].x = BW+CW2 + cos(a) * (CW2 * mid) + cos(a - dta) * dthick;
+ points[1].y = BH+CH2 - sin(a) * (CH2 * mid) - sin(a - dta) * dthick;
+ points[2].x = BW+CW2 + cos(a) * (CW2 * len);
+ points[2].y = BH+CH2 - sin(a) * (CH2 * len);
+ points[3].x = BW+CW2 + cos(a) * (CW2 * mid) + cos(a + dta) * dthick;
+ points[3].y = BH+CH2 - sin(a) * (CH2 * mid) - sin(a + dta) * dthick;
+ points[4].x = BW+CW2; /* back to origin for XDrawLines() */
+ points[4].y = BH+CH2;
+
+ if (thick) {
+ XFillPolygon(Dpy, d, gc, points, 4,
+ Convex, CoordModeOrigin);
+ } else {
+ XDrawLine(Dpy, d, gc, points[0].x, points[0].y,
+ points[2].x, points[2].y);
+ }
+}
+
+static void
+drawtick(GC gc, double a, double len, double thick)
+{
+ XPoint points[4];
+ double dta = M_PI / 2.0; /* 90 degrees in rad */
+ double beg;
+ double dthick = thick * CW2;
+
+ if (dthick < 1.0)
+ dthick = 1.0;
+
+ a = -a + M_PI / 2.0; /* direction of movement + top = 0 deg */
+
+ dta = M_PI / 2;
+ beg = 1.0;
+ len = beg - len;
+
+ points[0].x = BW+CW2 + cos(a) * (CW2 * beg) + cos(a - dta) * dthick;
+ points[0].y = BH+CH2 - sin(a) * (CH2 * beg) - sin(a - dta) * dthick;
+ points[1].x = BW+CW2 + cos(a) * (CW2 * beg) + cos(a + dta) * dthick;
+ points[1].y = BH+CH2 - sin(a) * (CH2 * beg) - sin(a + dta) * dthick;
+ points[2].x = BW+CW2 + cos(a) * (CW2 * len) + cos(a + dta) * dthick;
+ points[2].y = BH+CH2 - sin(a) * (CH2 * len) - sin(a + dta) * dthick;
+ points[3].x = BW+CW2 + cos(a) * (CW2 * len) + cos(a - dta) * dthick;
+ points[3].y = BH+CH2 - sin(a) * (CH2 * len) - sin(a - dta) * dthick;
+
+ XFillPolygon(Dpy, AreaPixmap, gc, points, 4, Convex, CoordModeOrigin);
+}
+
+/*
+ * A font specification may have at most one '%f' in it, and
+ * no other '%' specifications. Return -1 on failure.
+ */
+static int
+fontcheck(const char *str)
+{
+ int found = 0;
+
+ while (*str) {
+ if (*str == '%') {
+ if (found || str[1] != 'f')
+ return -1;
+ found = 1;
+ }
+ ++str;
+ }
+}
+
+static void
+usage(const char *prg, const char *badopt)
+{
+ FILE *fp;
+
+ if (badopt) {
+ fprintf(stderr, "%s: malformed option: %s\n", prg, badopt);
+ fp = stderr;
+ } else {
+ fp = stdout;
+ }
+ fprintf(fp, "%s",
+ "Available options:\n"
+ " -geometry <geom>.\n"
+ " -12 \t\t- (default) 12-hour mode, tack on a/p\n"
+ " -24 \t\t- 24-hour mode\n"
+ " -dom \t\t- (default) Include short text on face.\n"
+ " -nodom \t\t- Remove text in face.\n"
+ " -strftime <fmt>\t- Replace bottom format string.\n"
+ " -strftime2 <fmt>\t- Replace face format string.\n"
+ " -font <id>\t\t- specify font, the string may contain\n"
+ " \t\t a single (optional) %f for point size.\n"
+ " \t\t (also changes -font2's default).\n"
+ " \t\t (example: \"Droid Sans-%f\"\n"
+ " -font2 <id>\t\t- similarly, specify font for dom/time2.\n"
+ " -S scale \t\t- Scale point size.\n"
+ " -shape \t\t- (default) Use shaped window.\n"
+ " -noshape \t\t- Use normal square window.\n"
+ " -subsecond\t\t- Waste some CPU.\n"
+ " -r \t\t- reverse fg/bg.\n"
+ " -s \t\t- (default) include a second's hand.\n"
+ " -m \t\t- do not include a second's hand.\n"
+ " -v \t\t- print version and exit.\n"
+ " -h \t\t- print this help text and exit.\n"
+ " -t <shape>\t\t- Shape type (only 1 right now).\n"
+ " -b <border>\t\t- Specify border size in pixels.\n"
+ " -fg ffffff\t\t- choose color, 6 hex digits.\n"
+ " -bg 000000\t\t- choose color, 6 hex digits.\n"
+ " -t2 e0e000\t\t- choose color, 6 hex digits.\n"
+ "\n"
+ " NOTE: If the text contains seconds (%s, %S, or %T)\n"
+ " then the clock window double-buffer updates\n"
+ " on a 1-second basis. With a second-hand it only\n"
+ " double-buffer updates once every 15 seconds and\n"
+ " with neither only every 60 seconds.\n"
+ );
+ exit(1);
+}
+
+/*
+ * Shaped window setup or refresh
+ */
+static void
+shaped_window_setup(void)
+{
+ static GC ShapeGC;
+ static Pixmap ShapeMask;
+ static int ShapeWidth;
+ static int ShapeHeight;
+ XGCValues xgcv;
+ int ww = WindowWidth;
+ int wh = WindowHeight;
+
+ if (ShapeMask && ShapeWidth == ww && ShapeHeight == wh)
+ return;
+ if (ShapeGC)
+ XFreeGC(Dpy, ShapeGC);
+ if (ShapeMask) {
+ XFreePixmap(Dpy, ShapeMask);
+ ShapeMask = 0;
+ }
+ ShapeWidth = ww;
+ ShapeHeight = wh;
+ ShapeMask = XCreatePixmap(Dpy, Win, ww, wh, 1);
+ ShapeGC = XCreateGC(Dpy, ShapeMask, 0, &xgcv);
+ XSetForeground(Dpy, ShapeGC, 0);
+ XFillRectangle(Dpy, ShapeMask, ShapeGC, 0, 0, ww, wh);
+ XSetForeground(Dpy, ShapeGC, 1);
+
+ switch(ShapeType) {
+ case 1:
+ default:
+ XFillArc(Dpy, ShapeMask, ShapeGC,
+ 0, 0, CW + BW, CH + BH,
+ 0, 360 * 64);
+ if (EnableText & 1) {
+ XFillRectangle(Dpy, ShapeMask, ShapeGC,
+ ShapeTextBoxX - 1,
+ ShapeTextBoxY,
+ ShapeTextBoxW + 2,
+ ShapeTextBoxH);
+ }
+ break;
+ }
+
+ XShapeCombineMask(Dpy, Win, ShapeBounding, 0, 0, ShapeMask, ShapeSet);
+ XShapeCombineMask(Dpy, Win, ShapeClip, 0, 0, ShapeMask, ShapeSet);
+}
+
+/*
+ * Geometry support
+ */
+static double
+angleof(int v1, int lim)
+{
+ double a;
+
+ a = (double)v1 / (double)lim * (M_PI * 2);
+
+ return a;
+}