changeset 1678:e489ead6574a

New help system * Configurable multiple help location directories * Multiple languages support * Utilizing option guards to be configurable dynamically * Language autodetection from locale with fallback to 'en'
author Myhailo Danylenko <isbear@ukrpost.net>
date Mon, 18 Jan 2010 18:49:25 +0200
parents 9a0ed33fb91b
children b2504df1c910
files mcabber/mcabber/help.c mcabber/mcabber/help.h mcabber/mcabber/main.c
diffstat 3 files changed, 300 insertions(+), 88 deletions(-) [+]
line wrap: on
line diff
--- a/mcabber/mcabber/help.c	Wed Jan 20 11:08:03 2010 +0200
+++ b/mcabber/mcabber/help.c	Mon Jan 18 18:49:25 2010 +0200
@@ -2,6 +2,7 @@
  * help.c       -- Help command
  *
  * Copyright (C) 2006-2009 Mikael Berthe <mikael@lilotux.net>
+ * Copyrigth (C) 2009      Myhailo Danylenko <isbear@ukrpost.net>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,117 +20,323 @@
  * USA
  */
 
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <ctype.h>
-#include <glib.h>
+/*
+ * How it works
+ *
+ * Main calls help_init, that installs option guards. These guards do
+ * nothing, but set help_dirs_stalled flag. When user issues help command,
+ * it checks, if help_dirs_stalled flag is set, and if it is, it calls
+ * init_help_dirs before performing help search.
+ *
+ * Options:
+ *   lang       List of semicolon-separated language codes. If unset, will
+ *              be detected from locale, with fallback to english.
+ *   help_dirs  List of semicolon-seaparated directories, where search for
+ *              help (in language subdirectories) will be performed.
+ *              Defaults to DATA_DIR/mcabber/help.
+ *   help_to_current  Print help to current buddy's buffer.
+ *
+ * XXX:
+ *   Remove command list from hlp.txt and print detected list of all help
+ *   topics?
+ */
 
+#include <glib.h>
+#include <string.h>
+#include <locale.h>
+#include <sys/types.h>
+#include <dirent.h>
+
+#include "logprint.h"
+#include "screen.h"
+#include "hbuf.h"
 #include "settings.h"
-#include "logprint.h"
 #include "utils.h"
-#include "screen.h"
 
-#define DEFAULT_LANG "en"
+static GSList   *help_dirs         = NULL;
+static gboolean  help_dirs_stalled = TRUE;
 
