]> andersk Git - test.git/commitdiff
Initial version of code that allows users to interactively select from
authorMarkus Gutschke <markus@shellinabox.com>
Tue, 11 Aug 2009 07:21:51 +0000 (07:21 +0000)
committerMarkus Gutschke <markus@shellinabox.com>
Tue, 11 Aug 2009 07:21:51 +0000 (07:21 +0000)
different style sheet options. This code is still incomplete and subject to
change (e.g. the command line syntax might still change). But it is good
enough to demonstrate the concept on simple style sheets (such as selecting
between normal and reverse video).

14 files changed:
Makefile.am
Makefile.in
config.h
configure
configure.ac
demo/vt100.js
shellinabox/root_page.html
shellinabox/service.c
shellinabox/shell_in_a_box.js
shellinabox/shellinaboxd.c
shellinabox/usercss.c [new file with mode: 0644]
shellinabox/usercss.h [new file with mode: 0644]
shellinabox/vt100.js
shellinabox/vt100.jspp

index 10182e7dd42d3122d4681b60835b5149758fe9c4..1c78369a9e8eb884fb6e01b9c761c1f5f4fd8e15 100644 (file)
@@ -76,6 +76,8 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c                             \
                        shellinabox/service.h                                  \
                        shellinabox/session.c                                  \
                        shellinabox/session.h                                  \
+                       shellinabox/usercss.c                                  \
+                       shellinabox/usercss.h                                  \
                        shellinabox/cgi_root.html                              \
                        shellinabox/root_page.html                             \
                        shellinabox/vt100.jspp                                 \
index 856ae63a0132be2a781e7198b109b2b1a75f4ff3..e61f461241b5e4d3bad407bde4755760c3d0d190 100644 (file)
@@ -73,7 +73,7 @@ PROGRAMS = $(bin_PROGRAMS)
 am__dirstamp = $(am__leading_dot)dirstamp
 am_shellinaboxd_OBJECTS = shellinaboxd.$(OBJEXT) \
        externalfile.$(OBJEXT) launcher.$(OBJEXT) privileges.$(OBJEXT) \
-       service.$(OBJEXT) session.$(OBJEXT) \
+       service.$(OBJEXT) session.$(OBJEXT) usercss.$(OBJEXT) \
        shellinabox/cgi_root.$(OBJEXT) shellinabox/root_page.$(OBJEXT) \
        shellinabox/vt100.$(OBJEXT) \
        shellinabox/shell_in_a_box.$(OBJEXT) \
@@ -320,6 +320,8 @@ shellinaboxd_SOURCES = shellinabox/shellinaboxd.c                             \
                        shellinabox/service.h                                  \
                        shellinabox/session.c                                  \
                        shellinabox/session.h                                  \
+                       shellinabox/usercss.c                                  \
+                       shellinabox/usercss.h                                  \
                        shellinabox/cgi_root.html                              \
                        shellinabox/root_page.html                             \
                        shellinabox/vt100.jspp                                 \
@@ -503,6 +505,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/ssl.Plo@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/trie.Plo@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/url.Plo@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/usercss.Po@am__quote@
 
 .c.o:
 @am__fastdepCC_TRUE@   $(COMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $<
@@ -658,6 +661,20 @@ session.obj: shellinabox/session.c
 @AMDEP_TRUE@@am__fastdepCC_FALSE@      DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
 @am__fastdepCC_FALSE@  $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o session.obj `if test -f 'shellinabox/session.c'; then $(CYGPATH_W) 'shellinabox/session.c'; else $(CYGPATH_W) '$(srcdir)/shellinabox/session.c'; fi`
 
+usercss.o: shellinabox/usercss.c
+@am__fastdepCC_TRUE@   $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT usercss.o -MD -MP -MF $(DEPDIR)/usercss.Tpo -c -o usercss.o `test -f 'shellinabox/usercss.c' || echo '$(srcdir)/'`shellinabox/usercss.c
+@am__fastdepCC_TRUE@   mv -f $(DEPDIR)/usercss.Tpo $(DEPDIR)/usercss.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      source='shellinabox/usercss.c' object='usercss.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@  $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o usercss.o `test -f 'shellinabox/usercss.c' || echo '$(srcdir)/'`shellinabox/usercss.c
+
+usercss.obj: shellinabox/usercss.c
+@am__fastdepCC_TRUE@   $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT usercss.obj -MD -MP -MF $(DEPDIR)/usercss.Tpo -c -o usercss.obj `if test -f 'shellinabox/usercss.c'; then $(CYGPATH_W) 'shellinabox/usercss.c'; else $(CYGPATH_W) '$(srcdir)/shellinabox/usercss.c'; fi`
+@am__fastdepCC_TRUE@   mv -f $(DEPDIR)/usercss.Tpo $(DEPDIR)/usercss.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      source='shellinabox/usercss.c' object='usercss.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@      DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@  $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o usercss.obj `if test -f 'shellinabox/usercss.c'; then $(CYGPATH_W) 'shellinabox/usercss.c'; else $(CYGPATH_W) '$(srcdir)/shellinabox/usercss.c'; fi`
+
 mostlyclean-libtool:
        -rm -f *.lo
 
index 8f5d734d946f3bf310b8877673daef91c85d63da..d2118c942ad6b62501649fd933e5f8281cb1b939 100644 (file)
--- a/config.h
+++ b/config.h
 #define STDC_HEADERS 1
 
 /* Most recent revision number in the version control system */
-#define VCS_REVISION "164"
+#define VCS_REVISION "165"
 
 /* Version number of package */
 #define VERSION "2.9"
index 399f436e43088be450e99e54345c7d26a7f4a156..37e5ae403d9bb552acfbe27e8ff8547040a17028 100755 (executable)
--- a/configure
+++ b/configure
@@ -2317,7 +2317,7 @@ ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $
 ac_compiler_gnu=$ac_cv_c_compiler_gnu
 
 
-VCS_REVISION=164
+VCS_REVISION=165
 
 
 cat >>confdefs.h <<_ACEOF
index 3cdd440a04a8fc599f4c6afca735708f000cba1e..b214501232c79d64541eca07a820cde158dbff14 100644 (file)
@@ -2,7 +2,7 @@ AC_PREREQ(2.57)
 
 dnl This is the one location where the authoritative version number is stored
 AC_INIT(shellinabox, 2.9, markus@shellinabox.com)
-VCS_REVISION=164
+VCS_REVISION=165
 AC_SUBST(VCS_REVISION)
 AC_DEFINE_UNQUOTED(VCS_REVISION, "${VCS_REVISION}",
                    [Most recent revision number in the version control system])
index 23dd26f87c432d21ad0e3f62d5bec445a751317a..e66f1638e219f81833be5c08024176b04ad5ffca 100644 (file)
@@ -259,6 +259,108 @@ VT100.prototype.addListener = function(elem, event, listener) {
   }
 };
 
