quake-style console with xterm
1/*
2 * qconsole
3 * Copyright (c) 2005-2017 joshua stein <jcs@jcs.org>
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include <err.h>
30#include <getopt.h>
31#include <signal.h>
32#include <stdio.h>
33#include <stdarg.h>
34#include <stdlib.h>
35#include <string.h>
36#include <time.h>
37#include <unistd.h>
38#include <sys/types.h>
39#include <sys/wait.h>
40
41#include <X11/Xlib.h>
42#include <X11/Xutil.h>
43#include <X11/Xproto.h>
44
45#define DIR_UP 1
46#define DIR_DOWN -1
47
48#define MAX_SPEED 10
49#define DEF_SPEED (MAX_SPEED / 2)
50
51#define BORDER 4
52
53#define WIN_NAME "qconsole"
54
55struct xinfo {
56 Display* dpy;
57 Window win;
58 Window xterm;
59 int dpy_width, dpy_height;
60 int screen;
61 int speed;
62 int width, height;
63 int cur_direction;
64} main_win;
65
66/* args to pass to xterm; requires a trailing blank -into option */
67static char *xterm_args[6] = {
68 "xterm",
69 "-name", WIN_NAME,
70 "-into", "",
71 NULL
72};
73
74static int debug = 0;
75#define DPRINTF(x) { if (debug) { printf x; } };
76
77static pid_t xterm_pid = 0;
78static int shutting_down = 0;
79static int respawning = 0;
80
81extern char *__progname;
82
83void draw_window(const char *);
84void scroll(int direction, int quick);
85void xterm_spawn(void);
86void child_handler(int sig);
87void exit_handler(int sig);
88void x_error_handler(Display * d, XErrorEvent * e);
89void usage(void);
90
91int
92main(int argc, char* argv[])
93{
94 char *display = NULL, *p;
95 int ch;
96
97 memset(&main_win, 0, sizeof(struct xinfo));
98 main_win.speed = DEF_SPEED;
99 main_win.cur_direction = DIR_UP;
100
101 /* will get set according to screen height if not overridden by -h */
102 main_win.height = 0;
103
104 while ((ch = getopt(argc, argv, "dh:s:")) != -1)
105 switch (ch) {
106 case 'd':
107 debug = 1;
108 break;
109
110 case 'h':
111 main_win.height = strtol(optarg, &p, 10);
112 if (*p || main_win.height < 1)
113 errx(1, "illegal height value -- %s", optarg);
114
115 break;
116
117 case 's':
118 main_win.speed = strtol(optarg, &p, 10);
119 if (*p || main_win.speed < 1 ||
120 main_win.speed > MAX_SPEED)
121 errx(1, "speed must be between 1 and %d",
122 MAX_SPEED);
123
124 break;
125
126 default:
127 usage();
128 }
129
130 argc -= optind;
131 argv += optind;
132
133 /* die gracefully */
134 signal(SIGINT, exit_handler);
135 signal(SIGTERM, exit_handler);
136
137 /* handle xterm exiting */
138 signal(SIGCHLD, child_handler);
139
140 /* fire up initial xterm */
141 draw_window(display);
142 xterm_spawn();
143
144 /* wait for events */
145 for (;;) {
146 XEvent event;
147 memset(&event, 0, sizeof(XEvent));
148 XNextEvent(main_win.dpy, &event);
149
150 switch (event.type) {
151 case ReparentNotify: {
152 DPRINTF(("xterm spawned and reparented to us\n"));
153
154 XReparentEvent *e = (XReparentEvent *) &event;
155 main_win.xterm = e->window;
156
157 /* move completely off-screen */
158 XMoveWindow(main_win.dpy, main_win.xterm, -1, -1);
159 XResizeWindow(main_win.dpy, main_win.xterm,
160 main_win.width, main_win.height - BORDER);
161
162 break;
163 }
164
165 case UnmapNotify:
166 DPRINTF(("xterm unmapped, respawning\n"));
167
168 xterm_spawn();
169
170 break;
171
172 case KeyRelease:
173 scroll(-(main_win.cur_direction), 0);
174 break;
175
176 case FocusOut:
177 if (main_win.cur_direction == DIR_DOWN)
178 XSetInputFocus(main_win.dpy, main_win.xterm,
179 RevertToParent, CurrentTime);
180
181 break;
182 }
183 }
184
185 return (0);
186}
187
188void
189draw_window(const char *display)
190{
191 int rc;
192 XSetWindowAttributes attributes;
193 XTextProperty win_name_prop;
194 char *win_name = WIN_NAME;
195
196 if (!(main_win.dpy = XOpenDisplay(display)))
197 errx(1, "Unable to open display %s", XDisplayName(display));
198
199 XSetErrorHandler ((XErrorHandler) x_error_handler);
200
201 if (main_win.height == 0)
202 main_win.height = DisplayHeight(main_win.dpy,
203 main_win.screen) / 5;
204
205 main_win.screen = DefaultScreen(main_win.dpy);
206 main_win.width = main_win.dpy_width = DisplayWidth(main_win.dpy,
207 main_win.screen);
208 main_win.dpy_height = DisplayHeight(main_win.dpy, main_win.screen);
209 main_win.win = XCreateSimpleWindow(main_win.dpy,
210 RootWindow(main_win.dpy, main_win.screen),
211 0, -(main_win.height),
212 main_win.width, main_win.height,
213 0,
214 BlackPixel(main_win.dpy, main_win.screen),
215 BlackPixel(main_win.dpy, main_win.screen));
216
217 if (!(rc = XStringListToTextProperty(&win_name, 1, &win_name_prop)))
218 errx(1, "XStringListToTextProperty");
219
220 XSetWMName(main_win.dpy, main_win.win, &win_name_prop);
221
222 /* remove all window manager decorations and force our position/size */
223 attributes.override_redirect = True;
224 XChangeWindowAttributes(main_win.dpy, main_win.win, CWOverrideRedirect,
225 &attributes);
226
227 XMapRaised(main_win.dpy, main_win.win);
228
229 XSync(main_win.dpy, False);
230
231 /* we need to know when the xterm gets reparented to us */
232 XSelectInput(main_win.dpy, main_win.win, SubstructureNotifyMask |
233 FocusChangeMask);
234
235 /* bind to control+o */
236 /* TODO: allow this key to be configurable */
237 XGrabKey(main_win.dpy, XKeysymToKeycode(main_win.dpy, XK_o),
238 ControlMask, DefaultRootWindow(main_win.dpy), False, GrabModeAsync,
239 GrabModeAsync);
240}
241
242void
243scroll(int direction, int quick)
244{
245 int cur_x, cur_y, inc, dest;
246 unsigned width, height, bw, depth;
247 Window root;
248
249 if (direction == DIR_DOWN) {
250 XSetInputFocus(main_win.dpy, main_win.xterm, RevertToParent,
251 CurrentTime);
252 dest = 0;
253 } else {
254 XSetInputFocus(main_win.dpy, PointerRoot, RevertToParent,
255 CurrentTime);
256 dest = -(main_win.height);
257 }
258
259 XGetGeometry(main_win.dpy, main_win.win, &root, &cur_x, &cur_y, &width,
260 &height, &bw, &depth);
261
262 DPRINTF(("scrolling from %d to %d%s\n", cur_y, dest,
263 (quick ? " quickly" : "")));
264
265 if (direction == DIR_DOWN)
266 XRaiseWindow(main_win.dpy, main_win.win);
267
268 /* smoothly scroll to our destination */
269 while (!quick && cur_y != dest) {
270 inc = (abs(dest - cur_y) / MAX_SPEED) / (MAX_SPEED + 1 -
271 main_win.speed);
272
273 if (inc < 1)
274 inc = 1;
275
276 if ((direction == DIR_DOWN && (cur_y + inc >= dest)) ||
277 (direction == DIR_UP && (cur_y - inc < dest)))
278 break;
279
280 cur_y -= (inc * direction);
281
282 XMoveWindow(main_win.dpy, main_win.win, 0, cur_y);
283 XSync(main_win.dpy, False);
284 }
285
286 XMoveWindow(main_win.dpy, main_win.win, 0, dest);
287 main_win.cur_direction = direction;
288
289 if (direction == DIR_DOWN)
290 XRaiseWindow(main_win.dpy, main_win.win);
291 else
292 XLowerWindow(main_win.dpy, main_win.win);
293}
294
295void
296child_handler(int sig)
297{
298 if (!xterm_pid)
299 return;
300
301 if (sig)
302 DPRINTF(("got SIGCHLD, cleaning up after xterm pid %d\n",
303 xterm_pid));
304
305 waitpid(-1, NULL, WNOHANG);
306 xterm_pid = 0;
307}
308
309void
310xterm_spawn(void)
311{
312 pid_t pid;
313
314 /* this is called in response to SIGCHLD, but signal handlers can't do
315 * any x11 operations */
316
317 if (shutting_down) {
318 DPRINTF(("shutting down, not respawning\n"));
319
320 return;
321 }
322
323 DPRINTF(("in xterm_spawn\n"));
324
325 /* clean up if previous xterm died */
326 if (xterm_pid) {
327 child_handler(0);
328
329 scroll(DIR_UP, 1);
330 XUnmapSubwindows(main_win.dpy, main_win.win);
331 }
332
333 if (!xterm_pid && !shutting_down) {
334 DPRINTF(("forking new xterm into %d\n", (int)main_win.win));
335
336 switch (pid = fork()) {
337 case -1:
338 errx(1, "unable to fork");
339
340 case 0:
341 /* fork xterm and pass our window id to -into opt */
342 asprintf(&xterm_args[4], "%d", (int)main_win.win);
343 execvp("xterm", xterm_args);
344 exit(0);
345
346 default:
347 xterm_pid = pid;
348 /* we are now able to be killed */
349 respawning = 1;
350 }
351 }
352}
353
354void
355exit_handler(int sig)
356{
357 shutting_down = 1;
358
359 DPRINTF(("in exit_handler with sig %d, shutting down\n", sig));
360
361 if (xterm_pid)
362 kill(xterm_pid, SIGKILL);
363
364 exit(0);
365}
366
367void
368x_error_handler(Display * d, XErrorEvent * e)
369{
370 if (e->error_code == BadAccess && e->request_code == X_GrabKey)
371 errx(1, "could not bind key. possibly another application "
372 "bound to it?\n");
373}
374
375void
376usage(void)
377{
378 fprintf(stderr, "usage: %s [-d] [-h <height>] [-s <speed 1-%d>]\n",
379 __progname, MAX_SPEED);
380 exit(1);
381}