-//  get_lang()
-// Return the language code string (a 2-letters string).
-static const char *get_lang(void) {
-  static const char *lang_str = DEFAULT_LANG;
-#ifdef DATA_DIR
-  static char lang[3];
-  const char *opt_l;
-  opt_l = settings_opt_get("lang");
-  if (opt_l && strlen(opt_l) == 2 && isalpha(opt_l[0]) && isalpha(opt_l[1])) {
-    strncpy(lang, opt_l, sizeof(lang));
-    mc_strtolower(lang);
-    lang_str = lang;
+void free_help_dirs(void)
+{
+  GSList *hel;
+
+  for (hel = help_dirs; hel; hel = hel->next)
+    g_free(hel->data);
+
+  g_slist_free(help_dirs);
+
+  help_dirs = NULL;
+}
+
+void dir_push_languages(const char *langs, const char *dir)
+{
+  const char *lstart = langs;
+  const char *lend;
+  char       *path   = expand_filename(dir);
+
+  for (lend = strchr(lstart, ';'); lend; lend = strchr(lstart, ';')) {
+    char *lang = g_strndup(lstart, lend - lstart);
+    char *dir  = g_strdup_printf("%s/%s", path, lang);
+
+    help_dirs = g_slist_append(help_dirs, dir);
+
+    g_free(lang);
+    lstart = lend + 1;
   }
-#endif /* DATA_DIR */
-  return lang_str;
+
+  { // finishing element
+    char *dir = g_strdup_printf("%s/%s", path, lstart);
+
+    help_dirs = g_slist_append(help_dirs, dir);
+  }
+
+  g_free(path);
 }
 
-//  help_process(string)
-// Display help about the "string" command.
-// If string is null, display general help.
-// Return 0 in case of success.
-int help_process(char *string)
+void init_help_dirs(void)
 {
-#ifndef DATA_DIR
-  scr_LogPrint(LPRINT_NORMAL, "Help isn't available.");
-  return -1;
+  const char *paths;
+  const char *langs;
+  char        lang[6];
+
+  if (help_dirs)
+    free_help_dirs();
+
+  // initialize variables
+  paths = settings_opt_get("help_dirs");
+  if (!paths || !*paths)
+#ifdef DATA_DIR
+    paths = DATA_DIR "/mcabber/help";
 #else
-  const char *lang;
-  FILE *fp;
-  char *helpfiles_dir, *filename;
-  char *data;
-  const int datasize = 4096;
-  int linecount = 0;
-  char *p;
+    paths = "/usr/local/share/mcabber/help;/usr/share/mcabber/help";
+#endif
+
+  langs = settings_opt_get("lang");
+
+  if (!langs || !*langs) {
+    char *locale = setlocale(LC_MESSAGES, NULL);
 
-  // Check string is ok
-  for (p = string; p && *p; p++) {
-    if (!isalnum(*p) && *p != '_' && *p != '-') {
-      scr_LogPrint(LPRINT_NORMAL, "Cannot find help (invalid keyword).");
-      return 1;
-    }
+    // XXX crude method to distinguish between xx_XX xx xx@xxx
+    // and C POSIX NULL etc.
+    if (locale && isalpha(locale[0]) && isalpha(locale[1])
+        && !isalpha(locale[2])) {
+      lang[0] = locale[0];
+      lang[1] = locale[1];
+
+      if (lang[0] == 'e' && lang[1] == 'n')
+        lang[2] = '\0';
+      else {
+        lang[2] = ';';
+        lang[3] = 'e';
+        lang[4] = 'n';
+        lang[5] = '\0';
+      }
+
+      langs = lang;
+    } else
+      langs = "en";
   }
 
-  // Look for help file
-  lang = get_lang();
-  helpfiles_dir = g_strdup_printf("%s/mcabber/help", DATA_DIR);
-  p = NULL;
+  { // parse
+    const char *pstart = paths;
+    const char *pend;
 
-  if (string && *string) {
-    p = g_strdup(string);
-    mc_strtolower(p);
-    filename = g_strdup_printf("%s/%s/hlp_%s.txt", helpfiles_dir, lang, p);
-  } else
-    filename = g_strdup_printf("%s/%s/hlp.txt", helpfiles_dir, lang);
-
-  fp = fopen(filename, "r");
+    for (pend = strchr(pstart, ';'); pend; pend = strchr(pstart, ';')) {
+      char *path = g_strndup(pstart, pend - pstart);
 
-  if (!(fp) && (g_strcmp0(lang, DEFAULT_LANG)) ) {
-    g_free(filename);
-    if (p)
-      filename = g_strdup_printf("%s/%s/hlp_%s.txt", helpfiles_dir, DEFAULT_LANG, p);
-    else
-      filename = g_strdup_printf("%s/%s/hlp.txt", helpfiles_dir, DEFAULT_LANG);
+      dir_push_languages(langs, path);
 
-    fp = fopen(filename, "r");
-  }
-  g_free(p);
-  g_free(filename);
-  g_free(helpfiles_dir);
+      g_free(path);
+      pstart = pend + 1;
+    }
 
-  if (!fp) {
-    scr_LogPrint(LPRINT_NORMAL, "No help found.");
-    return -1;
+    // last element
+    dir_push_languages(langs, pstart);
   }
 
-  data = g_new(char, datasize);
-  while (!feof(fp)) {
-    if (fgets(data, datasize, fp) == NULL) break;
-    // Strip trailing newline
-    for (p = data; *p; p++) ;
-    if (p > data)
-      p--;
-    if (*p == '\n' || *p == '\r')
-      *p = '\0';
-    // Displaty the help line
-    scr_LogPrint(LPRINT_NORMAL, "%s", data);
-    linecount++;
+  help_dirs_stalled = FALSE;
+}
+
+static gboolean do_help_in_dir(const char *arg, const char *path, const char *jid)
+{
+  char       *fname;
+  GIOChannel *channel;
+  GString    *line;
+  int         lines   = 0;
+
+  if (arg && *arg)
+    fname = g_strdup_printf("%s/hlp_%s.txt", path, arg);
+  else
+    fname = g_strdup_printf("%s/hlp.txt", path);
+  
+  channel = g_io_channel_new_file(fname, "r", NULL);
+
+  if (!channel)
+    return FALSE;
+
+  line = g_string_new(NULL);
+
+  while (TRUE) {
+    gsize     endpos;
+    GIOStatus ret;
+    
+    ret = g_io_channel_read_line_string(channel, line, &endpos, NULL);
+    if (ret != G_IO_STATUS_NORMAL) // XXX G_IO_STATUS_AGAIN?
+      break;
+
+    line->str[endpos] = '\0';
+
+    if (jid)
+      scr_WriteIncomingMessage(jid, line->str, 0,
+                               HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0);
+    else
+      scr_LogPrint(LPRINT_NORMAL, "%s", line->str);
+
+    ++lines;
   }
-  fclose(fp);
-  g_free(data);
+
+  g_string_free(line, TRUE);
 
-  if (linecount) {
+  if (!lines)
+    return FALSE;
+
+  if (!jid) {
     scr_setmsgflag_if_needed(SPECIAL_BUFFER_STATUS_ID, TRUE);
     update_roster = TRUE;
   }
 
-  return 0;
-#endif /* DATA_DIR */
+  return TRUE;
 }
 
-/* vim: set expandtab cindent cinoptions=>2\:2(0:  For Vim users... */
+void help_process(char *arg)
+{
+  gchar      *string;
+  const char *jid    = NULL;
+  gboolean    done   = FALSE;
+
+  if (help_dirs_stalled)
+    init_help_dirs();
+
+  { // check input
+    char *c;
+
+    for (c = arg; *c; ++c)
+      if (!isalnum(*c) && *c != '-' && *c != '_') {
+        scr_LogPrint(LPRINT_NORMAL, "Wrong help expression, "
+                     "it can contain only alphbetic, numeric"
+                     " characters and symbols '-' and '_'.");
+        return;
+      }
+
+    string = g_strdup(arg);
+    mc_strtolower(string);
+  }
+
+  if (settings_opt_get_int("help_to_current") && CURRENT_JID)
+    jid = CURRENT_JID;
+  
+  { // search
+    GSList *hel;
+
+    for (hel = help_dirs; hel && !done; hel = hel->next) {
+      char *dir = (char *)hel->data;
+      done = do_help_in_dir(string, dir, jid);
+    }
+  }
+
+  if (!done && string && *string) { // match and print any similar topics
+    GSList *hel;
+    GSList *matches = NULL;
+
+    for (hel = help_dirs; hel; hel = hel->next) {
+      const char *path = (const char *)hel->data;
+      DIR        *dd   = opendir(path);
+
+      if (dd) {
+        struct dirent *file;
+
+        for (file = readdir(dd); file; file = readdir(dd)) {
+          const char *name = file->d_name;
+
+          if (name && name[0] == 'h' && name[1] == 'l' &&
+                      name[2] == 'p' && name[3] == '_') {
+            const char *nstart = name + 4;
+            const char *nend   = strrchr(nstart, '.');
+
+            if (nend) {
+              gsize len = nend - nstart;
+
+              if (g_strstr_len(nstart, len, string)) {
+                gchar *match = g_strndup(nstart, len);
+              
+                if (!g_slist_find_custom(matches, match,
+                                         (GCompareFunc)strcmp))
+                  matches = g_slist_append(matches, match);
+                else
+                  g_free(match);
+
+                done = TRUE;
+              }
+            }
+          }
+        }
+
+        closedir(dd);
+      }
+    }
+
+    if (done) {
+      GString *message = g_string_new("No exact match found. "
+                                      "Keywords, that contain this word:");
+      GSList  *wel;
+
+      for (wel = matches; wel; wel = wel->next) {
+        gchar *word = (gchar *)wel->data;
+
+        g_string_append_printf(message, " %s,", word);
+
+        g_free(wel->data);
+      }
+
+      message->str[message->len - 1] = '.';
+
+      g_slist_free(matches);
+
+      {
+        char *msg = g_string_free(message, FALSE);
+
+        if (jid)
+          scr_WriteIncomingMessage(jid, msg, 0,
+                                   HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0);
+        else
+          scr_LogPrint(LPRINT_NORMAL, "%s", msg);
+
+        g_free(msg);
+      }
+    }
+  }
+
+  if (!done) {
+    if (jid) // XXX
+      scr_WriteIncomingMessage(jid, "No help found.", 0,
+                               HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0);
+    else
+      scr_LogPrint(LPRINT_NORMAL, "No help found.");
+  }
+
+  g_free(string);
+}
+
+static gchar *help_guard(const gchar *key, const gchar *new_value)
+{
+  help_dirs_stalled = TRUE;
+  return g_strdup(new_value);
+}
+
+void help_init(void)
+{
+  settings_set_guard("lang", help_guard);
+  settings_set_guard("help_dirs", help_guard);
+}
+
+/* vim: set expandtab cindent cinoptions=>2\:2(0 sw=2 ts=2:  For Vim users... */
--- a/mcabber/mcabber/help.h	Wed Jan 20 11:08:03 2010 +0200
+++ b/mcabber/mcabber/help.h	Mon Jan 18 18:49:25 2010 +0200
@@ -1,7 +1,10 @@
 #ifndef __MCABBER_HELP_H__
 #define __MCABBER_HELP_H__ 1
 
-int help_process(char *string);
+void free_help_dirs(void);
+void init_help_dirs(void);
+void help_process(char *string);
+void help_init(void);
 
 #endif /* __MCABBER_HELP_H__ */
 
--- a/mcabber/mcabber/main.c	Wed Jan 20 11:08:03 2010 +0200
+++ b/mcabber/mcabber/main.c	Mon Jan 18 18:49:25 2010 +0200
@@ -44,6 +44,7 @@
 #include "otr.h"
 #include "fifo.h"
 #include "xmpp.h"
+#include "help.h"
 
 #ifdef ENABLE_HGCSET
 # include "hgcset.h"
@@ -359,6 +360,7 @@
   /* Initialize charset */
   scr_InitLocaleCharSet();
   ut_InitDebug();
+  help_init();
 
   /* Parsing config file... */
   ret = cfg_read_file(configFile, TRUE);