Index: apps/plugins/viewers.config
===================================================================
--- apps/plugins/viewers.config	(revision 12622)
+++ apps/plugins/viewers.config	(working copy)
@@ -28,3 +28,4 @@
 tzx,viewers/zxbox,66 52 4A 66 52 4A
 z80,viewers/zxbox,66 52 4A 66 52 4A
 zzz,viewers/properties,00 00 00 00 00 00
+idx,viewers/dict,01 55 55 55 55 54
Index: apps/plugins/dict.c
===================================================================
--- apps/plugins/dict.c	(revision 12622)
+++ apps/plugins/dict.c	(working copy)
@@ -7,7 +7,7 @@
  *                     \/            \/     \/    \/            \/
  * $Id$
  *
- * Copyright (C) 2005 Tomas Salfischberger
+ * Copyright (C) 2005 Tomas Salfischberger, 2006 Timo Horstschäfer
  *
  * All files in this archive are subject to the GNU General Public License.
  * See the file COPYING in the source tree root for full license agreement.
@@ -18,302 +18,934 @@
  ****************************************************************************/
 
 #include "plugin.h"
+#include "pluginlib_actions.h"
+#include "configfile.h"
+#include "playback_control.h"
 
+#define UP          PLA_UP
+#define UP_REPEAT   PLA_UP_REPEAT
+#define DOWN        PLA_DOWN
+#define DOWN_REPEAT PLA_DOWN_REPEAT
+#define NEW         PLA_START
+#define MENU        PLA_MENU
+#define QUIT        PLA_QUIT
+
 PLUGIN_HEADER
 
-/* save the plugin api pointer. */
 static struct plugin_api* rb;
-/* screen info */
-static int display_columns, display_lines;
 
-/* Some lenghts */
-#define WORDLEN 32 /* has to be the same in rdf2binary.c */
+#define CONFIG_VERSION  1
+#define CACHE_VERSION   1
 
-/* Struct packing */
-#ifdef __GNUC__
-#define STRUCT_PACKED __attribute__((packed))
-#else
-#define STRUCT_PACKED
-#pragma pack (push, 2)
-#endif
+//! Global dict variables
+static struct dict_s {
+    char path[255]; /**< Path of current dictionary */
+    char name[255]; /**< Basename of current dictionary */
 
-/* The word struct :) */
-struct stWord
+    int fCache;
+    int fIndex;
+    int fDict;
+    //! Current description file (there may be more than one)
+    int fDict_n; 
+
+    //! Display information
+    int cols, rows;
+
+    int line; /* current line in description */
+
+} Dict;
+
+#define DESC_BUFFER_ADD 512
+
+#define CONFIG_FILENAME "dict.cfg"
+#define CONFIG_ITEMS ( sizeof(Conf_data) / sizeof (struct configdata) )
+struct conf_s {
+    int scroll; /**< Number of lines to scroll */
+    int max_list; /**< Max number of articles listed */
+} Conf;
+
+struct configdata Conf_data[] = {
+    { TYPE_INT, 1, 256, &Conf.scroll, "scroll", NULL, NULL },
+    { TYPE_INT, 1, 4096, &Conf.scroll, "max_list", NULL, NULL },
+};
+
+#define SCROLL_DEFAULT      Dict.rows
+#define MAX_LIST_DEFAULT    100
+
+//! Malloc variables
+static struct malloc_s {
+    char *buf;
+    int bufsize;
+    int bufpos;
+} Malloc;
+
+struct cache_h {
+    char magic[9];
+    uint16_t version;
+};
+static struct cache_h Cache_h = {
+    "DICT_OFT",
+    CACHE_VERSION
+};
+
+//! Length of word_str in stardict's DICT format
+#define WORDLEN 256
+
+//! Rockbox filesize limit
+#define MAX_FILESIZE 2147483647 
+
+enum dict_action
 {
-    char word[WORDLEN];
-    long offset;
-} STRUCT_PACKED;
+    DICT_USB_CONNECTED = -4,
+    DICT_QUIT = -3,
+    DICT_NOT_FOUND = -2,
+    DICT_ERROR = -1,
+    DICT_OK,
+    DICT_NEW,
+    DICT_MENU,
+};
 