+VT100.prototype.initializeUserCSSStyles = function() {
+  this.usercssActions                    = [];
+  if (typeof userCSSList != 'undefined') {
+    var menu                             = '';
+    var group                            = '';
+    var wasSingleSel                     = 1;
+    var beginOfGroup                     = 0;
+    for (var i = 0; i <= userCSSList.length; ++i) {
+      if (i < userCSSList.length) {
+        var label                        = userCSSList[i][0];
+        var newGroup                     = userCSSList[i][1];
+        var enabled                      = userCSSList[i][2];
+      
+        // Add user style sheet to document
+        var style                        = document.createElement('link');
+        var id                           = document.createAttribute('id');
+        id.nodeValue                     = 'usercss-' + i;
+        style.setAttributeNode(id);
+        var rel                          = document.createAttribute('rel');
+        rel.nodeValue                    = 'stylesheet';
+        style.setAttributeNode(rel);
+        var href                         = document.createAttribute('href');
+        href.nodeValue                   = 'usercss-' + i + '.css';
+        style.setAttributeNode(href);
+        var type                         = document.createAttribute('type');
+        type.nodeValue                   = 'text/css';
+        style.setAttributeNode(type);
+        document.getElementsByTagName('head')[0].appendChild(style);
+        style.disabled                   = !enabled;
+      }
+    
+      // Add entry to menu
+      if (newGroup || i == userCSSList.length) {
+        if (beginOfGroup != 0 && (i - beginOfGroup > 1 || !wasSingleSel)) {
+          // The last group had multiple entries that are mutually exclusive;
+          // or the previous to last group did. In either case, we need to
+          // append a "<hr />" before we can add the last group to the menu.
+          menu                          += '<hr />';
+        }
+        wasSingleSel                     = i - beginOfGroup < 1;
+        menu                            += group;
+        group                            = '';
+
+        for (var j = beginOfGroup; j < i; ++j) {
+          this.usercssActions[this.usercssActions.length] =
+            function(vt100, current, begin, count) {
+
+              // Deselect all other entries in the group, then either select
+              // (for multiple entries in group) or toggle (for on/off entry)
+              // the current entry.
+              return function() {
+                var entry                = vt100.getChildById(vt100.menu,
+                                                              'beginusercss');
+                var i                    = -1;
+                var j                    = -1;
+                for (var c = count; c > 0; ++j) {
+                  if (entry.tagName == 'LI') {
+                    if (++i >= begin) {
+                      --c;
+                      var label          = vt100.usercss.childNodes[j];
+                      label.innerHTML    =
+                                       label.innerHTML.replace(/^\u2714 /, '');
+                      var sheet          = document.getElementById(
+                                                               'usercss-' + i);
+                      if (i == current) {
+                        if (count == 1) {
+                          sheet.disabled = !sheet.disabled;
+                        } else {
+                          sheet.disabled = false;
+                        }
+                        if (!sheet.disabled) {
+                          label.innerHTML= '&#10004; ' + label.innerHTML;
+                        }
+                      } else {
+                        sheet.disabled   = true;
+                      }
+                    }
+                  }
+                  entry                  = entry.nextSibling;
+                }
+              };
+            }(this, j, beginOfGroup, i - beginOfGroup);
+        }
+
+        if (i == userCSSList.length) {
+          break;
+        }
+
+        beginOfGroup                     = i;
+      }
+      // Collect all entries in a group, before attaching them to the menu.
+      // This is necessary as we don't know whether this is a group of
+      // mutually exclusive options (which should be separated by "<hr />" on
+      // both ends), or whether this is a on/off toggle, which can be grouped
+      // together with other on/off options.
+      group                             +=
+        '<li>' + (enabled ? '&#10004; ' : '') + label + '</li>';
+    }
+    this.usercss.innerHTML               = menu;
+  }
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -279,6 +381,7 @@ VT100.prototype.initializeElements = function(container) {
       !this.getChildById(this.container, 'padding')     ||
       !this.getChildById(this.container, 'cursor')      ||
       !this.getChildById(this.container, 'lineheight')  ||
+      !this.getChildById(this.container, 'usercss')     ||
       !this.getChildById(this.container, 'space')       ||
       !this.getChildById(this.container, 'input')       ||
       !this.getChildById(this.container, 'cliphelper')  ||
@@ -325,6 +428,7 @@ VT100.prototype.initializeElements = function(container) {
                          '<pre id="cursor">&nbsp;</pre>' +
                        '</div>' +
                        '<div class="hidden">' +
+                         '<div id="usercss"></div>' +
                          '<pre><div><span id="space"></span></div></pre>' +
                          '<input type="textfield" id="input" />' +
                          '<input type="textfield" id="cliphelper" />' +
@@ -365,12 +469,16 @@ VT100.prototype.initializeElements = function(container) {
   var ieProbe                  = this.getChildById(this.container, 'ieprobe');
   this.padding                 = this.getChildById(this.container, 'padding');
   this.cursor                  = this.getChildById(this.container, 'cursor');
+  this.usercss                 = this.getChildById(this.container, 'usercss');
   this.space                   = this.getChildById(this.container, 'space');
   this.input                   = this.getChildById(this.container, 'input');
   this.cliphelper              = this.getChildById(this.container,
                                                                  'cliphelper');
   this.attributeHelper         = this.getChildById(this.container, 'attrib');
 
+  // Add any user selectable style sheets to the menu
+  this.initializeUserCSSStyles();
+
   // Remember the dimensions of a standard character glyph. We would
   // expect that we could just check cursor.clientWidth/Height at any time,
   // but it turns out that browsers sometimes invalidate these values
@@ -1693,7 +1801,7 @@ VT100.prototype.toggleBell = function() {
 };
 
 VT100.prototype.about = function() {
-  alert("VT100 Terminal Emulator " + "2.9 (revision 164)" +
+  alert("VT100 Terminal Emulator " + "2.9 (revision 165)" +
         "\nCopyright 2008-2009 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com");
 };
@@ -1724,7 +1832,11 @@ VT100.prototype.showContextMenu = function(x, y) {
              (this.utfEnabled ? '&#10004; ' : '') + 'Unicode</li>' +
           '<li id="endconfig">' +
              (this.visualBell ? '&#10004; ' : '') + 'Visual Bell</li>'+
-          '<hr />' +
+          (this.usercss.firstChild ?
+           '<hr id="beginusercss" />' +
+           this.usercss.innerHTML +
+           '<hr id="endusercss" />' :
+           '<hr />') +
           '<li id="about">About...</li>' +
         '</ul>' +
       '</td></tr>' +
@@ -1744,9 +1856,16 @@ VT100.prototype.showContextMenu = function(x, y) {
     menuentries.childNodes[1].className
                               = 'disabled';
   }
+
+  // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
-                                  this.toggleUTF, this.toggleBell,
-                                  this.about ];
+                                  this.toggleUTF, this.toggleBell ];
+
+  // Actions for user CSS styles (if any)
+  for (var i = 0; i < this.usercssActions.length; ++i) {
+    actions[actions.length]   = this.usercssActions[i];
+  }
+  actions[actions.length]     = this.about;
 
   // Allow subclasses to dynamically add entries to the context menu
   this.extendContextMenu(menuentries, actions);
index 753cdf9980fe7f3b75155b7818c9cee011079113..3b15b6551bd6d0fdd9a13680de82f2bd1b684fd8 100644 (file)
     --></script>
     <link rel="stylesheet" href="styles.css" type="text/css">
     <script type="text/javascript"><!--
-
-      // We would like to hide overflowing lines as this can lead to
-      // visually jarring results if the browser substitutes oversized
-      // Unicode characters from different fonts. Unfortunately, a bug
-      // in Firefox prevents it from allowing multi-line text
-      // selections whenever we change the "overflow" style. So, only
-      // do so for non-Netscape browsers.
-      if (typeof navigator.appName == 'undefined' ||
-          navigator.appName != 'Netscape') {
-        document.write('<style type="text/css">' +
-                       '#vt100 #console div, #vt100 #alt_console div {' +
-                       '  overflow: hidden;' +
-                       '}' +
-                       '</style>');
-      }
+      (function() {
+        // We would like to hide overflowing lines as this can lead to
+        // visually jarring results if the browser substitutes oversized
+        // Unicode characters from different fonts. Unfortunately, a bug
+        // in Firefox prevents it from allowing multi-line text
+        // selections whenever we change the "overflow" style. So, only
+        // do so for non-Netscape browsers.
+        if (typeof navigator.appName == 'undefined' ||
+            navigator.appName != 'Netscape') {
+          document.write('<style type="text/css">' +
+                         '#vt100 #console div, #vt100 #alt_console div {' +
+                         '  overflow: hidden;' +
+                         '}' +
+                         '</style>');
+        }
+      })();
     --></script>
     <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
     <script type="text/javascript" src="ShellInABox.js"></script>
index 28f64471c5d0e7f5356dd6fc4c6367e8e282aea7..96ba7f48f4de3df171d435b45f8e1e72e820cbf4 100644 (file)
 #include <sys/types.h>
 #include <unistd.h>
 
-#include "shellinabox/service.h"
+#include "logging/logging.h"
 #include "shellinabox/launcher.h"
 #include "shellinabox/privileges.h"
-#include "logging/logging.h"
+#include "shellinabox/service.h"
 
 
 struct Service **services;
index 4465e1adb6e24fb1a9f36393f33499c039c0d9c4..90bf13a910ff114e7a48fcc2a717c0b550111253 100644 (file)
@@ -355,7 +355,7 @@ ShellInABox.prototype.extendContextMenu = function(entries, actions) {
 };
 
 ShellInABox.prototype.about = function() {
-  alert("Shell In A Box version " + "2.9 (revision 164)" +
+  alert("Shell In A Box version " + "2.9 (revision 165)" +
         "\nCopyright 2008-2009 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com" +
         (typeof serverSupportsSSL != 'undefined' && serverSupportsSSL ?
index e5628bb8fb68d19d111cb7e33dd45a5e36364fe0..e077b28c70c320d2fd356deaaa71c6589a055d05 100644 (file)
 #include "shellinabox/privileges.h"
 #include "shellinabox/service.h"
 #include "shellinabox/session.h"
+#include "shellinabox/usercss.h"
 
 #define PORTNUM           4200
 #define MAX_RESPONSE      2048
 
-static int     port;
-static int     portMin;
-static int     portMax;
-static int     localhostOnly = 0;
-static int     noBeep        = 0;
-static int     numericHosts  = 0;
-static int     enableSSL     = 1;
-static int     enableSSLMenu = 1;
-static int     linkifyURLs   = 1;
-static char    *certificateDir;
-static int     certificateFd = -1;
-static HashMap *externalFiles;
-static Server  *cgiServer;
-static char    *cgiSessionKey;
-static int     cgiSessions;
-static char    *cssStyleSheet;
+static int            port;
+static int            portMin;
+static int            portMax;
+static int            localhostOnly = 0;
+static int            noBeep        = 0;
+static int            numericHosts  = 0;
+static int            enableSSL     = 1;
+static int            enableSSLMenu = 1;
+static int            linkifyURLs   = 1;
+static char           *certificateDir;
+static int            certificateFd = -1;
+static HashMap        *externalFiles;
+static Server         *cgiServer;
+static char           *cgiSessionKey;
+static int            cgiSessions;
+static char           *cssStyleSheet;
+static struct UserCSS *userCSSList;
 
 static char *jsonEscape(const char *buf, int len) {
   static const char *hexDigit = "0123456789ABCDEF";
@@ -517,15 +519,18 @@ static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
     extern char vt100End[];
     extern char shellInABoxStart[];
     extern char shellInABoxEnd[];
+    char *userCSSString   = getUserCSSString(userCSSList);
     char *stateVars       = stringPrintf(NULL,
                                          "serverSupportsSSL = %s;\n"
                                          "disableSSLMenu    = %s;\n"
                                          "suppressAllAudio  = %s;\n"
-                                         "linkifyURLs       = %d;\n\n",
+                                         "linkifyURLs       = %d;\n"
+                                         "userCSSList       = %s;\n\n",
                                          enableSSL      ? "true" : "false",
                                          !enableSSLMenu ? "true" : "false",
                                          noBeep         ? "true" : "false",
-                                         linkifyURLs);
+                                         linkifyURLs, userCSSString);
+    free(userCSSString);
     int stateVarsLength   = strlen(stateVars);
     int contentLength     = stateVarsLength +
                             (addr(vt100End) - addr(vt100Start)) +
@@ -552,6 +557,18 @@ static int shellInABoxHttpHandler(HttpConnection *http, void *arg,
     // Serve the style sheet.
     serveStaticFile(http, "text/css; charset=utf-8",
                     cssStyleSheet, strrchr(cssStyleSheet, '\000'));
+  } else if (pathInfoLength > 8 && !memcmp(pathInfo, "usercss-", 8)) {
+    // Server user style sheets (if any)
+    struct UserCSS *css   = userCSSList;
+    for (int idx          = atoi(pathInfo + 8);
+         idx-- > 0 && css; css = css->next ) {
+    }
+    if (css) {
+      serveStaticFile(http, "text/css; charset=utf-8",
+                      css->style, css->style + css->styleLen);
+    } else {
+      httpSendReply(http, 404, "File not found", NO_MSG);
+    }
   } else {
     httpSendReply(http, 404, "File not found", NO_MSG);
   }
@@ -605,13 +622,14 @@ static void usage(void) {
           "%s"
           "  -q, --quiet                 turn off all messages\n"
           "  -u, --user=UID              switch to this user (default: %s)\n"
+          "      --user-css=STYLES       defines user-selectable CSS options\n"
           "  -v, --verbose               enable logging messages\n"
           "      --version               prints version information\n"
           "\n"
           "Debug, quiet, and verbose are mutually exclusive.\n"
           "\n"
           "One or more --service arguments define services that should "
-          "be made available \n"
+          "be made available\n"
           "through the web interface:\n"
           "  SERVICE := <url-path> ':' APP\n"
           "  APP     := 'LOGIN' | 'SSH' [ : <host> ] | "
@@ -627,7 +645,18 @@ static void usage(void) {
           "  ${lines}   - number of rows\n"
           "  ${peer}    - name of remote peer\n"
           "  ${uid}     - user id\n"
-          "  ${user}    - user name",
+          "  ${user}    - user name\n"
+          "\n"
+          "One or more --user-css arguments define optional user-selectable "
+          "CSS options.\n"
+          "These options show up in the right-click context menu:\n"
+          "  STYLES  := GROUP { ';' GROUP }*\n"
+          "  GROUP   := OPTION { ',' OPTION }*\n"
+          "  OPTION  := <label> ':' [ '-' | '+' ] <css-file>\n"
+          "\n"
+          "OPTIONs that make up a GROUP are mutually exclusive. But "
+          "individual GROUPs are\n"
+          "independent of each other.\n",
           !serverSupportsSSL() ? "" :
           "  -c, --cert=CERTDIR          set certificate dir "
           "(default: $PWD)\n"
@@ -685,6 +714,7 @@ static void parseArgs(int argc, char * const argv[]) {
       { "disable-ssl-menu", 0, 0,  0  },
       { "quiet",            0, 0, 'q' },
       { "user",             1, 0, 'u' },
+      { "user-css",         1, 0,  0  },
       { "verbose",          0, 0, 'v' },
       { "version",          0, 0,  0  },
       { 0,                  0, 0,  0  } };
@@ -703,7 +733,7 @@ static void parseArgs(int argc, char * const argv[]) {
     if (idx-- <= 0) {
       // Help (or invalid argument)
       usage();
-      if (idx == -1) {
+      if (idx < -1) {
         fatal("Failed to parse command line");
       }
       exit(0);
@@ -894,6 +924,12 @@ static void parseArgs(int argc, char * const argv[]) {
         fatal("\"--user\" expects a user name.");
       }
       runAsUser            = parseUser(optarg, NULL);
+    } else if (!idx--) {
+      // User CSS
+      if (!optarg || !*optarg) {
+        fatal("\"--user-css\" expects a list of styles sheets and labels");
+      }
+      parseUserCSS(&userCSSList, optarg);
     } else if (!idx--) {
       // Verbose
       if (!logIsDefault() && (!logIsInfo() || logIsDebug())) {
diff --git a/shellinabox/usercss.c b/shellinabox/usercss.c
new file mode 100644 (file)
index 0000000..5ab733a
--- /dev/null
@@ -0,0 +1,210 @@
+// usercss.c -- Defines user-selectable CSS options
+// Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// In addition to these license terms, the author grants the following
+// additional rights:
+//
+// If you modify this program, or any covered work, by linking or
+// combining it with the OpenSSL project's OpenSSL library (or a
+// modified version of that library), containing parts covered by the
+// terms of the OpenSSL or SSLeay licenses, the author
+// grants you additional permission to convey the resulting work.
+// Corresponding Source for a non-source form of such a combination
+// shall include the source code for the parts of OpenSSL used as well
+// as that of the covered work.
+//
+// You may at your option choose to remove this additional permission from
+// the work, or from any part of it.
+//
+// It is possible to build this program in a way that it loads OpenSSL
+// libraries at run-time. If doing so, the following notices are required
+// by the OpenSSL and SSLeay licenses:
+//
+// This product includes software developed by the OpenSSL Project
+// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
+//
+// This product includes cryptographic software written by Eric Young
+// (eay@cryptsoft.com)
+//
+//
+// The most up-to-date version of this program is always available from
+// http://shellinabox.com
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "logging/logging.h"
+#include "shellinabox/usercss.h"
+
+
+static void readStylesheet(const char *filename, char **style, size_t *len) {
+  int fd               = open(filename, O_RDONLY);
+  struct stat st;
+  if (fd < 0 || fstat(fd, &st)) {
+    fatal("Cannot access style sheet \"%s\"", filename);
+  }
+  FILE *fp;
+  check(fp             = fdopen(fd, "r"));
+  check(*style         = malloc(st.st_size + 1));
+  check(fread(*style, 1, st.st_size, fp) == st.st_size);
+  (*style)[st.st_size] = '\000';
+  *len                 = st.st_size;
+  fclose(fp);
+}
+
+void initUserCSS(struct UserCSS *userCSS, const char *arg) {
+  userCSS->newGroup                       = 1;
+
+  int numMembers                          = 1;
+  int hasActiveMember                     = 0;
+  for (;;) {
+    const char *colon                     = strchr(arg, ':');
+    if (!colon) {
+      fatal("Incomplete user CSS definition: \"%s\"", arg);
+    }
+
+    check(userCSS->label                  = malloc(6*(colon - arg) + 1));
+    for (const char *src = arg, *dst = userCSS->label;;) {
+      if (src == colon) {
+        *(char *)dst                      = '\000';
+        break;
+      }
+      char ch                             = *src++;
+      if (ch == '<') {
+        memcpy((char *)dst, "&lt;", 4);
+        dst                              += 4;
+      } else if (ch == '&') {
+        memcpy((char *)dst, "&amp;", 5);
+        dst                              += 5;
+      } else if (ch == '\'') {
+        memcpy((char *)dst, "&apos;", 6);
+        dst                              += 6;
+      } else if (ch == '"') {
+        memcpy((char *)dst, "&quot;", 6);
+        dst                              += 6;
+      } else {
+        *(char *)dst++                    = ch;
+      }
+    }
+
+    int filenameLen                       = strcspn(colon + 1, ",;");
+    char *filename;
+    check(filename                        = malloc(filenameLen + 1));
+    memcpy(filename, colon + 1, filenameLen);
+    filename[filenameLen]                 = '\000';
+
+    switch (*filename) {
+      case '-':
+        userCSS->isActivated              = 0;
+        break;
+      case '+':
+        if (hasActiveMember) {
+          fatal("There can only be one active style option per group. Maybe "
+                "use ';' instead of ',' to start a new group.");
+        }
+        hasActiveMember                   = 1;
+        userCSS->isActivated              = 1;
+        break;
+      default:
+        fatal("Must indicate with '+' or '-' whether the style option is "
+              "active by default");
+    }
+
+    readStylesheet(filename + 1, (char **)&userCSS->style, &userCSS->styleLen);
+    free(filename);
+
+    arg                                   = colon + 1 + filenameLen;
+    if (!*arg) {
+      userCSS->next                       = NULL;
+      break;
+    }
+    check(userCSS->next                   = malloc(sizeof(struct UserCSS)));
+    userCSS                               = userCSS->next;
+    userCSS->newGroup                     = *arg++ == ';';
+    if (userCSS->newGroup) {
+      if (!hasActiveMember && numMembers > 1) {
+        // Print error message
+        break;
+      }
+      numMembers                          = 1;
+      hasActiveMember                     = 0;
+    } else {
+      ++numMembers;
+    }
+  }
+  if (!hasActiveMember && numMembers > 1) {
+    fatal("Each group of style options must have exactly one style that is "
+          "active by\n"
+          "default.");
+  }
+}
+
+struct UserCSS *newUserCSS(const char *arg) {
+  struct UserCSS *userCSS;
+  check(userCSS = malloc(sizeof(struct UserCSS)));
+  initUserCSS(userCSS, arg);
+  return userCSS;
+}
+
+void parseUserCSS(struct UserCSS **userCSSList, const char *arg) {
+  while (*userCSSList) {
+    userCSSList = &(*userCSSList)->next;
+  }
+  *userCSSList  = newUserCSS(arg);
+}
+
+void destroyUserCSS(struct UserCSS *userCSS) {
+  if (userCSS) {
+    free((void *)userCSS->label);
+    userCSS->label         = NULL;
+    free((void *)userCSS->style);
+    userCSS->style         = NULL;
+    userCSS->styleLen      = -1;
+    for (struct UserCSS *child = userCSS->next; child; ) {
+      struct UserCSS *next = child->next;
+      free((void *)child->label);
+      free((void *)child->style);
+      free(child);
+      child                = next;
+    }
+    userCSS->next          = NULL;
+  }
+}
+
+void deleteUserCSS(struct UserCSS *userCSS) {
+  destroyUserCSS(userCSS);
+  free(userCSS);
+}
+
+char *getUserCSSString(struct UserCSS *userCSS) {
+  char *s   = stringPrintf(NULL, "[ ");
+  while (userCSS) {
+    s       = stringPrintf(s, "[ '%s', %s, %s ]%s",
+                           userCSS->label,
+                           userCSS->newGroup    ? "true" : "false",
+                           userCSS->isActivated ? "true" : "false",
+                           userCSS->next        ? ", "   : "");
+    userCSS = userCSS->next;
+  }
+  return stringPrintf(s, " ]");
+}
diff --git a/shellinabox/usercss.h b/shellinabox/usercss.h
new file mode 100644 (file)
index 0000000..5747530
--- /dev/null
@@ -0,0 +1,65 @@
+// usercss.h -- Defines user-selectable CSS options
+// Copyright (C) 2008-2009 Markus Gutschke <markus@shellinabox.com>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// In addition to these license terms, the author grants the following
+// additional rights:
+//
+// If you modify this program, or any covered work, by linking or
+// combining it with the OpenSSL project's OpenSSL library (or a
+// modified version of that library), containing parts covered by the
+// terms of the OpenSSL or SSLeay licenses, the author
+// grants you additional permission to convey the resulting work.
+// Corresponding Source for a non-source form of such a combination
+// shall include the source code for the parts of OpenSSL used as well
+// as that of the covered work.
+//
+// You may at your option choose to remove this additional permission from
+// the work, or from any part of it.
+//
+// It is possible to build this program in a way that it loads OpenSSL
+// libraries at run-time. If doing so, the following notices are required
+// by the OpenSSL and SSLeay licenses:
+//
+// This product includes software developed by the OpenSSL Project
+// for use in the OpenSSL Toolkit. (http://www.openssl.org/)
+//
+// This product includes cryptographic software written by Eric Young
+// (eay@cryptsoft.com)
+//
+//
+// The most up-to-date version of this program is always available from
+// http://shellinabox.com
+
+#ifndef USERCSS_H__
+#define USERCSS_H__
+
+struct UserCSS {
+  struct UserCSS *next;
+  int            newGroup;
+  int            isActivated;
+  const char     *label;
+  const char     *style;
+  size_t         styleLen;
+};
+
+void initUserCSS(struct UserCSS *userCSS, const char *arg);
+struct UserCSS *newUserCSS(const char *arg);
+void parseUserCSS(struct UserCSS **userCSSList, const char *arg);
+void destroyUserCSS(struct UserCSS *userCSS);
+void deleteUserCSS(struct UserCSS *userCSS);
+char *getUserCSSString(struct UserCSS *userCSS);
+
+#endif
index 23dd26f87c432d21ad0e3f62d5bec445a751317a..e66f1638e219f81833be5c08024176b04ad5ffca 100644 (file)
@@ -259,6 +259,108 @@ VT100.prototype.addListener = function(elem, event, listener) {
   }
 };
 
+VT100.prototype.initializeUserCSSStyles = function() {
+  this.usercssActions                    = [];
+  if (typeof userCSSList != 'undefined') {
+    var menu                             = '';
+    var group                            = '';
+    var wasSingleSel                     = 1;
+    var beginOfGroup                     = 0;
+    for (var i = 0; i <= userCSSList.length; ++i) {
+      if (i < userCSSList.length) {
+        var label                        = userCSSList[i][0];
+        var newGroup                     = userCSSList[i][1];
+        var enabled                      = userCSSList[i][2];
+      
+        // Add user style sheet to document
+        var style                        = document.createElement('link');
+        var id                           = document.createAttribute('id');
+        id.nodeValue                     = 'usercss-' + i;
+        style.setAttributeNode(id);
+        var rel                          = document.createAttribute('rel');
+        rel.nodeValue                    = 'stylesheet';
+        style.setAttributeNode(rel);
+        var href                         = document.createAttribute('href');
+        href.nodeValue                   = 'usercss-' + i + '.css';
+        style.setAttributeNode(href);
+        var type                         = document.createAttribute('type');
+        type.nodeValue                   = 'text/css';
+        style.setAttributeNode(type);
+        document.getElementsByTagName('head')[0].appendChild(style);
+        style.disabled                   = !enabled;
+      }
+    
+      // Add entry to menu
+      if (newGroup || i == userCSSList.length) {
+        if (beginOfGroup != 0 && (i - beginOfGroup > 1 || !wasSingleSel)) {
+          // The last group had multiple entries that are mutually exclusive;
+          // or the previous to last group did. In either case, we need to
+          // append a "<hr />" before we can add the last group to the menu.
+          menu                          += '<hr />';
+        }
+        wasSingleSel                     = i - beginOfGroup < 1;
+        menu                            += group;
+        group                            = '';
+
+        for (var j = beginOfGroup; j < i; ++j) {
+          this.usercssActions[this.usercssActions.length] =
+            function(vt100, current, begin, count) {
+
+              // Deselect all other entries in the group, then either select
+              // (for multiple entries in group) or toggle (for on/off entry)
+              // the current entry.
+              return function() {
+                var entry                = vt100.getChildById(vt100.menu,
+                                                              'beginusercss');
+                var i                    = -1;
+                var j                    = -1;
+                for (var c = count; c > 0; ++j) {
+                  if (entry.tagName == 'LI') {
+                    if (++i >= begin) {
+                      --c;
+                      var label          = vt100.usercss.childNodes[j];
+                      label.innerHTML    =
+                                       label.innerHTML.replace(/^\u2714 /, '');
+                      var sheet          = document.getElementById(
+                                                               'usercss-' + i);
+                      if (i == current) {
+                        if (count == 1) {
+                          sheet.disabled = !sheet.disabled;
+                        } else {
+                          sheet.disabled = false;
+                        }
+                        if (!sheet.disabled) {
+                          label.innerHTML= '&#10004; ' + label.innerHTML;
+                        }
+                      } else {
+                        sheet.disabled   = true;
+                      }
+                    }
+                  }
+                  entry                  = entry.nextSibling;
+                }
+              };
+            }(this, j, beginOfGroup, i - beginOfGroup);
+        }
+
+        if (i == userCSSList.length) {
+          break;
+        }
+
+        beginOfGroup                     = i;
+      }
+      // Collect all entries in a group, before attaching them to the menu.
+      // This is necessary as we don't know whether this is a group of
+      // mutually exclusive options (which should be separated by "<hr />" on
+      // both ends), or whether this is a on/off toggle, which can be grouped
+      // together with other on/off options.
+      group                             +=
+        '<li>' + (enabled ? '&#10004; ' : '') + label + '</li>';
+    }
+    this.usercss.innerHTML               = menu;
+  }
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -279,6 +381,7 @@ VT100.prototype.initializeElements = function(container) {
       !this.getChildById(this.container, 'padding')     ||
       !this.getChildById(this.container, 'cursor')      ||
       !this.getChildById(this.container, 'lineheight')  ||
+      !this.getChildById(this.container, 'usercss')     ||
       !this.getChildById(this.container, 'space')       ||
       !this.getChildById(this.container, 'input')       ||
       !this.getChildById(this.container, 'cliphelper')  ||
@@ -325,6 +428,7 @@ VT100.prototype.initializeElements = function(container) {
                          '<pre id="cursor">&nbsp;</pre>' +
                        '</div>' +
                        '<div class="hidden">' +
+                         '<div id="usercss"></div>' +
                          '<pre><div><span id="space"></span></div></pre>' +
                          '<input type="textfield" id="input" />' +
                          '<input type="textfield" id="cliphelper" />' +
@@ -365,12 +469,16 @@ VT100.prototype.initializeElements = function(container) {
   var ieProbe                  = this.getChildById(this.container, 'ieprobe');
   this.padding                 = this.getChildById(this.container, 'padding');
   this.cursor                  = this.getChildById(this.container, 'cursor');
+  this.usercss                 = this.getChildById(this.container, 'usercss');
   this.space                   = this.getChildById(this.container, 'space');
   this.input                   = this.getChildById(this.container, 'input');
   this.cliphelper              = this.getChildById(this.container,
                                                                  'cliphelper');
   this.attributeHelper         = this.getChildById(this.container, 'attrib');
 
+  // Add any user selectable style sheets to the menu
+  this.initializeUserCSSStyles();
+
   // Remember the dimensions of a standard character glyph. We would
   // expect that we could just check cursor.clientWidth/Height at any time,
   // but it turns out that browsers sometimes invalidate these values
@@ -1693,7 +1801,7 @@ VT100.prototype.toggleBell = function() {
 };
 
 VT100.prototype.about = function() {
-  alert("VT100 Terminal Emulator " + "2.9 (revision 164)" +
+  alert("VT100 Terminal Emulator " + "2.9 (revision 165)" +
         "\nCopyright 2008-2009 by Markus Gutschke\n" +
         "For more information check http://shellinabox.com");
 };
@@ -1724,7 +1832,11 @@ VT100.prototype.showContextMenu = function(x, y) {
              (this.utfEnabled ? '&#10004; ' : '') + 'Unicode</li>' +
           '<li id="endconfig">' +
              (this.visualBell ? '&#10004; ' : '') + 'Visual Bell</li>'+
-          '<hr />' +
+          (this.usercss.firstChild ?
+           '<hr id="beginusercss" />' +
+           this.usercss.innerHTML +
+           '<hr id="endusercss" />' :
+           '<hr />') +
           '<li id="about">About...</li>' +
         '</ul>' +
       '</td></tr>' +
@@ -1744,9 +1856,16 @@ VT100.prototype.showContextMenu = function(x, y) {
     menuentries.childNodes[1].className
                               = 'disabled';
   }
+
+  // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
-                                  this.toggleUTF, this.toggleBell,
-                                  this.about ];
+                                  this.toggleUTF, this.toggleBell ];
+
+  // Actions for user CSS styles (if any)
+  for (var i = 0; i < this.usercssActions.length; ++i) {
+    actions[actions.length]   = this.usercssActions[i];
+  }
+  actions[actions.length]     = this.about;
 
   // Allow subclasses to dynamically add entries to the context menu
   this.extendContextMenu(menuentries, actions);
index 49f0b94c027187a0e21c2a6968d3a1f5adf12c93..36d682810d182db4aea8ec526dba59370135daf2 100644 (file)
@@ -259,6 +259,108 @@ VT100.prototype.addListener = function(elem, event, listener) {
   }
 };
 
+VT100.prototype.initializeUserCSSStyles = function() {
+  this.usercssActions                    = [];
+  if (typeof userCSSList != 'undefined') {
+    var menu                             = '';
+    var group                            = '';
+    var wasSingleSel                     = 1;
+    var beginOfGroup                     = 0;
+    for (var i = 0; i <= userCSSList.length; ++i) {
+      if (i < userCSSList.length) {
+        var label                        = userCSSList[i][0];
+        var newGroup                     = userCSSList[i][1];
+        var enabled                      = userCSSList[i][2];
+      
+        // Add user style sheet to document
+        var style                        = document.createElement('link');
+        var id                           = document.createAttribute('id');
+        id.nodeValue                     = 'usercss-' + i;
+        style.setAttributeNode(id);
+        var rel                          = document.createAttribute('rel');
+        rel.nodeValue                    = 'stylesheet';
+        style.setAttributeNode(rel);
+        var href                         = document.createAttribute('href');
+        href.nodeValue                   = 'usercss-' + i + '.css';
+        style.setAttributeNode(href);
+        var type                         = document.createAttribute('type');
+        type.nodeValue                   = 'text/css';
+        style.setAttributeNode(type);
+        document.getElementsByTagName('head')[0].appendChild(style);
+        style.disabled                   = !enabled;
+      }
+    
+      // Add entry to menu
+      if (newGroup || i == userCSSList.length) {
+        if (beginOfGroup != 0 && (i - beginOfGroup > 1 || !wasSingleSel)) {
+          // The last group had multiple entries that are mutually exclusive;
+          // or the previous to last group did. In either case, we need to
+          // append a "<hr />" before we can add the last group to the menu.
+          menu                          += '<hr />';
+        }
+        wasSingleSel                     = i - beginOfGroup < 1;
+        menu                            += group;
+        group                            = '';
+
+        for (var j = beginOfGroup; j < i; ++j) {
+          this.usercssActions[this.usercssActions.length] =
+            function(vt100, current, begin, count) {
+
+              // Deselect all other entries in the group, then either select
+              // (for multiple entries in group) or toggle (for on/off entry)
+              // the current entry.
+              return function() {
+                var entry                = vt100.getChildById(vt100.menu,
+                                                              'beginusercss');
+                var i                    = -1;
+                var j                    = -1;
+                for (var c = count; c > 0; ++j) {
+                  if (entry.tagName == 'LI') {
+                    if (++i >= begin) {
+                      --c;
+                      var label          = vt100.usercss.childNodes[j];
+                      label.innerHTML    =
+                                       label.innerHTML.replace(/^\u2714 /, '');
+                      var sheet          = document.getElementById(
+                                                               'usercss-' + i);
+                      if (i == current) {
+                        if (count == 1) {
+                          sheet.disabled = !sheet.disabled;
+                        } else {
+                          sheet.disabled = false;
+                        }
+                        if (!sheet.disabled) {
+                          label.innerHTML= '&#10004; ' + label.innerHTML;
+                        }
+                      } else {
+                        sheet.disabled   = true;
+                      }
+                    }
+                  }
+                  entry                  = entry.nextSibling;
+                }
+              };
+            }(this, j, beginOfGroup, i - beginOfGroup);
+        }
+
+        if (i == userCSSList.length) {
+          break;
+        }
+
+        beginOfGroup                     = i;
+      }
+      // Collect all entries in a group, before attaching them to the menu.
+      // This is necessary as we don't know whether this is a group of
+      // mutually exclusive options (which should be separated by "<hr />" on
+      // both ends), or whether this is a on/off toggle, which can be grouped
+      // together with other on/off options.
+      group                             +=
+        '<li>' + (enabled ? '&#10004; ' : '') + label + '</li>';
+    }
+    this.usercss.innerHTML               = menu;
+  }
+};
+
 VT100.prototype.initializeElements = function(container) {
   // If the necessary objects have not already been defined in the HTML
   // page, create them now.
@@ -279,6 +381,7 @@ VT100.prototype.initializeElements = function(container) {
       !this.getChildById(this.container, 'padding')     ||
       !this.getChildById(this.container, 'cursor')      ||
       !this.getChildById(this.container, 'lineheight')  ||
+      !this.getChildById(this.container, 'usercss')     ||
       !this.getChildById(this.container, 'space')       ||
       !this.getChildById(this.container, 'input')       ||
       !this.getChildById(this.container, 'cliphelper')  ||
@@ -325,6 +428,7 @@ VT100.prototype.initializeElements = function(container) {
                          '<pre id="cursor">&nbsp;</pre>' +
                        '</div>' +
                        '<div class="hidden">' +
+                         '<div id="usercss"></div>' +
                          '<pre><div><span id="space"></span></div></pre>' +
                          '<input type="textfield" id="input" />' +
                          '<input type="textfield" id="cliphelper" />' +
@@ -365,12 +469,16 @@ VT100.prototype.initializeElements = function(container) {
   var ieProbe                  = this.getChildById(this.container, 'ieprobe');
   this.padding                 = this.getChildById(this.container, 'padding');
   this.cursor                  = this.getChildById(this.container, 'cursor');
+  this.usercss                 = this.getChildById(this.container, 'usercss');
   this.space                   = this.getChildById(this.container, 'space');
   this.input                   = this.getChildById(this.container, 'input');
   this.cliphelper              = this.getChildById(this.container,
                                                                  'cliphelper');
   this.attributeHelper         = this.getChildById(this.container, 'attrib');
 
+  // Add any user selectable style sheets to the menu
+  this.initializeUserCSSStyles();
+
   // Remember the dimensions of a standard character glyph. We would
   // expect that we could just check cursor.clientWidth/Height at any time,
   // but it turns out that browsers sometimes invalidate these values
@@ -1724,7 +1832,11 @@ VT100.prototype.showContextMenu = function(x, y) {
              (this.utfEnabled ? '&#10004; ' : '') + 'Unicode</li>' +
           '<li id="endconfig">' +
              (this.visualBell ? '&#10004; ' : '') + 'Visual Bell</li>'+
-          '<hr />' +
+          (this.usercss.firstChild ?
+           '<hr id="beginusercss" />' +
+           this.usercss.innerHTML +
+           '<hr id="endusercss" />' :
+           '<hr />') +
           '<li id="about">About...</li>' +
         '</ul>' +
       '</td></tr>' +
@@ -1744,9 +1856,16 @@ VT100.prototype.showContextMenu = function(x, y) {
     menuentries.childNodes[1].className
                               = 'disabled';
   }
+
+  // Actions for default items
   var actions                 = [ this.copyLast, p, this.reset,
-                                  this.toggleUTF, this.toggleBell,
-                                  this.about ];
+                                  this.toggleUTF, this.toggleBell ];
+
+  // Actions for user CSS styles (if any)
+  for (var i = 0; i < this.usercssActions.length; ++i) {
+    actions[actions.length]   = this.usercssActions[i];
+  }
+  actions[actions.length]     = this.about;
 
   // Allow subclasses to dynamically add entries to the context menu
   this.extendContextMenu(menuentries, actions);
This page took 0.089026 seconds and 5 git commands to generate.