Calendar/time for progman
1/* vim:ts=8
2 *
3 * Copyright (c) 2023 joshua stein <jcs@jcs.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <err.h>
19#include <getopt.h>
20#include <poll.h>
21#include <signal.h>
22#include <stdio.h>
23#include <stdlib.h>
24#include <string.h>
25#include <unistd.h>
26#include <sys/fcntl.h>
27#include <sys/types.h>
28
29#include <X11/Xlib.h>
30#include <X11/Xutil.h>
31#include <X11/xpm.h>
32
33#include "icons/calendar.xpm"
34#include "icons/digits.xpm"
35
36struct {
37 Display *dpy;
38 int screen;
39 Window win;
40 XWMHints hints;
41 GC gc;
42 Pixmap calendar_pm;
43 Pixmap calendar_pm_mask;
44 XpmAttributes calendar_pm_attrs;
45 Pixmap digits_pm;
46 Pixmap digits_pm_mask;
47 XpmAttributes digits_pm_attrs;
48 Pixmap icon_pm;
49 char *title_fmt;
50} xinfo = { 0 };
51
52extern char *__progname;
53
54void killer(int);
55void usage(void);
56void redraw_icon(int);
57
58int exit_msg[2];
59int last_day = 0;
60int last_min = -1;
61
62#define WINDOW_WIDTH 200
63#define WINDOW_HEIGHT 100
64#define DEFAULT_TITLE_FMT "%a %H:%M"
65
66int
67main(int argc, char* argv[])
68{
69 XEvent event;
70 XSizeHints *hints;
71 XGCValues gcv;
72 XWindowAttributes xgwa;
73 struct pollfd pfd[2];
74 struct sigaction act;
75 char *display = NULL;
76 int ch;
77
78 while ((ch = getopt(argc, argv, "d:f:")) != -1) {
79 switch (ch) {
80 case 'd':
81 display = optarg;
82 break;
83 case 'f':
84 xinfo.title_fmt = strdup(optarg);
85 break;
86 default:
87 usage();
88 }
89 }
90 argc -= optind;
91 argv += optind;
92
93 if (!(xinfo.dpy = XOpenDisplay(display)))
94 errx(1, "can't open display %s", XDisplayName(display));
95
96#ifdef __OpenBSD_
97 if (pledge("stdio") == -1)
98 err(1, "pledge");
99#endif
100
101 if (xinfo.title_fmt == NULL)
102 xinfo.title_fmt = strdup(DEFAULT_TITLE_FMT);
103
104 /* setup exit handler pipe that we'll poll on */
105 if (pipe2(exit_msg, O_CLOEXEC) != 0)
106 err(1, "pipe2");
107 act.sa_handler = killer;
108 act.sa_flags = 0;
109 sigaction(SIGTERM, &act, NULL);
110 sigaction(SIGINT, &act, NULL);
111 sigaction(SIGHUP, &act, NULL);
112
113 xinfo.screen = DefaultScreen(xinfo.dpy);
114 xinfo.win = XCreateSimpleWindow(xinfo.dpy,
115 RootWindow(xinfo.dpy, xinfo.screen),
116 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 0,
117 BlackPixel(xinfo.dpy, xinfo.screen),
118 WhitePixel(xinfo.dpy, xinfo.screen));
119 gcv.foreground = 1;
120 gcv.background = 0;
121 xinfo.gc = XCreateGC(xinfo.dpy, xinfo.win, GCForeground | GCBackground,
122 &gcv);
123 XSetFunction(xinfo.dpy, xinfo.gc, GXcopy);
124
125 /* load XPMs */
126 if (XpmCreatePixmapFromData(xinfo.dpy,
127 RootWindow(xinfo.dpy, xinfo.screen),
128 calendar_xpm, &xinfo.calendar_pm, &xinfo.calendar_pm_mask,
129 &xinfo.calendar_pm_attrs) != 0)
130 errx(1, "XpmCreatePixmapFromData failed");
131
132 if (XpmCreatePixmapFromData(xinfo.dpy,
133 RootWindow(xinfo.dpy, xinfo.screen),
134 digits_xpm, &xinfo.digits_pm, &xinfo.digits_pm_mask,
135 &xinfo.digits_pm_attrs) != 0)
136 errx(1, "XpmCreatePixmapFromData failed");
137
138 XGetWindowAttributes(xinfo.dpy, xinfo.win, &xgwa);
139 xinfo.icon_pm = XCreatePixmap(xinfo.dpy,
140 RootWindow(xinfo.dpy, xinfo.screen),
141 xinfo.calendar_pm_attrs.width,
142 xinfo.calendar_pm_attrs.height,
143 xgwa.depth);
144
145 hints = XAllocSizeHints();
146 if (!hints)
147 err(1, "XAllocSizeHints");
148 hints->flags = PMinSize | PMaxSize;
149 hints->min_width = WINDOW_WIDTH;
150 hints->min_height = WINDOW_HEIGHT;
151 hints->max_width = WINDOW_WIDTH;
152 hints->max_height = WINDOW_HEIGHT;
153#if 0 /* disabled until progman displays minimize on non-dialog wins */
154 XSetWMNormalHints(xinfo.dpy, xinfo.win, hints);
155#endif
156
157 redraw_icon(1);
158
159 xinfo.hints.initial_state = IconicState;
160 xinfo.hints.flags |= StateHint;
161 XSetWMHints(xinfo.dpy, xinfo.win, &xinfo.hints);
162 XMapWindow(xinfo.dpy, xinfo.win);
163
164 memset(&pfd, 0, sizeof(pfd));
165 pfd[0].fd = ConnectionNumber(xinfo.dpy);
166 pfd[0].events = POLLIN;
167 pfd[1].fd = exit_msg[0];
168 pfd[1].events = POLLIN;
169
170 /* we need to know when we're exposed */
171 XSelectInput(xinfo.dpy, xinfo.win, ExposureMask);
172
173 for (;;) {
174 if (!XPending(xinfo.dpy)) {
175 poll(pfd, 2, 900);
176 if (pfd[1].revents)
177 /* exit msg */
178 break;
179
180 if (!XPending(xinfo.dpy)) {
181 redraw_icon(0);
182 continue;
183 }
184 }
185
186 XNextEvent(xinfo.dpy, &event);
187
188 switch (event.type) {
189 case Expose:
190 redraw_icon(1);
191 break;
192 }
193 }
194
195 XFreePixmap(xinfo.dpy, xinfo.calendar_pm);
196 XFreePixmap(xinfo.dpy, xinfo.calendar_pm_mask);
197 XFreePixmap(xinfo.dpy, xinfo.digits_pm);
198 XFreePixmap(xinfo.dpy, xinfo.icon_pm);
199 XDestroyWindow(xinfo.dpy, xinfo.win);
200 XFree(hints);
201 XCloseDisplay(xinfo.dpy);
202 free(xinfo.title_fmt);
203
204 return 0;
205}
206
207void
208killer(int sig)
209{
210 if (write(exit_msg[1], &exit_msg, 1))
211 return;
212
213 warn("failed to exit cleanly");
214 exit(1);
215}
216
217void
218usage(void)
219{
220 fprintf(stderr, "usage: %s %s\n", __progname,
221 "[-d display] [-f title date format]");
222 exit(1);
223}
224
225void
226redraw_icon(int update_win)
227{
228 XTextProperty title_prop;
229 XWindowAttributes xgwa;
230 char title[100];
231 char *titlep = (char *)&title;
232 int rc, dwidth, xo, yo;
233 struct tm *tm;
234 time_t now;
235
236 time(&now);
237 tm = localtime(&now);
238
239 if (tm->tm_min != last_min) {
240#ifdef DEBUG
241 printf("minute changed, updating title\n");
242#endif
243 last_min = tm->tm_min;
244
245 strftime(title, sizeof(title), xinfo.title_fmt, tm);
246
247 /* update icon and window titles */
248 if (!(rc = XStringListToTextProperty(&titlep, 1, &title_prop)))
249 errx(1, "XStringListToTextProperty");
250 XSetWMIconName(xinfo.dpy, xinfo.win, &title_prop);
251 XStoreName(xinfo.dpy, xinfo.win, title);
252 }
253
254 if (tm->tm_mday == last_day && !update_win)
255 return;
256
257 if (tm->tm_mday != last_day) {
258#ifdef DEBUG
259 printf("day changed, rebuilding icon\n");
260#endif
261 last_day = tm->tm_mday;
262
263 XCopyArea(xinfo.dpy, xinfo.calendar_pm, xinfo.icon_pm, xinfo.gc,
264 0, 0,
265 xinfo.calendar_pm_attrs.width,
266 xinfo.calendar_pm_attrs.height,
267 0, 0);
268
269 dwidth = (xinfo.digits_pm_attrs.width / 10);
270 if (tm->tm_mday >= 10) {
271 XCopyArea(xinfo.dpy,
272 xinfo.digits_pm, xinfo.icon_pm, xinfo.gc,
273 dwidth * (tm->tm_mday / 10),
274 0,
275 dwidth,
276 xinfo.digits_pm_attrs.height,
277 19, 28);
278 XCopyArea(xinfo.dpy,
279 xinfo.digits_pm, xinfo.icon_pm, xinfo.gc,
280 dwidth * (tm->tm_mday % 10),
281 0,
282 dwidth,
283 xinfo.digits_pm_attrs.height,
284 33, 28);
285 } else {
286 XCopyArea(xinfo.dpy,
287 xinfo.digits_pm, xinfo.icon_pm, xinfo.gc,
288 dwidth * tm->tm_mday, 0,
289 dwidth,
290 xinfo.digits_pm_attrs.height,
291 26, 28);
292 }
293
294 /* update the icon */
295 xinfo.hints.icon_pixmap = xinfo.icon_pm;
296 xinfo.hints.icon_mask = xinfo.calendar_pm_mask;
297 xinfo.hints.flags = IconPixmapHint | IconMaskHint;
298 XSetWMHints(xinfo.dpy, xinfo.win, &xinfo.hints);
299
300 update_win = 1;
301 }
302
303 if (update_win) {
304 /* draw it in the center of the window */
305#ifdef DEBUG
306 printf("updating window\n");
307#endif
308 XGetWindowAttributes(xinfo.dpy, xinfo.win, &xgwa);
309 xo = (xgwa.width / 2) - (xinfo.calendar_pm_attrs.width / 2);
310 yo = (xgwa.height / 2) - (xinfo.calendar_pm_attrs.height / 2);
311 XClearWindow(xinfo.dpy, xinfo.win);
312 XSetClipMask(xinfo.dpy, xinfo.gc, xinfo.calendar_pm_mask);
313 XSetClipOrigin(xinfo.dpy, xinfo.gc, xo, yo);
314 XSetFunction(xinfo.dpy, xinfo.gc, GXcopy);
315 XCopyArea(xinfo.dpy, xinfo.icon_pm, xinfo.win, xinfo.gc,
316 0, 0,
317 xinfo.calendar_pm_attrs.width,
318 xinfo.calendar_pm_attrs.height,
319 xo, yo);
320 XSetClipMask(xinfo.dpy, xinfo.gc, xinfo.calendar_pm_mask);
321 XSetClipOrigin(xinfo.dpy, xinfo.gc, 0, 0);
322 }
323}