Add session management support
[cteddy.git] / src / cteddy.cxx
blob6203609b36cea053b57f264d6e162a13f7234695
1 // Copyright 2008 Philip Allison <sane@not.co.uk>
3 // This file is part of cteddy.
4 //
5 // cteddy is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // cteddy is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with cteddy. If not, see <http://www.gnu.org/licenses/>.
20 // Includes
23 // Project headers
24 #ifdef HAVE_CONFIG_H
25 # include "config.h"
26 #endif
28 // Language headers
29 #include <string>
30 #include <iostream>
31 #include <cstring>
32 #include <cerrno>
34 // Library headers
35 #include <gtk/gtk.h>
36 #include <gdk/gdkkeysyms.h>
37 #ifdef __USE_SM
38 # include <X11/SM/SMlib.h>
39 #endif
41 // System headers
42 #include <sys/types.h>
43 #include <sys/stat.h>
44 #include <fcntl.h>
45 #include <unistd.h>
46 #ifdef __USE_SM
47 # include <syslog.h>
48 #endif
52 // Globals
55 // Main window
56 GtkWidget* window;
58 // GTK/GDK returned pointer
59 GError* gerr = NULL;
61 // Image to display and its dimensions
62 GdkPixbuf* pixbuf;
63 gint width, height;
65 // Whether or not the key binding for quit is disabled
66 bool noquit = false;
68 #ifdef __USE_SM
69 // Options structure - needed so that sm_save_yourself can know about the
70 // values of various program options which are usually only in main()'s scope
71 typedef struct smopts
73 bool sticky;
74 bool noquit;
75 bool ontop;
76 char* image;
77 char* progname;
78 char* smcid;
80 #endif
84 // Implementation
87 // Re-paint the window when necessary
88 gboolean handle_expose(GtkWidget* widget, GdkEventExpose* event)
90 cairo_t* cairo = gdk_cairo_create(widget->window);
92 // Blank the surface
93 cairo_set_source_rgba (cairo, 1.0f, 1.0f, 1.0f, 0.0f);
94 cairo_set_operator (cairo, CAIRO_OPERATOR_SOURCE);
95 cairo_paint (cairo);
97 // Set cairo's source pattern from the desired image
98 gdk_cairo_set_source_pixbuf(cairo, pixbuf, 0.0f, 0.0f);
100 // Draw the image
101 cairo_rectangle(cairo, 0.0f, 0.0f,
102 static_cast<double>(width), static_cast<double>(height));
103 cairo_fill(cairo);
105 cairo_destroy(cairo);
107 return false;
110 // Handle mouse click events
111 gboolean window_clicked(GtkWidget* widget, GdkEventButton* event)
113 if (event->type == GDK_BUTTON_PRESS)
114 // Move the window with the mouse
115 gtk_window_begin_move_drag(GTK_WINDOW(window),
116 event->button, static_cast<gint>(event->x_root),
117 static_cast<gint>(event->y_root), event->time);
118 return true;
121 // Set up an RGBA colourmap for the window
122 void handle_screen(GtkWidget* window, GdkScreen* old, gpointer data)
124 GdkScreen* s = gtk_widget_get_screen(window);
125 GdkColormap* c = gdk_screen_get_rgba_colormap(s);
127 if (!c)
128 // Use standard RGB colourmap if we must. Window will have
129 // an ugly black border if this is the case.
130 c = gdk_screen_get_rgb_colormap(s);
132 gtk_widget_set_colormap(window, c);
135 // Handle key press events
136 void handle_key_press(GtkWidget *Window, GdkEventKey *theEvent)
138 switch( theEvent->keyval )
140 case GDK_q:
141 // Quit on "q"
142 if (!noquit)
143 gtk_main_quit();
144 break;
146 default:
147 // Ignore all other keys
148 break;
153 // Function for seeing whether or not a particular image file exists.
154 // If we find a file, copy the filename into image and return true, otherwise return false.
155 bool image_exists(std::string& image, const std::string& find_image)
158 // Set of image extensions to try appending when searching for an image
159 static const char* extensions[16];
160 extensions[0] = ".png"; extensions[1] = ".gif"; extensions[2] = ".jpg";
161 extensions[3] = ".jpeg"; extensions[4] = ".pnm"; extensions[5] = ".xpm";
162 extensions[6] = ".tif"; extensions[7] = ".tiff";
163 extensions[8] = ".PNG"; extensions[9] = ".GIF"; extensions[10] = ".JPG";
164 extensions[11] = ".JPEG"; extensions[12] = ".PNM"; extensions[13] = ".XPM";
165 extensions[14] = ".TIF"; extensions[15] = ".TIFF";
167 struct stat s;
168 // Try unadulterated filename, then extensions in lower & upper case
169 for (size_t i = 0; i < 16; ++i)
171 std::string ximage(find_image);
172 if (i > 0) ximage.append(extensions[i - 1]);
173 if (stat(ximage.c_str(), &s) == 0)
175 image = ximage;
176 return true;
179 return false;
182 #ifdef __USE_SM
183 // Session management "SaveYourself" callback
184 void sm_save_yourself(SmcConn conn, SmPointer data, int type, Bool shutdown, int istyle, Bool fast)
186 // Set the required properties for session management
187 smopts* opts = static_cast<smopts*>(data);
189 // Program name
190 SmProp progname;
191 progname.name = SmProgram;
192 progname.type = SmARRAY8;
193 progname.num_vals = 1;
194 SmPropValue progname_value;
195 progname_value.length = strlen(opts->progname);
196 progname_value.value = opts->progname;
197 progname.vals = &progname_value;
199 // User ID
200 SmProp uid;
201 uid.name = SmUserID;
202 uid.type = SmARRAY8;
203 uid.num_vals = 1;
204 SmPropValue uid_value;
205 const char* c_uidstr = g_get_user_name();
206 char* uidstr = new char[strlen(c_uidstr) + 1];
207 strcpy(uidstr, c_uidstr);
208 uid_value.length = strlen(c_uidstr);
209 uid_value.value = uidstr;
210 uid.vals = &uid_value;
212 // Current working directory
213 SmProp cwd;
214 cwd.name = SmCurrentDirectory;
215 cwd.type = SmARRAY8;
216 cwd.num_vals = 1;
217 SmPropValue cwd_value;
218 const char* c_cwdstr = g_get_current_dir();
219 char* cwdstr = new char[strlen(c_cwdstr) + 1];
220 strcpy(cwdstr, c_cwdstr);
221 cwd_value.length = strlen(c_cwdstr);
222 cwd_value.value = cwdstr;
223 cwd.vals = &cwd_value;
225 // Clone & restart commands
226 SmPropValue command[6];
227 int numvals = 0;
228 command[numvals++] = progname_value;
229 SmProp restartcommand, clonecommand;
230 char sv[] = "-stick";
231 char fv[] = "-float";
232 char qv[] = "-noquit";
234 if (opts->sticky)
236 SmPropValue stickprop_value;
237 stickprop_value.value = sv;
238 stickprop_value.length = 6;
239 command[numvals++] = stickprop_value;
242 if (opts->ontop)
244 SmPropValue floatprop_value;
245 floatprop_value.value = fv;
246 floatprop_value.length = 6;
247 command[numvals++] = floatprop_value;
250 if (opts->noquit)
252 SmPropValue noquitprop_value;
253 noquitprop_value.value = qv;
254 noquitprop_value.length = 7;
255 command[numvals++] = noquitprop_value;
258 SmPropValue image_value;
259 char* imagestr = new char[strlen(opts->image) + 3];
260 imagestr[0] = '-'; imagestr[1] = 'F';
261 strcpy(imagestr + 2, opts->image);
262 image_value.value = imagestr;
263 image_value.length = strlen(imagestr);
264 command[numvals++] = image_value;
266 SmPropValue smcid_value;
267 std::string c_smcidarg("-smcid");
268 c_smcidarg.append(opts->smcid);
269 char* smcidarg = new char[c_smcidarg.length() + 1];
270 strcpy(smcidarg, c_smcidarg.c_str());
271 smcid_value.value = smcidarg;
272 smcid_value.length = c_smcidarg.length();
273 command[numvals++] = smcid_value;
275 restartcommand.num_vals = numvals;
276 clonecommand.num_vals = numvals - 1;
277 restartcommand.vals = command;
278 clonecommand.vals = command;
280 restartcommand.name = SmRestartCommand;
281 restartcommand.type = SmLISTofARRAY8;
282 clonecommand.name = SmCloneCommand;
283 clonecommand.type = SmLISTofARRAY8;
285 SmProp* props[5];
286 props[0] = &progname;
287 props[1] = &uid;
288 props[2] = &cwd;
289 props[3] = &restartcommand;
290 props[4] = &clonecommand;
291 SmcSetProperties(conn, 5, props);
293 // Delete any non-const things we had to explicitly allocate
294 delete[] uidstr;
295 delete[] cwdstr;
296 delete[] smcidarg;
297 delete[] imagestr;
299 // Send SaveYourselfDone.
300 SmcSaveYourselfDone(conn, True);
303 // Session management "Save complete" callback
304 void sm_save_complete(SmcConn conn, SmPointer data)
308 // Session management "Die" callback
309 void sm_die(SmcConn conn, SmPointer data)
311 // TODO - Have GTK quit event handler disconnect from SM if in use
312 SmcCloseConnection(conn, 0, NULL);
313 gtk_main_quit();
316 // Pump ICE messages when data comes in
317 gboolean sm_pump_ice(GIOChannel* chan, GIOCondition cond, gpointer data)
319 IceProcessMessages(static_cast<IceConn>(data), NULL, NULL);
320 return true;
323 // Called whenever an ICE connection is opened or closed
324 void ice_connection_watch(IceConn conn, IcePointer data, Bool opening, IcePointer* wdata)
326 if (opening)
328 // When we get a new ICE connection, install a IO channel which will pump
329 // ICE messages when data is waiting
330 int icefd = IceConnectionNumber(conn);
331 GIOChannel* ioc = g_io_channel_unix_new(icefd);
332 g_io_add_watch(ioc, G_IO_IN, sm_pump_ice, conn);
333 *wdata = ioc;
334 } else {
335 // When an ICE connection closes, close the IO channel
336 g_io_channel_unref(static_cast<GIOChannel*>(*wdata));
339 #endif
341 // Entry point
342 int main (int argc, char* argv[])
344 gtk_init(&argc, &argv);
346 // Whether or not to set the window's keep-on-top hint
347 bool ontop = false;
349 // Whether or not the window's sticky hint should be set
350 bool sticky = false;
352 // Whether or not to fork into the background
353 bool background = true;
355 #ifdef __USE_SM
356 // Whether or not to use session management
357 bool sm = true;
359 // Old session client ID, if being restored from a saved session
360 char* oldsmcid = NULL;
362 // Set of callbacks
363 smopts opts;
364 SmcCallbacks smcallbacks;
365 smcallbacks.save_yourself.callback = sm_save_yourself;
366 smcallbacks.save_yourself.client_data = &opts;
367 smcallbacks.die.callback = sm_die;
368 smcallbacks.die.client_data = NULL;
369 smcallbacks.save_complete.callback = sm_save_complete;
370 smcallbacks.save_complete.client_data = NULL;
371 #endif
373 // Mimic the image loading behaviour of xteddy, to an extent.
375 // Start from the name of the executable itself
376 std::string image(argv[0]);
377 #ifdef __USE_SM
378 char* smimage = argv[0];
379 #endif
381 // Take just the last component of the path
382 std::string::size_type s = image.find_last_of('/');
383 if (s != std::string::npos)
384 image = image.substr(s + 1);
386 // Parse options
387 // XXX This code is really quick and nasty
388 for (int i = 1; i < argc; ++i)
390 // Allow image specification with -F<file>
391 if (strncmp(argv[i], "-F", 2) == 0)
393 image = argv[i] + 2;
394 #ifdef __USE_SM
395 smimage = argv[i] + 2;
396 #endif
398 // Allow disablement of "q" keybinding
399 else if (strncmp(argv[i], "-noquit", 7) == 0)
400 noquit = true;
401 // Allow keep-on-top
402 else if (strncmp(argv[i], "-float", 6) == 0)
403 ontop = true;
404 // Allow sticking to all desktops
405 else if (strncmp(argv[i], "-stick", 6) == 0)
406 sticky = true;
407 // Allow disablement of backgrounding
408 // Implies -nosm (see below)
409 else if (strncmp(argv[i], "-nobg", 5) == 0)
410 background = false;
411 #ifdef __USE_SM
412 // Allow disablement of session management
413 else if (strncmp(argv[i], "-nosm", 5) == 0)
414 sm = false;
415 // Read in old session client ID
416 else if (strncmp(argv[i], "-smcid", 6) == 0)
417 oldsmcid = argv[i] + 6;
418 #endif
419 else
420 std::cerr << "Unrecognised option: " << argv[i] << std::endl;
423 #ifdef __USE_SM
424 // If we're being restarted by the session manager, don't bother
425 // backgrounding, as we aren't attached to a terminal anyway
426 if (sm && oldsmcid != NULL)
427 background = false;
429 // On the other hand, don't use session management if not forking
430 // into the background, if being started directly by the user
431 // rather than restarted as part of session.
432 // XXX This is how -nobg imples -nosm.
433 else if (!background && oldsmcid == NULL)
434 sm = false;
436 if (sm)
438 // Open & monitor a GIOChannel each time there is a new ICE connection
439 IceAddConnectionWatch(ice_connection_watch, NULL);
441 // Fill in SM opts so sm_save_yourself can set properties
442 opts.sticky = sticky;
443 opts.ontop = ontop;
444 opts.noquit = noquit;
445 opts.progname = argv[0];
446 opts.image = smimage;
448 // Try to connect to the session manager, displaying an error if we can't
449 char smerr[512];
450 memset(smerr, '\0', 512);
451 if (SmcOpenConnection(NULL, NULL, SmProtoMajor, SmProtoMinor,
452 SmcSaveYourselfProcMask | SmcDieProcMask | SmcSaveCompleteProcMask,
453 &smcallbacks, oldsmcid, &(opts.smcid), 512, smerr) == NULL)
455 if (strlen(smerr) == 0)
456 std::cerr << "Cannot connect to session manager; unknown error." << std::endl;
457 else
458 std::cerr << "Cannot connect to session manager: " << smerr << std::endl;
460 // Continue anyway, just without SM (for convenience of anyone without a session manager)
461 sm = false;
465 // TODO - Use GTK dialogues to display errors, not stderr, as we may not be attached to a terminal
466 #endif
468 // If the given path contains slashes, assume it is absolute or
469 // relative to the current working directory and use it verbatim.
470 // Otherwise, search for it.
471 if (image.find_last_of('/') == std::string::npos)
473 // Search for the image
474 bool found = false;
475 std::string searchimage(PKGDATADIR "/");
476 searchimage.append(image);
477 found = image_exists(image, searchimage);
478 // Try converting a leading x to a c and vice versa
479 if (!found && image.at(0) == 'x')
481 std::string ximage(PKGDATADIR "/c");
482 ximage.append(image.substr(1));
483 found = image_exists(image, ximage);
485 if (!found && image.at(0) == 'c')
487 std::string ximage(PKGDATADIR "/x");
488 ximage.append(image.substr(1));
489 found = image_exists(image, ximage);
491 // Try prepending c or x
492 if (!found)
494 std::string ximage(PKGDATADIR "/c");
495 ximage.append(image);
496 found = image_exists(image, ximage);
498 if (!found)
500 std::string ximage(PKGDATADIR "/x");
501 ximage.append(image);
502 found = image_exists(image, ximage);
505 if (!found)
507 std::cerr << "Cannot find image!" << std::endl;
508 return 1;
512 // Load the image from the supplied file path
513 pixbuf = gdk_pixbuf_new_from_file(image.c_str(), &gerr);
514 if (gerr)
516 std::cerr << "Cannot load image \"" << image << "\": " << gerr->message << std::endl;
517 g_error_free(gerr);
518 return 1;
522 // Create the window
523 window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
525 // Top-level windows don't normally respond to mouse button events directly.
526 // We want this one to, though.
527 gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK);
529 // Set up event callbacks
530 g_signal_connect(G_OBJECT(window), "expose-event", G_CALLBACK(handle_expose), NULL);
531 g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
532 g_signal_connect(G_OBJECT(window), "button-press-event", G_CALLBACK(window_clicked), NULL);
533 g_signal_connect(G_OBJECT(window), "screen-changed", G_CALLBACK(handle_screen), NULL);
534 g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(handle_key_press), NULL);
537 // Grap pixmap and alpha-thresholded bitmap from original pixmap
538 GdkPixmap* pixmap;
539 GdkBitmap* bitmap;
540 gdk_pixbuf_render_pixmap_and_mask(pixbuf, &pixmap, &bitmap, 128);
542 // Set window size to image size
543 gdk_drawable_get_size(pixmap, &width, &height);
544 gtk_widget_set_size_request(window, width, height);
546 // If we have an alpha channel on the image, set the window's input shape,
547 // clearing it completely first
548 if (bitmap)
550 gtk_widget_input_shape_combine_mask(window, NULL, 0, 0);
551 gtk_widget_input_shape_combine_mask(window, bitmap, 0, 0);
554 g_object_unref(G_OBJECT(pixmap));
555 g_object_unref(G_OBJECT(bitmap));
558 // Set up various other properties we're interested in
559 gtk_window_set_title(GTK_WINDOW(window), "cteddy");
560 gtk_window_set_resizable(GTK_WINDOW(window), false);
561 gtk_window_set_decorated(GTK_WINDOW(window), false);
562 gtk_widget_set_app_paintable(window, true);
564 // Trigger initial attempt to switch to an RGBA colourmap
565 handle_screen(window, NULL, NULL);
567 // Have to do this before we can use window->window as a GdkWindow*
568 gtk_widget_realize(window);
570 #ifdef __USE_SM
571 // Set the window's SM client ID
572 if (sm)
573 gdk_set_sm_client_id(opts.smcid);
574 #endif
577 // Load in the heart-shaped cursor image and use it for our window's cursor
578 GdkPixbuf* heart = gdk_pixbuf_new_from_file(PKGDATADIR "/heart.png", &gerr);
579 if (gerr)
581 std::cerr << "Cannot load image: " << gerr->message << std::endl;
582 g_error_free(gerr);
583 return 1;
585 GdkCursor* cursor = gdk_cursor_new_from_pixbuf(gdk_display_get_default(),
586 heart, 12, 12);
587 gdk_window_set_cursor(window->window, cursor);
590 // Clear the window's default background
591 gdk_window_set_back_pixmap(window->window, NULL, false);
593 // Honour float & sticky options
594 if (ontop)
595 gtk_window_set_keep_above(GTK_WINDOW(window), true);
596 if (sticky)
597 gtk_window_stick(GTK_WINDOW(window));
599 gtk_widget_show_all(window);
601 // Fork into background if desired
602 if (background)
604 pid_t p = fork();
605 if (p < 0)
607 std::cerr << "Could not fork: " << strerror(errno) << std::endl;
608 return 1;
610 else if (p == 0)
612 // Macro for performing a particular system call,
613 // trying again if it returns EINTR, displaying an error otherwise
614 #define __IGNORE_EINTR(fun, msg) \
615 while(fun) \
617 if (errno == EINTR) \
618 continue; \
619 std::cerr << msg << strerror(errno) << std::endl; \
620 return 1; \
623 // Close stdin
624 __IGNORE_EINTR(close(0) != 0, "Could not close stdin: ");
625 // Redirect stdout & stderr to /dev/null
626 int nullfd;
627 __IGNORE_EINTR((nullfd = open("/dev/null", O_WRONLY)) < 0, "Cannot open /dev/null: ");
628 __IGNORE_EINTR(dup2(nullfd, 1) < 0, "Cannot redirect stdout to /dev/null: ");
629 __IGNORE_EINTR(dup2(nullfd, 2) < 0, "Cannot redirect stderr to /dev/null: ");
630 // Create new process session
631 if (setsid() < 0)
633 std::cerr << "Cannot create new session: " << strerror(errno) << std::endl;
634 return 1;
637 else
638 return 0;
641 gtk_main();
643 return 0;