-/* A funtion to get width and height etc (from viewer.c) */
-void init_screen(void)
+/** \brief Data structure in .idx files
+ *
+ * Structure of .idx files:
+ * \code
+ *      char[]      name;   // variable length, zero-terminated, UTF-8
+ *      uint32_t    offset; // beginning of the article in the .dict file, Big Endian
+ *      uint32_t    size;   // article size, Big Endian
+ * \endcode
+ */
+struct WordData
 {
-#ifdef HAVE_LCD_BITMAP
-    int w,h;
+    uint32_t offset;
+    uint32_t size;
+};
 
-    rb->lcd_getstringsize("o", &w, &h);
-    display_lines = LCD_HEIGHT / h;
-    display_columns = LCD_WIDTH / w;
-#else
+//! Internal structure for the result
+struct DictEntry
+{
+    char name[WORDLEN];
+    long index;
+    struct WordData data;
+};
 
-    display_lines = 2;
-    display_columns = 11;
-#endif
+//! work around gcc wanting memcpy
+void *memcpy(void *destination, const void *source, size_t num)
+{
+    return rb->memcpy(destination, source, num);
 }
 
-/* global vars for pl_malloc() */
-void *bufptr;
-int bufleft;
+//! Display an error message
+void dict_error(const char *msg)
+{
+    DEBUGF("%s\n", msg);
+    rb->splash(HZ, true, msg);
+}
 
-/* simple function to "allocate" memory in pluginbuffer. */
-void *pl_malloc(int size)
+//! Initialize the plugin buffer
+void dict_buf_init(void)
 {
-    void *ptr;
-    ptr = bufptr;
+    Malloc.buf = rb->plugin_get_buffer(&Malloc.bufsize);
+    Malloc.bufpos = 0;
+}
 
-    if (bufleft < size)
+/** \brief Stack-like malloc
+ *
+ * This malloc implementation only keeps track of how much memory is allocated
+ */
+void *dict_malloc(int size)
+{
+    if (Malloc.bufpos+size <= Malloc.bufsize)
     {
+        Malloc.bufpos += size;
+        return Malloc.buf + Malloc.bufpos - size;
+    } else
         return NULL;
-    }
+}
+
+/** \brief Stack-like free
+ *
+ * @param size Nnumber of bytes to free, 0 will free the whole buffer
+ */
+void dict_free(int size)
+{
+    if (size == 0 || size > Malloc.bufpos)
+        Malloc.bufpos = 0;
     else
+        Malloc.bufpos -= size;
+}
+
+//! Extract the path and name of the given filename
+void dict_get_name(const char *name)
+{
+    char *slash;
+
+    slash = rb->strrchr(name, '/');
+    
+    /* get path */
+    rb->strncpy(Dict.path, name, slash-name);
+
+    /* get filename without extension */
+    rb->strncpy(Dict.name, slash+1, rb->strrchr(name, '.')-slash-1);
+}
+
+//! Generates the cache file to the current dictionary
+enum dict_action dict_create_cache(void)
+{
+    uint32_t offset;
+    int size;
+    char c;
+    enum dict_action ret = DICT_OK;
+
+    int button;
+    const struct button_mapping *plugin_contexts[] = {
+        generic_actions
+    };
+
+    rb->splash(0, true, "Creating cache file... may take some minutes");
+
+    rb->lseek(Dict.fCache, 0, SEEK_SET);
+
+    /* write header */
+    rb->write(Dict.fCache, &Cache_h, sizeof(struct cache_h));
+
+    offset = 0;
+    while (ret == DICT_OK)
     {
-        bufptr += size;
-        return ptr;
+        /* get name length */
+        size = 0;
+        while (true)
+        {
+            if (rb->read(Dict.fIndex, &c, 1) < 1)
+                break;
+            size++;
+            if (c == '\0')
+                break;
+        }
+        if (size < 1)
+            break;
+
+        /* write current position to file */
+        rb->write(Dict.fCache, &offset, sizeof(offset));
+
+        rb->lseek(Dict.fIndex, sizeof(struct WordData), SEEK_CUR);
+        offset += sizeof(struct WordData) + size;
+
+        /* don't take over all control */
+        button = pluginlib_getaction(rb, TIMEOUT_NOBLOCK, plugin_contexts, 1);
+        switch (button)
+        {
+            case DICT_QUIT:
+                ret = DICT_ERROR;
+                break;
+            default:
+                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
+                    ret = DICT_USB_CONNECTED;
+                break;
+        }
+        rb->yield();
     }
+
+    return ret;
 }
 
