From 4d900bd786b37a2d9a5bdff0d557c667d6d897ba Mon Sep 17 00:00:00 2001 From: Teruaki Kawashima Date: Mon, 12 Oct 2009 13:55:59 +0000 Subject: New plugin theme_remove which offers a way to remove specified theme. See the manual for usage. Flyspray: FS#10187 Author: myself, manual by David Kauffmann git-svn-id: svn://svn.rockbox.org/rockbox/trunk@23130 a1c6a512-1295-4272-9138-f99709370657 --- apps/plugins/CATEGORIES | 1 + apps/plugins/SOURCES | 1 + apps/plugins/rockbox-fonts.config | 73 ++++ apps/plugins/theme_remove.c | 702 ++++++++++++++++++++++++++++++++++++++ apps/plugins/viewers.config | 1 + 5 files changed, 778 insertions(+) create mode 100644 apps/plugins/rockbox-fonts.config create mode 100644 apps/plugins/theme_remove.c (limited to 'apps/plugins') diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES index ae764ca125..6809844efb 100644 --- a/apps/plugins/CATEGORIES +++ b/apps/plugins/CATEGORIES @@ -103,6 +103,7 @@ test_touchscreen,apps test_viewports,apps test_greylib_bitmap_scale,viewers text_editor,apps +theme_remove,viewers vbrfix,viewers video,viewers viewer,viewers diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES index a15564d0be..d89b0b6e5f 100644 --- a/apps/plugins/SOURCES +++ b/apps/plugins/SOURCES @@ -16,6 +16,7 @@ snow.c sort.c stats.c stopwatch.c +theme_remove.c vbrfix.c viewer.c #ifdef HAVE_BACKLIGHT diff --git a/apps/plugins/rockbox-fonts.config b/apps/plugins/rockbox-fonts.config new file mode 100644 index 0000000000..4995d71864 --- /dev/null +++ b/apps/plugins/rockbox-fonts.config @@ -0,0 +1,73 @@ +font: 05-Tiny +font: 06-Tiny +font: 07-Fixed +font: 08-Atadore +font: 08-Fixed +font: 08-Namil +font: 08-Nedore +font: 08-Rockbox-Propfont +font: 08-Rockfont +font: 08-Sazanami-Mincho +font: 08-Schumacher-Clean +font: 09-Fixed +font: 09-Nedore +font: 09-Sazanami-Mincho +font: 10-Adobe-Helvetica +font: 10-Adobe-Helvetica-Bold +font: 10-Artwiz-Snap +font: 10-Fixed +font: 10-Nimbus +font: 10-ProFont +font: 10-Sazanami-Mincho +font: 11-Nimbus +font: 11-ProFont +font: 11-Sazanami-Mincho +font: 12-Adobe-Helvetica +font: 12-Adobe-Helvetica-Bold +font: 12-Fixed-SemiCond +font: 12-Nimbus +font: 12-ProFont +font: 12-Sazanami-Mincho +font: 12-Terminus +font: 13-Fixed +font: 13-Fixed-Bold +font: 13-Fixed-SemiCond +font: 13-Fixed-SemiCond-Bold +font: 13-Nimbus +font: 13-Sazanami-Mincho +font: 14-Adobe-Helvetica-Bold +font: 14-Nimbus +font: 14-Rockbox-Mix +font: 14-Sazanami-Mincho +font: 14-Terminus +font: 14-Terminus-Bold +font: 15-Adobe-Helvetica +font: 15-ProFont +font: 16-Adobe-Helvetica +font: 16-Adobe-Helvetica-Bold +font: 16-GNU-Unifont +font: 16-Jackash +font: 16-Terminus +font: 16-Terminus-Bold +font: 17-ProFont +font: 18-Adobe-Helvetica +font: 18-Adobe-Helvetica-Bold +font: 18-Fixed +font: 18-Fixed-Bold +font: 19-Nimbus +font: 20-Terminus +font: 20-Terminus-Bold +font: 21-Adobe-Helvetica +font: 21-Adobe-Helvetica-Bold +font: 22-ProFont +font: 24-Terminus +font: 24-Terminus-Bold +font: 27-Adobe-Helvetica +font: 27-Adobe-Helvetica-Bold +font: 28-Terminus +font: 28-Terminus-Bold +font: 29-ProFont +font: 32-Terminus +font: 32-Terminus-Bold +font: 35-Adobe-Helvetica +font: 35-Adobe-Helvetica-Bold diff --git a/apps/plugins/theme_remove.c b/apps/plugins/theme_remove.c new file mode 100644 index 0000000000..a2e7bf21a5 --- /dev/null +++ b/apps/plugins/theme_remove.c @@ -0,0 +1,702 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * 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 the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "plugin.h" +#include "lib/configfile.h" + +PLUGIN_HEADER + +/* taken from apps/gui/wps_parser.c */ +#define WPS_DEFAULTCFG WPS_DIR "/rockbox_default.wps" +#define RWPS_DEFAULTCFG WPS_DIR "/rockbox_default.rwps" + +#define CONFIG_FILENAME "theme_remove.cfg" +#define LOG_FILENAME "/theme_remove_log.txt" +#define RB_FONTS_CONFIG VIEWERS_DIR "/rockbox-fonts.config" + +enum remove_option { + ALWAYS_REMOVE, + NEVER_REMOVE, + REMOVE_IF_NOT_USED, + ASK_FOR_REMOVAL, + NUM_REMOVE_OPTION +}; + +struct remove_setting { + const char *name; + const char *prefix, *suffix; + char value[MAX_PATH]; + int option; + int (*func)(struct remove_setting *); + bool used; +}; + +static int remove_wps(struct remove_setting *); +#ifdef HAVE_LCD_BITMAP +static int remove_icons(struct remove_setting *setting); +#endif + +enum remove_settings { +#ifdef HAVE_LCD_BITMAP + REMOVE_FONT, +#endif + REMOVE_WPS, +#ifdef HAVE_REMOTE_LCD + REMOVE_RWPS, +#endif +#if LCD_DEPTH > 1 + REMOVE_BACKDROP, +#endif +#ifdef HAVE_LCD_BITMAP + REMOVE_ICON, + REMOVE_VICON, +#endif +#ifdef HAVE_REMOTE_LCD + REMOVE_RICON, + REMOVE_RVICON, +#endif +#ifdef HAVE_LCD_COLOR + REMOVE_COLOURS, +#endif + NUM_REMOVE_ITEMS +}; + +static bool create_log = true; +static struct remove_setting remove_list[NUM_REMOVE_ITEMS] = { +#ifdef HAVE_LCD_BITMAP + [REMOVE_FONT] = { "font", FONT_DIR "/", ".fnt", "", + ASK_FOR_REMOVAL, NULL, false }, +#endif + [REMOVE_WPS] = { "wps", WPS_DIR "/", ".wps", "", + REMOVE_IF_NOT_USED, remove_wps, false }, +#ifdef HAVE_REMOTE_LCD + [REMOVE_RWPS] = { "rwps", WPS_DIR "/", ".rwps", "", + REMOVE_IF_NOT_USED, remove_wps, false }, +#endif +#if LCD_DEPTH > 1 + [REMOVE_BACKDROP] = { "backdrop", BACKDROP_DIR "/", ".bmp", "", + REMOVE_IF_NOT_USED, NULL, false }, +#endif +#ifdef HAVE_LCD_BITMAP + [REMOVE_ICON] = { "iconset", ICON_DIR "/", ".bmp", "", + ASK_FOR_REMOVAL, NULL, false }, + [REMOVE_VICON] = { "viewers iconset", ICON_DIR "/", ".bmp", "", + ASK_FOR_REMOVAL, remove_icons, false }, +#endif +#ifdef HAVE_REMOTE_LCD + [REMOVE_RICON] = { "remote iconset", ICON_DIR "/", ".bmp", "", + ASK_FOR_REMOVAL, NULL, false }, + [REMOVE_RVICON] = { "remote viewers iconset", ICON_DIR "/", ".bmp", "", + ASK_FOR_REMOVAL, NULL, false }, +#endif +#ifdef HAVE_LCD_COLOR + [REMOVE_COLOURS] = { "filetype colours", THEME_DIR "/", ".colours", "", + ASK_FOR_REMOVAL, NULL, false }, +#endif +}; +static char *option_names[NUM_REMOVE_OPTION] = { + "always", "never", "not used", "ask", +}; +static struct configdata config[] = { +#ifdef HAVE_LCD_BITMAP + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_FONT].option }, + "remove font", option_names }, +#endif + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_WPS].option }, + "remove wps", option_names }, +#ifdef HAVE_REMOTE_LCD + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_RWPS].option }, + "remove rwps", option_names }, +#endif +#if LCD_DEPTH > 1 + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_BACKDROP].option }, + "remove backdrop", option_names }, +#endif +#ifdef HAVE_LCD_BITMAP + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_ICON].option }, + "remove iconset", option_names }, + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_VICON].option }, + "remove viconset", option_names }, +#endif +#ifdef HAVE_REMOTE_LCD + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_RICON].option }, + "remove riconset", option_names }, + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_RVICON].option }, + "remove rviconset", option_names }, +#endif +#ifdef HAVE_LCD_COLOR + { TYPE_INT, 0, NUM_REMOVE_OPTION, + { .int_p = &remove_list[REMOVE_COLOURS].option }, + "remove colours", option_names }, +#endif + {TYPE_BOOL, 0, 1, { .bool_p = &create_log }, + "create log", NULL}, +}; +static const int nb_config = sizeof(config)/sizeof(*config); +static char themefile[MAX_PATH]; +static int log_fd = -1; + +static int show_mess(const char *text, const char *file) +{ + static char buf[MAX_PATH*2]; + + if (file) + rb->snprintf(buf, sizeof(buf), "%s: %s", text, file); + else + rb->snprintf(buf, sizeof(buf), "%s", text); + + DEBUGF("%s\n", buf); + if (log_fd >= 0) + rb->fdprintf(log_fd, "%s\n", buf); + + rb->splash(0, buf); + rb->sleep(HZ/4); + + return 0; +} + +/* set full path of file. */ +static void set_file_name(char *buf, const char*file, + struct remove_setting *setting) +{ + int len1, len2; + if (rb->strncasecmp(file, setting->prefix, rb->strlen(setting->prefix))) + rb->snprintf(buf, MAX_PATH, "%s%s", setting->prefix, file); + else + rb->strlcpy(buf, file, MAX_PATH); + len1 = rb->strlen(buf); + len2 = rb->strlen(setting->suffix); + if (rb->strcasecmp(buf+len1-len2, setting->suffix)) + rb->strlcpy(&buf[len1], setting->suffix, MAX_PATH-len1); +} + +/* taken from apps/onplay.c */ +/* helper function to remove a non-empty directory */ +static int remove_dir(char* dirname, int len) +{ + int result = 0; + DIR* dir; + int dirlen = rb->strlen(dirname); + + dir = rb->opendir(dirname); + if (!dir) + return -1; /* open error */ + + while (true) + { + struct dirent* entry; + /* walk through the directory content */ + entry = rb->readdir(dir); + if (!entry) + break; + + dirname[dirlen] ='\0'; + + /* append name to current directory */ + rb->snprintf(dirname+dirlen, len-dirlen, "/%s", entry->d_name); + if (entry->attribute & ATTR_DIRECTORY) + { + /* remove a subdirectory */ + if (!rb->strcmp((char *)entry->d_name, ".") || + !rb->strcmp((char *)entry->d_name, "..")) + continue; /* skip these */ + + result = remove_dir(dirname, len); /* recursion */ + if (result) + break; + } + else + { + /* remove a file */ + result = rb->remove(dirname); + } + if (ACTION_STD_CANCEL == rb->get_action(CONTEXT_STD, TIMEOUT_NOBLOCK)) + { + show_mess("Canceled", NULL); + result = -1; + break; + } + } + rb->closedir(dir); + + if (!result) + { /* remove the now empty directory */ + dirname[dirlen] = '\0'; /* terminate to original length */ + + result = rb->rmdir(dirname); + show_mess("Removed", dirname); + } + + return result; +} + +static int remove_wps(struct remove_setting *setting) +{ + char bmpdir[MAX_PATH]; + char *p; + rb->strcpy(bmpdir, setting->value); + p = rb->strrchr(bmpdir, '.'); + if (p) *p = 0; + if (!rb->dir_exists(bmpdir)) + return 0; + return remove_dir(bmpdir, MAX_PATH); +} + +#ifdef HAVE_LCD_BITMAP +static int remove_icons(struct remove_setting *setting) +{ + char path[MAX_PATH]; + char *p; + rb->strcpy(path, setting->value); + p = rb->strrchr(path, '.'); + rb->strlcpy(p, ".icons", path+MAX_PATH-p); + + if (!rb->file_exists(path)) + { + return 0; + } + if (rb->remove(path)) + { + show_mess("Failed", path); + return 1; + } + show_mess("Removed", path); + return 0; +} +#endif + +#ifdef HAVE_LCD_BITMAP +static char font_file[MAX_PATH]; +#endif +static bool is_deny_file(const char *file) +{ + const char *deny_files[] = { + WPS_DEFAULTCFG, + RWPS_DEFAULTCFG, +#ifdef HAVE_LCD_BITMAP + font_file, +#endif + NULL + }; + const char **p = deny_files; + while ( *p ) + { + if (!rb->strcmp(file, *p)) + return true; + p++; + } + return false; +} + +static void check_whether_used_in_setting(void) +{ + const char *setting_files[] = { +#ifdef HAVE_LCD_BITMAP + rb->global_settings->font_file, +#endif + rb->global_settings->wps_file, +#ifdef HAVE_REMOTE_LCD + rb->global_settings->rwps_file, +#endif +#if LCD_DEPTH > 1 + rb->global_settings->backdrop_file, +#endif +#ifdef HAVE_LCD_BITMAP + rb->global_settings->icon_file, + rb->global_settings->viewers_icon_file, +#endif +#ifdef HAVE_REMOTE_LCD + rb->global_settings->remote_icon_file, + rb->global_settings->remote_viewers_icon_file, +#endif +#ifdef HAVE_LCD_COLOR + rb->global_settings->colors_file, +#endif + }; + char tempfile[MAX_PATH]; + int i; + for (i=0; ivalue[0]) + { + set_file_name(tempfile, setting_files[i], setting); + if (!rb->strcasecmp(tempfile, setting->value)) + setting->used = true; + } + } +} +static void check_whether_used_in_file(const char *cfgfile) +{ + char line[MAX_PATH]; + char settingfile[MAX_PATH]; + char *p; + int fd; + char *name, *value; + int i; + + if (!rb->strcasecmp(themefile, cfgfile)) + return; + fd = rb->open(cfgfile, O_RDONLY); + if (fd < 0) + return; + while (rb->read_line(fd, line, sizeof(line)) > 0) + { + if (!rb->settings_parseline(line, &name, &value)) + continue; + /* remove trailing spaces. */ + p = value+rb->strlen(value)-1; + while (*p == ' ') *p-- = 0; + if (*value == 0 || !rb->strcmp(value, "-")) + continue; + for (i=0; istrcmp(name, setting->name)) + { + if (setting->value[0]) + { + set_file_name(settingfile, value, setting); + if (!rb->strcasecmp(settingfile, setting->value)) + setting->used = true; + } + break; + } + } + } + rb->close(fd); +} +static void check_whether_used(void) +{ + char cfgfile[MAX_PATH]; + DIR *dir; + + check_whether_used_in_setting(); +#ifdef HAVE_LCD_BITMAP + /* mark font files come from rockbox-font.zip as used and don't remove + * them automatically as themes may depend on those fonts. */ + if (remove_list[REMOVE_FONT].option == REMOVE_IF_NOT_USED) + check_whether_used_in_file(RB_FONTS_CONFIG); +#endif + + dir = rb->opendir(THEME_DIR); + if (!dir) + return; /* open error */ + + while (true) + { + struct dirent* entry; + char *p; + int i; + /* walk through the directory content */ + entry = rb->readdir(dir); + if (!entry) + break; + p = rb->strrchr(entry->d_name, '.'); + if (!p || rb->strcmp(p, ".cfg")) + continue; + + rb->snprintf(cfgfile, MAX_PATH, "%s/%s", THEME_DIR, entry->d_name); + check_whether_used_in_file(cfgfile); + /* break the loop if all files need to be checked in the theme + * turned out to be used. */ + for (i = 0; i < NUM_REMOVE_ITEMS; i++) + { + struct remove_setting *setting = &remove_list[i]; + if (setting->option == REMOVE_IF_NOT_USED) + { + if (setting->value[0] && !setting->used) + break; + } + } + if (i == NUM_REMOVE_ITEMS) + break; + } + rb->closedir(dir); +} + +static int remove_file(struct remove_setting *setting) +{ + if (!rb->file_exists(setting->value)) + { + show_mess("Doesn't exist", setting->value); + return 0; + } + if (is_deny_file(setting->value)) + { + show_mess("Denied", setting->value); + return 0; + } + switch (setting->option) + { + case ALWAYS_REMOVE: + break; + case NEVER_REMOVE: + show_mess("Skipped", setting->value); + return 0; + break; + case REMOVE_IF_NOT_USED: + if (setting->used) + { + show_mess("Used", setting->value); + return 0; + } + break; + case ASK_FOR_REMOVAL: + default: + { + const char *message_lines[] = { "Delete?", setting->value }; + const struct text_message text_message = { message_lines, 2 }; + if (rb->gui_syncyesno_run(&text_message, NULL, NULL) != YESNO_YES) + { + show_mess("Skipped", setting->value); + return 0; + } + } + break; + } + if (rb->remove(setting->value)) + { + show_mess("Failed", setting->value); + return -1; + } + if (setting->func && setting->func(setting)) + return -1; + show_mess("Removed", setting->value); + return 1; +} +static int remove_theme(void) +{ + static char line[MAX_PATH]; + int fd; + int i, num_removed = 0; + char *name, *value; + bool needs_to_check_whether_used = false; + + /* initialize for safe */ + for (i=0; iopen(themefile, O_RDONLY); + if (fd < 0) return fd; + while (rb->read_line(fd, line, sizeof(line)) > 0) + { + if (!rb->settings_parseline(line, &name, &value)) + continue; + /* remove trailing spaces. */ + char *p = value+rb->strlen(value)-1; + while (*p == ' ') *p-- = 0; + if (*value == 0 || !rb->strcmp(value, "-")) + continue; + for (i=0; istrcmp(name, setting->name)) + { + set_file_name(setting->value, value, setting); + if(setting->option == REMOVE_IF_NOT_USED) + needs_to_check_whether_used = true; + break; + } + } + } + rb->close(fd); + + if(needs_to_check_whether_used) + check_whether_used(); + + /* now remove file assosiated to the theme. */ + for (i=0; istrncasecmp(themefile, THEME_DIR "/", sizeof(THEME_DIR "/")-1)) + { + show_mess("Skipped", themefile); + } + else if (rb->remove(themefile)) + { + show_mess("Failed", themefile); + return -1; + } + else + { + show_mess("Removed", themefile); + rb->reload_directory(); + num_removed++; + } + return num_removed; +} + +static bool option_changed = false; +static bool option_menu(void) +{ + MENUITEM_STRINGLIST(option_menu, "Remove Options", NULL, + /* same order as remove_list */ +#ifdef HAVE_LCD_BITMAP + "Font", +#endif + "WPS", +#ifdef HAVE_REMOTE_LCD + "Remote WPS", +#endif +#if LCD_DEPTH > 1 + "Backdrop", +#endif +#ifdef HAVE_LCD_BITMAP + "Iconset", "Viewers Iconset", +#endif +#ifdef HAVE_REMOTE_LCD + "Remote Iconset", "Remote Viewers Iconset", +#endif +#ifdef HAVE_LCD_COLOR + "Filetype Colours", +#endif + "Create Log File"); + struct opt_items remove_names[] = { + {"Always Remove", -1}, {"Never Remove", -1}, + {"Remove if not Used", -1}, {"Ask for Removal", -1}, + }; + int selected = 0, result; + + while (1) + { + result = rb->do_menu(&option_menu, &selected, NULL, false); + if (result >= 0 && result < NUM_REMOVE_ITEMS) + { + struct remove_setting *setting = &remove_list[result]; + int prev_option = setting->option; + if (rb->set_option(option_menu_[result], &setting->option, INT, + remove_names, NUM_REMOVE_OPTION, NULL)) + return true; + if (prev_option != setting->option) + option_changed = true; + } + else if (result == NUM_REMOVE_ITEMS) + { + bool prev_value = create_log; + if(rb->set_bool("Create Log File", &create_log)) + return true; + if (prev_value != create_log) + option_changed = true; + } + else if (result == MENU_ATTACHED_USB) + return true; + else + return false; + } + + return false; +} + +enum plugin_status plugin_start(const void* parameter) +{ + static char title[64]; + char *p; + MENUITEM_STRINGLIST(menu, title, NULL, + "Remove Theme", "Remove Options", + "Quit"); + int selected = 0, ret; + bool exit = false; + + if (!parameter) + return PLUGIN_ERROR; + + rb->snprintf(title, sizeof(title), "Remove %s", + rb->strrchr(parameter, '/')+1); + if((p = rb->strrchr(title, '.'))) + *p = 0; + +#ifdef HAVE_LCD_BITMAP + rb->snprintf(font_file, MAX_PATH, FONT_DIR "/%s.fnt", + rb->global_settings->font_file); +#endif + rb->strlcpy(themefile, parameter, MAX_PATH); + if (!rb->file_exists(themefile)) + { + rb->splash(HZ, "File open error!"); + return PLUGIN_ERROR; + } + configfile_load(CONFIG_FILENAME, config, nb_config, 0); + while (!exit) + { + switch (rb->do_menu(&menu, &selected, NULL, false)) + { + case 0: + if(create_log) + { + log_fd = rb->open(LOG_FILENAME, O_WRONLY|O_CREAT|O_APPEND); + if(log_fd >= 0) + rb->fdprintf(log_fd, "---- %s ----\n", title); + else + show_mess("Couldn't open log file.", NULL); + } + ret = remove_theme(); + p = (ret >= 0? "Successfully removed!": "Remove failure"); + show_mess(p, NULL); + if(log_fd >= 0) + { + rb->fdprintf(log_fd, "----------------\n"); + rb->close(log_fd); + log_fd = -1; + } + rb->lcd_clear_display(); + rb->lcd_update(); + rb->splashf(0, "%s %s", p, "Press any key to exit."); + rb->button_clear_queue(); + rb->button_get(true); + exit = true; + break; + case 1: + if (option_menu()) + return PLUGIN_USB_CONNECTED; + break; + case 2: + exit = true; + break; + case MENU_ATTACHED_USB: + return PLUGIN_USB_CONNECTED; + break; + default: + break; + } + } + if(option_changed) + configfile_save(CONFIG_FILENAME, config, nb_config, 0); + return PLUGIN_OK; +} diff --git a/apps/plugins/viewers.config b/apps/plugins/viewers.config index e3b15fe116..205bafb12a 100644 --- a/apps/plugins/viewers.config +++ b/apps/plugins/viewers.config @@ -50,6 +50,7 @@ sna,viewers/zxbox,12 tzx,viewers/zxbox,12 z80,viewers/zxbox,12 ppm,viewers/ppmviewer,2 +cfg,viewers/theme_remove,- *,viewers/properties,- colours,apps/text_editor,11 ssg,games/superdom,- -- cgit v1.2.3