-/* init function for pl_malloc() */
-void pl_malloc_init(void)
+//! Closes all file descriptors
+void dict_close(void)
 {
-    bufptr = rb->plugin_get_buffer(&bufleft);
+    rb->close(Dict.fCache);
+    rb->close(Dict.fIndex);
+    rb->close(Dict.fDict);
+
+    configfile_save(CONFIG_FILENAME, Conf_data,
+                    CONFIG_ITEMS, CONFIG_VERSION);
 }
 
-/* for endian problems */
-#ifdef ROCKBOX_BIG_ENDIAN
-#define reverse(x) x
-#else
-long reverse (long N) {
-    unsigned char B[4];
-    B[0] = (N & 0x000000FF) >> 0;
-    B[1] = (N & 0x0000FF00) >> 8;
-    B[2] = (N & 0x00FF0000) >> 16;
-    B[3] = (N & 0xFF000000) >> 24;
-    return ((B[0] << 24) | (B[1] << 16) | (B[2] << 8) | (B[3] << 0));
+enum dict_action dict_open(void)
+{
+    char fn[FAT_FILENAME_BYTES];
+
+    /* index file */
+    rb->snprintf(fn, sizeof(fn), "%s/%s.idx", Dict.path, Dict.name);
+    Dict.fIndex = rb->open(fn, O_RDONLY);
+    if (Dict.fIndex < 0)
+    {
+        dict_error("Failed to open index file");
+        dict_close();
+        return DICT_ERROR;
+    }
+
+    /* cache file */
+    rb->snprintf(fn, sizeof(fn), "%s/%s.oft", ROCKBOX_DIR, Dict.name);
+    Dict.fCache = rb->open(fn, O_RDONLY);
+    if (Dict.fCache >= 0)
+    {
+        /* check the cache file */
+        struct cache_h header;
+
+        rb->read(Dict.fCache, &header, sizeof(struct cache_h));
+        if (!rb->memcmp(&header, &Cache_h, sizeof(struct cache_h)))
+            return PLUGIN_OK;
+
+        dict_error("Cache file outdated");
+
+        rb->close(Dict.fCache);
+        rb->remove(fn);
+    }
+
+    /* incorrect cache file, create a new one */
+    Dict.fCache = rb->creat(fn);
+    if (Dict.fCache >= 0)
+    {
+        if (dict_create_cache() == DICT_OK)
+        {
+            /* close it to reopen it read-only */
+            rb->close(Dict.fCache);
+            Dict.fCache = rb->open(fn, O_RDONLY);
+            return DICT_OK;
+        }
+        else
+        {
+            rb->close(Dict.fCache);
+            rb->remove(fn);
+        }
+    }
+
+    dict_error("Failed to open cache file");
+    return DICT_ERROR;
 }
-#endif
 
-/* Button definitions */
-#if CONFIG_KEYPAD == PLAYER_PAD
-#define LP_QUIT BUTTON_STOP
-#elif (CONFIG_KEYPAD == IPOD_4G_PAD) || \
-      (CONFIG_KEYPAD == IPOD_3G_PAD)
-#define LP_QUIT BUTTON_MENU
-#elif CONFIG_KEYPAD == IRIVER_IFP7XX_PAD
-#define LP_QUIT BUTTON_PLAY
-#elif CONFIG_KEYPAD == IAUDIO_X5_PAD
-#define LP_QUIT BUTTON_POWER
-#elif CONFIG_KEYPAD == GIGABEAT_PAD
-#define LP_QUIT BUTTON_A
-#elif CONFIG_KEYPAD == SANSA_E200_PAD
-#define LP_QUIT BUTTON_POWER
-#elif CONFIG_KEYPAD == IRIVER_H10_PAD
-#define LP_QUIT BUTTON_POWER
+/** \brief Initializes everything related to dict.
+ *
+ * @param filename Filename of the .idx file for the dictionary
+ */
+enum dict_action dict_init(const char *file)
+{
+    /* malloc */
+    dict_buf_init();
+
+    /* display */
+#ifdef HAVE_LCD_BITMAP
+    rb->lcd_getstringsize("o", &Dict.cols, &Dict.rows);
+    Dict.cols = LCD_WIDTH / Dict.cols;
+    Dict.rows = LCD_HEIGHT / Dict.rows;
 #else
-#define LP_QUIT BUTTON_OFF
+    Dict.cols = 11;
+    Dict.rows = 2;
 #endif
 
-/* data files */
-#define DICT_INDEX ROCKBOX_DIR "/dict.index"
-#define DICT_DESC ROCKBOX_DIR "/dict.desc"
+    /* variables */
+    Conf.scroll     = SCROLL_DEFAULT;
+    Conf.max_list   = MAX_LIST_DEFAULT;
+    Dict.line = 0;
+    Dict.fDict_n = -1;
 
-/* the main plugin function */
-enum plugin_status plugin_start(struct plugin_api* api, void* parameter)
+    /* configuration data */
+    configfile_init(rb);
+    configfile_load(CONFIG_FILENAME, Conf_data,
+                    CONFIG_ITEMS, CONFIG_VERSION);
+
+    /* dict name */
+    dict_get_name(file);
+
+    /* files */
+    return dict_open();
+}
+
+/** \brief Returns offset for an entry in the idx file
+ *
+ * This function allows random access to the idx file
+ */
+uint32_t dict_cache_offset(int32_t index)
 {
-    char searchword[WORDLEN]; /* word to search for */
-    char *description; /* pointer to description buffer */
-    char *output; /* pointer to output buffer */
-    char *ptr, *space;
-    struct stWord word; /* the struct to read into */
-    int fIndex, fData; /* files */
-    int filesize, high, low, probe;
-    int lines, len, outputted, next;
+    uint32_t offset;
 
-    /* plugin stuff */
-    (void)parameter;
-    rb = api;
+    /* read offset */
+    rb->lseek(Dict.fCache, sizeof(uint32_t)*index + sizeof(struct cache_h),
+              SEEK_SET);
+    rb->read(Dict.fCache, &offset, sizeof(uint32_t));
 
-    /* get screen info */
-    init_screen();
+    return offset;
+}
 
-    /* get pl_malloc() buffer ready. */
-    pl_malloc_init();
+//! Compare function, that represents the order in the .idx file
+int dict_index_strcmp(const char *str1, const char *str2)
+{
+    int a;
 
-    /* init description buffer (size is because we don't have scrolling)*/
-    description = (char *)pl_malloc(display_columns * display_lines);
-    if (description == NULL)
+    a = rb->strcasecmp(str1, str2);
+    if (!a)
     {
-        DEBUGF("Err: failed to allocate description buffer.");
-        return PLUGIN_ERROR;
+        return rb->strcmp(str1, str2);
     }
+    
+    return a;
+}
 
-    /* init output buffer */
-    output = (char *)pl_malloc(display_columns);
-    if (output == NULL)
+/** \brief Reads up to n bytes from current fIndex position until a zero occurs
+ *
+ * \return Number of bytes read.
+ */
+int dict_index_gets(char *buf, int n)
+{
+    int i;
+    for (i=0; i<n; i++)
     {
-        DEBUGF("Err: failed to allocate output buffer.");
-        return PLUGIN_ERROR;
+        if (!rb->read(Dict.fIndex, buf, 1))
+            break;
+        if (! *buf++)
+        {
+            i++;
+            break;
+        }
     }
+    return i;
+}
 
-    /* "clear" input buffer */
-    searchword[0] = '\0';
+/** \brief Reads up to n bytes from an entry in the idx file until a zero occurs
+ *
+ * @param index The index in idx t read from.
+ * \return Number of bytes read.
+ */
+int dict_index_gets_from(long index, char *buf, int n)
+{
+    rb->lseek(Dict.fIndex, dict_cache_offset(index), SEEK_SET);
+    return dict_index_gets(buf, n);
+}
 
-    rb->kbd_input(searchword, sizeof(searchword)); /* get the word to search */
+/** \brief Extracts all data about an entry in idx
+ *
+ * \return length of the article name
+ */
+int dict_index_entry(long index, struct DictEntry *buf)
+{
+    int size;
 
-    fIndex = rb->open(DICT_INDEX, O_RDONLY); /* index file */
-    if (fIndex < 0)
-    {
-        DEBUGF("Err: Failed to open index file.\n");
-        rb->splash(HZ*2, true, "Failed to open index.");
-        return PLUGIN_ERROR;
-    }
+    buf->index = index;
 
-    filesize = rb->filesize(fIndex); /* get filesize */
+    size = dict_index_gets_from(index, buf->name, WORDLEN);
 
-    DEBUGF("Filesize: %d bytes = %d words \n", filesize,
-           (filesize / sizeof(struct stWord)));
+    rb->read(Dict.fIndex, &buf->data, sizeof( struct WordData ));
+    /* convert to host endianess */
+    buf->data.offset = betoh32(buf->data.offset);
+    buf->data.size = betoh32(buf->data.size);
 
-    /* for the searching algorithm */
-    high = filesize / sizeof( struct stWord );
+    return size;
+}
+
+/** \brief Binary search function
+ *
+ * @return If nothing was found, the last probe is returned negative.
+ */
+long dict_index_binary_search(const char *search,
+                              int(*cmpf)(const char *, const char *))
+{
+    char word[WORDLEN];
+    int32_t high, low, probe;
+    int cmp;
+
+    high = rb->filesize(Dict.fCache) / sizeof( uint32_t );
     low = -1;
+    probe = 0;
 
     while (high - low > 1)
     {
         probe = (high + low) / 2;
 
-        /* Jump to word pointed by probe, and read it. */
-        rb->lseek(fIndex, sizeof(struct stWord) * probe, SEEK_SET);
-        rb->read(fIndex, &word, sizeof(struct stWord));
+        dict_index_gets_from(probe, word, WORDLEN);
 
         /* jump according to the found word. */
-        if (rb->strcasecmp(searchword, word.word) < 0)
-        {
+        cmp = cmpf(search, word);
+        if (cmp < 0)
             high = probe;
-        }
+        else if (cmp > 0)
+            low = probe;
         else
+            return probe;
+    }
+
+    return -probe;
+}
+
+/** \brief Shows a selection menu for all articles starting with the given word
+ *
+ * The selections screen is only displayed, if there's more than one result.
+ */
+long dict_index_find_startingWith(const char *search)
+{
+    long index;
+    char buf[WORDLEN];
+    int len, new_len;
+    int n;
+    int buffer_size;
+    struct menu_item *items;
+    int menu;
+    int sel;
+    char *new_word;
+    int statusbar_setting;
+
+    len = rb->strlen(search);
+
+    index = dict_index_binary_search(search, rb->strcasecmp);
+    if (index < 0)
+        index = -index;
+
+    /* go to the first entry that starts with the word */
+    for (;;index--)
+    {
+        dict_index_gets_from(index, buf, WORDLEN);
+        if (rb->strncasecmp(search, buf, len) != 0)
         {
-            low = probe;
+            index++;
+            break;
         }
+        else if (index == 0)
+            break;
     }
 
-    /* read in the word */
-    rb->lseek(fIndex, sizeof(struct stWord) * low, SEEK_SET);
-    rb->read(fIndex, &word, sizeof(struct stWord));
+    buffer_size = Conf.max_list * sizeof(struct menu_item);
+    items = (struct menu_item *)dict_malloc(buffer_size);
 
-    /* Check if we found something */
-    if (low == -1 || rb->strcasecmp(searchword, word.word) != 0)
+    rb->lseek(Dict.fIndex, dict_cache_offset(index), SEEK_SET);
+
+    /* find all words that start with the word */
+    for (sel=n=0; n<Conf.max_list; n++)
     {
-        DEBUGF("Not found.\n");
-        rb->splash(HZ*2, true, "Not found.");
-        rb->close(fIndex);
-        return PLUGIN_OK;
+        new_len = dict_index_gets(buf, WORDLEN);
+
+        /* find entries starting with... */
+        if (rb->strncasecmp(search, buf, len) != 0)
+            break;
+
+        /* allocate memory and add to menu_items list */
+        new_word = (char *)dict_malloc(new_len);
+        if (new_word == NULL)
+            break;
+        buffer_size += new_len;
+
+        rb->strncpy(new_word, buf, new_len);
+
+        items[n].desc = new_word;
+        items[n].function = NULL;
+
+        /* ignore the entry data */
+        rb->lseek(Dict.fIndex, sizeof(struct WordData), SEEK_CUR);
     }
 
-    DEBUGF("Found %s at offset %d\n", word.word, reverse(word.offset));
+    /* show the selection list */
+    if (n > 1)
+    {
+        /* disable status bar */
+        statusbar_setting = rb->global_settings->statusbar;
+        rb->global_settings->statusbar = false;
 
-    /* now open the description file */
-    fData = rb->open(DICT_DESC, O_RDONLY);
-    if (fData < 0)
+        menu = rb->menu_init(items, n, NULL, NULL, NULL, NULL);
+        sel = rb->menu_show(menu);
+        rb->menu_exit(menu);
+
+        /* restore status bar */
+        rb->global_settings->statusbar = statusbar_setting;
+
+#ifdef HAVE_LCD_BITMAP
+        rb->lcd_setmargins(0, 0);
+#endif
+    }
+
+    dict_free(buffer_size);
+
+    if (n == 0)
+        return DICT_NOT_FOUND;
+
+    switch (sel)
     {
-        DEBUGF("Err: Failed to open description file.\n");
-        rb->splash(HZ*2, true, "Failed to open descriptions.");
-        rb->close(fIndex);
-        return PLUGIN_ERROR;
+        case MENU_ATTACHED_USB:
+            return DICT_USB_CONNECTED;
+        case MENU_SELECTED_EXIT:
+            return DICT_QUIT;
+        default:
+            return index + sel;
     }
+}
 
-    /* seek to the right offset */
-    rb->lseek(fData, (off_t)reverse(word.offset), SEEK_SET);
+//! Wrapper to open the correct .desc file depending on the description offset
+enum dict_action dict_desc_open(int n)
+{
+    char fn[FAT_FILENAME_BYTES];
 
-    /* Read in the description */
-    rb->read_line(fData, description, display_columns * display_lines);
+    if (n == Dict.fDict_n)
+        return DICT_OK;
 
+    if (n < 1)
+        rb->snprintf(fn, sizeof(fn), "%s/%s.dict", Dict.path, Dict.name);
+    else
+        rb->snprintf(fn, sizeof(fn), "%s/%s.dict.%d", Dict.path, Dict.name, n);
+
+    Dict.fDict = rb->open(fn, O_RDONLY);
+    if (Dict.fDict < 0)
+    {
+        dict_error("Failed to open description file");
+        /* show filename */
+        dict_error(fn);
+        return DICT_ERROR;
+    }
+
+    Dict.fDict_n = n;
+
+    return DICT_OK;
+}
+
+/** \brief Reads the article description
+ *
+ * @param article The article to be read.
+ */
+char *dict_desc_read(struct WordData *article, char *buf)
+{
+    uint32_t offset = article->offset;
+    int len, n = offset/MAX_FILESIZE;
+
+    if (dict_desc_open(n) != DICT_OK)
+        return NULL;
+
+    offset %= MAX_FILESIZE;
+
+    rb->lseek(Dict.fDict, offset, SEEK_SET);
+    len = rb->read(Dict.fDict, buf, article->size);
+
+    /* check if article is splitted between two files */
+    if (offset > MAX_FILESIZE - article->size)
+    {
+        rb->close(Dict.fDict);
+        if (dict_desc_open(++n) != DICT_OK)
+            return NULL;
+
+        rb->read(Dict.fDict, buf+len, article->size-len);
+    }
+    buf[article->size] = '\0';
+
     /* And print it to debug. */
-    DEBUGF("Description: %s\n", description);
+    DEBUGF("Description: %s\n", buf);
 
-    /* get pointer to first char */
-    ptr = description;
+    return buf;
+}
 
-    lines = 0;
-    outputted = 0;
-    len = rb->strlen(description);
+/** \brief Returns a pointer to the last character of a line.
+ *
+ * @param str Pointer to the actual line.
+ */
+char *dict_desc_endl(const char *str)
+{
+    const char *space = NULL;
+    int cols = Dict.cols;
 
-    /* clear screen */
+    while (true)
+    {
+        if (*str == '\n' || *str == '\0')
+            return (char *)str;
+        if (*str == ' ')
+            space = str;
+        if (!cols--)
+            break;
+        str += rb->utf8seek(str, 1);
+    }
+
+    return (char *) ( (space==NULL) ? str : space );
+}
+
+//! Returns a pointer to the beginning of the given line
+char *dict_desc_line(const char *str, int line)
+{
+    while (line--)
+    {
+        str = dict_desc_endl(str);
+        if (!*str)
+            return NULL;
+        str += rb->utf8seek(str, 1);
+    }
+
+    return (char *) str;
+}
+
+/** \brief Draws text on the display.
+ *
+ * Handles newline characters and line wrapping.
+ */
+void dict_desc_draw(const char *str)
+{
+    char *next;
+    int line, len;
+    char buf[WORDLEN];
+
     rb->lcd_clear_display();
 
-    /* for large screens display the searched word. */
-    if(display_lines > 4)
+    for (line=0; line<Dict.rows; line++)
     {
-        rb->lcd_puts(0, lines, searchword);
-        lines++;
+        next = dict_desc_endl(str);
+
+        len = next-str;
+        rb->strncpy(buf, str, len);
+        buf[len] = '\0';
+
+        rb->lcd_puts(0, line, buf);
+
+        if (!*next)
+            break;
+        str = next + rb->utf8seek(str, 1);
     }
 
-    /* TODO: Scroll, or just stop when there are to much lines. */
-    while (1)
+    rb->lcd_update();
+}
+
+//! Text viewer function
+enum dict_action dict_display(const char *str)
+{
+    const char *start, *tmp;
+
+    int button;
+    const struct button_mapping *plugin_contexts[] = {
+        generic_directions,
+        generic_actions
+    };
+
+    start = dict_desc_line(str, Dict.line);
+    dict_desc_draw(start);
+
+    while (true)
     {
-        /* copy one lcd line */
-        rb->strncpy(output, ptr, display_columns);
-        output[display_columns] = '\0';
+        rb->yield();
 
-        /* typecast to kill a warning... */
-        if((int)rb->strlen(ptr) < display_columns)
+        button = pluginlib_getaction(rb, HZ, plugin_contexts, 2);
+        rb->sleep(HZ/10);
+        switch (button)
         {
-            rb->lcd_puts(0, lines, output);
-            lines++;
-            break;
+            case UP: case UP_REPEAT:
+                Dict.line = (Dict.line>Conf.scroll) ? Dict.line-Conf.scroll : 0;
+                start = dict_desc_line(str, Dict.line);
+                dict_desc_draw(start);
+                break;
+            case DOWN: case DOWN_REPEAT:
+                tmp = dict_desc_line(start, Conf.scroll);
+                if (tmp == NULL)
+                    break;
+
+                start = tmp;
+                Dict.line += Conf.scroll;
+                dict_desc_draw(start);
+                break;
+            case NEW:
+                return DICT_NEW;
+            case MENU:
+                return DICT_MENU;
+            case QUIT:
+                return DICT_QUIT;
+            default:
+                if (rb->default_event_handler(button) == SYS_USB_CONNECTED)
+                    return DICT_USB_CONNECTED;
+                break;
         }
+    }
+}
 
+void dict_menu(void)
+{
+    int m;
+    bool quit = false;
+    struct menu_item items[] = {
+        { "Scroll lines", NULL },
+        { "Max listed articles", NULL },
+        { "Playback control", NULL },
+    };
+    m = rb->menu_init(items, sizeof(items) / sizeof(struct menu_item),
+                      NULL, NULL, NULL, NULL);
+    while (!quit) {
+        switch (rb->menu_show(m))
+        {
+            case 0:
+                quit = rb->set_int("Scroll lines", "", UNIT_INT,
+                                   &Conf.scroll, NULL, 1, 1, Dict.rows, NULL);
+                break;
+            case 1:
+                quit = rb->set_int("Max listed articles", "", UNIT_INT,
+                                   &Conf.max_list, NULL, 100, 100, 1000, NULL);
+                break;
+            case 2:
+                quit = playback_control(rb);
+                break;
+            default:
+                quit = true;
+                break;
+        }
+    }
 
-        /* get the last spacechar */
-        space = rb->strrchr(output, ' ');
+    rb->menu_exit(m);
+#ifdef HAVE_LCD_BITMAP
+    rb->lcd_setmargins(0,0);
+#endif
+}
 
-        if (space != NULL)
+//! Parses #redirect from MediaWiki
+int parse_mw_redirect(struct DictEntry *a, const char *desc)
+{
+    char *p;
+    int len;
+    char redirect[] = "#redirect";
+    long index;
+
+    if ( rb->strncasecmp(desc, redirect, sizeof(redirect)-1) ) 
+        return 0;
+
+    /* find destination article */
+    p = rb->strchr(desc+sizeof(redirect), '[') + 2;
+    len = rb->strchr(p, ']') - p;
+
+    rb->strncpy(a->name, p, len);
+    a->name[len] = '\0';
+
+    /* replace '_' with ' ' */
+    for (p=a->name; *p; p++)
+    {
+        if (*p == '_')
+            *p = ' ';
+    }
+
+
+    index = dict_index_binary_search(a->name, dict_index_strcmp);
+    if (index < 0)
+    {
+        dict_error("Illegal redirect");
+        return 0;
+    }
+    dict_index_entry(index, a);
+
+    return 1;
+}
+    
+
+//! Dict main loop
+enum dict_action dict_main(void)
+{
+    struct DictEntry result;
+    char search[WORDLEN], old[WORDLEN];
+    char *desc;
+    int desc_size = 0, title_size;
+    bool new_search = true;
+    enum plugin_status ret;
+
+    search[0] = '\0';
+
+    while (true)
+    {
+        rb->strcpy(old, search);
+        rb->kbd_input(search, WORDLEN);
+        /* exit if the search string is empty or the user didn't change it */
+        if (!rb->strlen(search) ||
+            (!new_search && !rb->strncmp(old, search, sizeof(old)) ))
         {
-            *space = '\0';
-            next = (space - (char*)output) + 1;
+            return DICT_OK;
         }
-        else
+        new_search = false;
+
+        /* keep the disk running */
+        rb->ata_spindown(256);
+
+        result.index = dict_index_find_startingWith(search);
+        switch (result.index)
         {
-            next = display_columns;
+            case DICT_NOT_FOUND:
+                dict_error("No articles found");
+                continue;
+            case DICT_QUIT:
+                return DICT_OK;
+            case DICT_USB_CONNECTED:
+                return PLUGIN_USB_CONNECTED;
         }
 
-        /* put the line on screen */
-        rb->lcd_puts(0, lines, output);
+        /* get result data */
+        dict_index_entry(result.index, &result); 
 
-        /* get output count */
-        outputted += rb->strlen(output);
+        do {
+            if (desc_size > 0)
+                dict_free(desc_size);
 
-        if (outputted < len)
+            /* increase the buffer to put a title in front of the article */
+            desc_size = result.data.size + DESC_BUFFER_ADD;
+
+            /* set desc pointer to beginning of the description */
+            desc = (char *)dict_malloc(desc_size) + DESC_BUFFER_ADD;
+            dict_desc_read(&result.data, desc);
+
+        } while (parse_mw_redirect(&result, desc));
+
+        /* stop the disk */
+        rb->ata_spindown(0);
+
+        DEBUGF("result:\n\tname:\t%s\n\tindex:\t%ld\n\toffset:\t%lu\n\tsize:\t%lu\n",
+                result.name, result.index, result.data.offset, result.data.size);
+
+
+        /* make title */
+        title_size = rb->strlen(result.name) + 2;
+        if (title_size > DESC_BUFFER_ADD)
+            title_size = DESC_BUFFER_ADD;
+        desc -= title_size;
+        rb->snprintf(desc, title_size, "%s\n", result.name);
+        desc[title_size-1] = '\n';
+
+        while (!new_search)
         {
-            /* set pointer to the next part */
-            ptr += next;
-            lines++;
+            ret = dict_display(desc);
+            rb->button_clear_queue();
+            switch (ret)
+            {
+                case DICT_NEW:
+                    new_search = true;
+                    continue;
+                case DICT_MENU:
+                    dict_menu();
+                    continue;
+                case DICT_ERROR:
+                    return PLUGIN_ERROR;
+                case DICT_USB_CONNECTED:
+                    return PLUGIN_USB_CONNECTED;
+                default:
+                    return PLUGIN_OK;
+            }
         }
-        else
-        {
-            break;
-        }
+
+        dict_free(desc_size);
     }
+}
 
-#ifdef HAVE_LCD_BITMAP
-    rb->lcd_update();
-#endif
+enum plugin_status plugin_start(struct plugin_api* api, void* file)
+{
+    enum plugin_status ret;
 
-    /* wait for keypress */
-    while(rb->button_get(true) != LP_QUIT)
+    rb = api;
+
+    switch (dict_init(file))
     {
-        /* do nothing */
-        /* maybe define some keys for navigation here someday. */
+        case DICT_ERROR:
+            return PLUGIN_ERROR;
+        case DICT_USB_CONNECTED:
+            return PLUGIN_USB_CONNECTED;
+        default:
+            break;
     }
 
-    rb->close(fIndex);
-    rb->close(fData);
-    return PLUGIN_OK;
+    switch (dict_main())
+    {
+        case DICT_OK:
+            ret = PLUGIN_OK;
+            break;
+        case DICT_USB_CONNECTED:
+            ret = PLUGIN_USB_CONNECTED;
+            break;
+        default:
+            ret = PLUGIN_ERROR;
+            break;
+    }
+
+    /* reset disk setting */
+    rb->ata_spindown(rb->global_settings->disk_spindown);
+
+    dict_close();
+
+    return ret;
 }
