From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001
From: cashmere
Date: Fri, 4 Sep 2026 09:05:24 +0200
Subject: initial commit
---
src/skins.c | 1442 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1442 insertions(+)
create mode 100644 src/skins.c
(limited to 'src/skins.c')
diff --git a/src/skins.c b/src/skins.c
new file mode 100644
index 0000000..63c7669
--- /dev/null
+++ b/src/skins.c
@@ -0,0 +1,1442 @@
+/*
+** Copyright (c) 2009 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+**
+** 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.
+**
+** Author contact information:
+** drh@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** Implementation of the Setup page for "skins".
+*/
+#include "config.h"
+#include
+#include "skins.h"
+
+/*
+** SETTING: default-skin width=16
+**
+** If the text value if this setting is the name of a built-in skin
+** then the named skin becomes the default skin for the repository.
+*/
+
+/*
+** An array of available built-in skins.
+**
+** To add new built-in skins:
+**
+** 1. Pick a name for the new skin. (Here we use "xyzzy").
+**
+** 2. Install files skins/xyzzy/css.txt, skins/xyzzy/header.txt,
+** and skins/xyzzy/footer.txt into the source tree.
+**
+** 3. Rerun "tclsh makemake.tcl" in the src/ folder in order to
+** rebuild the makefiles to reference the new CSS, headers, and footers.
+**
+** 4. Make an entry in the following array for the new skin.
+*/
+static struct BuiltinSkin {
+ const char *zDesc; /* Description of this skin */
+ const char *zLabel; /* The directory under skins/ holding this skin */
+ char *zSQL; /* Filled in at run-time with SQL to insert this skin */
+} aBuiltinSkin[] = {
+ { "Copland", "copland", 0 },
+ { "Default", "default", 0 },
+ { "Ardoise", "ardoise", 0 },
+ { "Black & White", "black_and_white", 0 },
+ { "Blitz", "blitz", 0 },
+ { "Dark Mode", "darkmode", 0 },
+ { "Eagle", "eagle", 0 },
+ { "Étienne", "etienne", 0 },
+ { "Khaki", "khaki", 0 },
+ { "Original", "original", 0 },
+ { "Plain Gray", "plain_gray", 0 },
+ { "Xekri", "xekri", 0 },
+};
+
+/*
+** A skin consists of five "files" named here:
+*/
+static const char *const azSkinFile[] = {
+ "css", "header", "footer", "details", "js"
+};
+
+/*
+** Alternative skins can be specified in the CGI script or by options
+** on the "http", "ui", and "server" commands. The alternative skin
+** name must be one of the aBuiltinSkin[].zLabel names. If there is
+** a match, that alternative is used.
+**
+** The following static variable holds the name of the alternative skin,
+** or NULL if the skin should be as configured.
+*/
+static struct BuiltinSkin *pAltSkin = 0;
+static char *zAltSkinDir = 0;
+static int iDraftSkin = 0;
+/*
+** Used by skin_use_alternative() to store the current skin rank skin
+** so that the /skins page can, if warranted, warn the user that skin
+** changes won't have any effect.
+*/
+static int nSkinRank = 6;
+
+/*
+** How the specific skin being used was chosen
+*/
+#if INTERFACE
+#define SKIN_FROM_DRAFT 0 /* The "draftN" prefix on the PATH_INFO */
+#define SKIN_FROM_CMDLINE 1 /* --skin option to server command-line */
+#define SKIN_FROM_CGI 2 /* skin: parameter in CGI script */
+#define SKIN_FROM_QPARAM 3 /* skin= query parameter */
+#define SKIN_FROM_COOKIE 4 /* skin= from fossil_display_settings cookie*/
+#define SKIN_FROM_SETTING 5 /* Built-in named by "default-skin" setting */
+#define SKIN_FROM_CUSTOM 6 /* Skin values in CONFIG table */
+#define SKIN_FROM_DEFAULT 7 /* The built-in named "default" */
+#define SKIN_FROM_UNKNOWN 8 /* Do not yet know which skin to use */
+#endif /* INTERFACE */
+static int iSkinSource = SKIN_FROM_UNKNOWN;
+
+
+/*
+** Skin details are a set of key/value pairs that define display
+** attributes of the skin that cannot be easily specified using CSS
+** or that need to be known on the server-side.
+**
+** The following array holds the value for all known skin details.
+*/
+static struct SkinDetail {
+ const char *zName; /* Name of the detail */
+ const char *zValue; /* Value of the detail */
+} aSkinDetail[] = {
+ { "pikchr-background", "" },
+ { "pikchr-fontscale", "" },
+ { "pikchr-foreground", "" },
+ { "pikchr-scale", "" },
+ { "timeline-arrowheads", "1" },
+ { "timeline-circle-nodes", "0" },
+ { "timeline-color-graph-lines", "0" },
+ { "white-foreground", "0" },
+};
+
+/*
+** Invoke this routine to set the alternative skin. Return NULL if the
+** alternative was successfully installed. Return a string listing all
+** available skins if zName does not match an available skin. Memory
+** for the returned string comes from fossil_malloc() and should be freed
+** by the caller.
+**
+** If the alternative skin name contains one or more '/' characters, then
+** it is assumed to be a directory on disk that holds override css.txt,
+** footer.txt, and header.txt. This mode can be used for interactive
+** development of new skins.
+**
+** The 2nd parameter is a ranking of how important this alternative
+** skin declaration is, and lower values trump higher ones. If a call
+** to this function passes a higher-valued rank than a previous call,
+** the subsequent call becomes a no-op. Only calls with the same or
+** lower rank (i.e. higher priority) will overwrite a previous
+** setting. This approach is used because the CGI/server-time
+** initialization happens in an order which is incompatible with our
+** preferred ranking, making it otherwise more invasive to tell the
+** internals "the --skin flag ranks higher than a URL parameter" (the
+** former gets initialized before both URL parameters and the /draft
+** path determination).
+**
+** The rankings were initially defined in
+** https://fossil-scm.org/forum/forumpost/caf8c9a8bb
+** but where subsequently revised:
+**
+** 0) A skin name matching the glob pattern "draft[1-9]" at the start of
+** the PATH_INFO.
+**
+** 1) The --skin flag for commands like "fossil ui", "fossil server", or
+** "fossil http", or the "skin:" CGI config setting.
+**
+** 2) The "skin" display setting cookie or URL argument, in that
+** order. If the "skin" URL argument is provided and refers to a legal
+** skin then that will update the display cookie. If the skin name is
+** illegal it is silently ignored.
+**
+** 3) The built-in skin identified by the "default-skin" setting, if such
+** a setting exists and matches one of the built-in skin names.
+**
+** 4) Skin properties (settings "css", "details", "footer", "header",
+** and "js") from the CONFIG db table
+**
+** 5) The built-in skin named "copland"
+**
+** The iSource integer privides additional detail about where the skin
+**
+** As a special case, a NULL or empty name resets zAltSkinDir and
+** pAltSkin to 0 to indicate that the current config-side skin should
+** be used (rank 3, above), then returns 0.
+*/
+char *skin_use_alternative(const char *zName, int rank, int iSource){
+ int i;
+ Blob err = BLOB_INITIALIZER;
+ if(rank > nSkinRank) return 0;
+ nSkinRank = rank;
+ if( zName && 1==rank && strchr(zName, '/')!=0 ){
+ zAltSkinDir = fossil_strdup(zName);
+ iSkinSource = iSource;
+ return 0;
+ }
+ if( zName && sqlite3_strglob("draft[1-9]", zName)==0 ){
+ skin_use_draft(zName[5] - '0');
+ iSkinSource = iSource;
+ return 0;
+ }
+ if(!zName || !*zName){
+ pAltSkin = 0;
+ zAltSkinDir = 0;
+ return 0;
+ }
+ if( fossil_strcmp(zName, "custom")==0 ){
+ pAltSkin = 0;
+ zAltSkinDir = 0;
+ iSkinSource = iSource;
+ return 0;
+ }
+ for(i=0; izLabel, zWhat);
+ zOut = builtin_text(z);
+ fossil_free(z);
+ }else{
+ zOut = db_get(zWhat, 0);
+ if( zOut==0 ){
+ z = mprintf("skins/copland/%s.txt", zWhat);
+ zOut = builtin_text(z);
+ fossil_free(z);
+ }else if( iSkinSource==SKIN_FROM_DEFAULT ){
+ iSkinSource = SKIN_FROM_CUSTOM;
+ }
+ }
+ return zOut;
+}
+
+/*
+** Return the command-line option used to set the skin, or return NULL
+** if the default skin is being used.
+*/
+const char *skin_in_use(void){
+ if( zAltSkinDir ) return zAltSkinDir;
+ if( pAltSkin ) return pAltSkin->zLabel;
+ return 0;
+}
+
+/*
+** Return a pointer to a SkinDetail element. Return 0 if not found.
+*/
+static struct SkinDetail *skin_detail_find(const char *zName){
+ int lwr = 0;
+ int upr = count(aSkinDetail);
+ while( upr>=lwr ){
+ int mid = (upr+lwr)/2;
+ int c = fossil_strcmp(aSkinDetail[mid].zName, zName);
+ if( c==0 ) return &aSkinDetail[mid];
+ if( c<0 ){
+ lwr = mid+1;
+ }else{
+ upr = mid-1;
+ }
+ }
+ return 0;
+}
+
+/* Initialize the aSkinDetail array using the text in the details.txt
+** file.
+*/
+static void skin_detail_initialize(void){
+ static int isInit = 0;
+ char *zDetail;
+ Blob detail, line, key, value;
+ if( isInit ) return;
+ isInit = 1;
+ zDetail = (char*)skin_get("details");
+ if( zDetail==0 ) return;
+ zDetail = fossil_strdup(zDetail);
+ blob_init(&detail, zDetail, -1);
+ while( blob_line(&detail, &line) ){
+ char *zKey;
+ int nKey;
+ struct SkinDetail *pDetail;
+ if( !blob_token(&line, &key) ) continue;
+ zKey = blob_buffer(&key);
+ if( zKey[0]=='#' ) continue;
+ nKey = blob_size(&key);
+ if( nKey<2 ) continue;
+ if( zKey[nKey-1]!=':' ) continue;
+ zKey[nKey-1] = 0;
+ pDetail = skin_detail_find(zKey);
+ if( pDetail==0 ) continue;
+ if( !blob_token(&line, &value) ) continue;
+ pDetail->zValue = fossil_strdup(blob_str(&value));
+ }
+ blob_reset(&detail);
+ fossil_free(zDetail);
+}
+
+/*
+** Return a skin detail setting
+*/
+const char *skin_detail(const char *zName){
+ struct SkinDetail *pDetail;
+ skin_detail_initialize();
+ pDetail = skin_detail_find(zName);
+ if( pDetail==0 ) fossil_fatal("no such skin detail: %s", zName);
+ return pDetail->zValue;
+}
+int skin_detail_boolean(const char *zName){
+ return !is_false(skin_detail(zName));
+}
+
+/*
+** Hash function for computing a skin id.
+*/
+static unsigned int skin_hash(unsigned int h, const char *z){
+ if( z==0 ) return h;
+ while( z[0] ){
+ h = (h<<11) ^ (h<<1) ^ (h>>3) ^ z[0];
+ z++;
+ }
+ return h;
+}
+
+/*
+** Return an identifier that is (probably) different for every skin
+** but that is (probably) the same if the skin is unchanged. This
+** identifier can be attached to resource URLs to force reloading when
+** the resources change but allow the resources to be read from cache
+** as long as they are unchanged.
+**
+** The zResource argument is the name of a CONFIG setting that
+** defines the resource. Examples: "css", "logo-image".
+*/
+unsigned int skin_id(const char *zResource){
+ unsigned int h = 0;
+ if( zAltSkinDir ){
+ h = skin_hash(0, zAltSkinDir);
+ }else if( pAltSkin ){
+ h = skin_hash(0, pAltSkin->zLabel);
+ }else{
+ char *zMTime = db_get_mtime(zResource, 0, 0);
+ h = skin_hash(0, zMTime);
+ fossil_free(zMTime);
+ }
+
+ /* Change the ID every time Fossil is recompiled */
+ h = skin_hash(h, fossil_exe_id());
+ return h;
+}
+
+/*
+** For a skin named zSkinName, compute the name of the CONFIG table
+** entry where that skin is stored and return it.
+**
+** Return NULL if zSkinName is NULL or an empty string.
+**
+** If ifExists is true, and the named skin does not exist, return NULL.
+*/
+static char *skinVarName(const char *zSkinName, int ifExists){
+ char *z;
+ if( zSkinName==0 || zSkinName[0]==0 ) return 0;
+ z = mprintf("skin:%s", zSkinName);
+ if( ifExists && !db_exists("SELECT 1 FROM config WHERE name=%Q", z) ){
+ free(z);
+ z = 0;
+ }
+ return z;
+}
+
+/*
+** Return true if there exists a skin name "zSkinName".
+*/
+static int skinExists(const char *zSkinName){
+ int i;
+ if( zSkinName==0 ) return 0;
+ for(i=0; iThere is already another skin
+ @ named "%h(zNewName)". Choose a different name.
+ }
+ @
+ style_finish_page();
+ return 1;
+ }
+ db_unprotect(PROTECT_CONFIG);
+ db_multi_exec(
+ "UPDATE config SET name='skin:%q' WHERE name='skin:%q';",
+ zNewName, zOldName
+ );
+ db_protect_pop();
+ return 0;
+}
+
+/*
+** Respond to a Save button press. Return TRUE if a dialog was painted.
+** Return FALSE to continue with the main Skins page.
+*/
+static int skinSave(const char *zCurrent){
+ const char *zNewName;
+ int ex = 0;
+ if( P("save")==0 ) return 0;
+ zNewName = P("svname");
+ if( zNewName && zNewName[0]!=0 ){
+ }
+ if( zNewName==0 || zNewName[0]==0 || (ex = skinExists(zNewName))!=0 ){
+ if( zNewName==0 ) zNewName = "";
+ style_set_current_feature("skins");
+ style_header("Save Current Skin");
+ if( ex ){
+ @
There is already another skin
+ @ named "%h(zNewName)". Choose a different name.
Deletion of a custom skin is a permanent action that cannot
+ @ be undone. Please confirm that this is what you want to do:
+ @
+ @
+ @
+ login_insert_csrf_secret();
+ @
+ style_finish_page();
+ db_end_transaction(1);
+ return;
+ }
+ if( P("del2")!=0 ){
+ db_unprotect(PROTECT_CONFIG);
+ if( fossil_strcmp(P("sn"),"custom")==0 ){
+ db_multi_exec("DELETE FROM config WHERE name IN"
+ "('css','details','footer','header','js')");
+ }else if( (zName = skinVarName(P("sn"), 1))!=0 ){
+ db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
+ }
+ db_protect_pop();
+ }
+ if( P("draftdel")!=0 ){
+ const char *zDraft = P("name");
+ if( sqlite3_strglob("draft[1-9]",zDraft)==0 ){
+ db_unprotect(PROTECT_CONFIG);
+ db_multi_exec("DELETE FROM config WHERE name GLOB '%q-*'", zDraft);
+ db_protect_pop();
+ }
+ }
+ if( P("editdraft")!=0 ){
+ db_end_transaction(0);
+ cgi_redirectf("%R/setup_skin");
+ return;
+ }
+ if( skinRename() || skinSave(zCurrent) ){
+ db_end_transaction(0);
+ return;
+ }
+
+ if( P("setdflt") && (z = P("bisl"))!=0 ){
+ if( z[0] ){
+ db_set("default-skin", z, 0);
+ }else{
+ db_unset("default-skin", 0);
+ }
+ db_end_transaction(0);
+ cgi_redirectf("%R/setup_skin_admin");
+ return;
+ }
+
+ /* The user pressed one of the "Install" buttons. */
+ if( P("load") && (z = P("sn"))!=0 && z[0] ){
+ int seen = 0;
+
+ /* Check to see if the current skin is already saved. If it is, there
+ ** is no need to create a backup */
+ hasCustom = skin_exists_custom();
+ if( hasCustom ){
+ zCurrent = getSkin(0);
+ for(i=0; i='1' && z[0]<='9' && z[1]==0 ){
+ skin_publish(z[0]-'0');
+ seen = 1;
+ }
+ for(i=0; seen==0 && i%h(zErr)
+ }
+ @
+ @
Built-in Skins:
+ for(i=0; i
%d(i+1).
%h(z)
+ @
+ }
+ if( zOverride ){
+ @
+ @
Note: Built-in skin "%h(zOverride)" is currently being used because of
+ switch( iSkinSource ){
+ case SKIN_FROM_CMDLINE:
+ @ the --skin command-line option.
+ break;
+ case SKIN_FROM_CGI:
+ @ the "skin:" option on CGI script.
+ break;
+ case SKIN_FROM_QPARAM:
+ @ the "skin=NAME" query parameter.
+ break;
+ case SKIN_FROM_COOKIE:
+ @ the "skin" value of the
+ @ fossil_display_setting cookie.
+ break;
+ case SKIN_FROM_SETTING:
+ @ the "default-skin" setting.
+ break;
+ default:
+ @ reasons unknown. (Fix me!)
+ break;
+ }
+ @
+ }
+ i++;
+ @
Custom skin:
+ @
%d(i).
+ if( hasCustom ){
+ @
Custom
+ }else{
+ @
(None)
+ }
+ @
+ @
+ db_prepare(&q,
+ "SELECT substr(name, 6) FROM config"
+ " WHERE name GLOB 'skin:*'"
+ " ORDER BY name"
+ );
+ once = 1;
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zN = db_column_text(&q, 0);
+ i++;
+ if( once ){
+ once = 0;
+ @
Backups of past custom skins:
+ }
+ @
%d(i).
%h(zN)
+ @
+ }
+ db_finalize(&q);
+ db_prepare(&q,
+ "SELECT DISTINCT substr(name, 1, 6) FROM config"
+ " WHERE name GLOB 'draft[1-9]-*'"
+ " ORDER BY name"
+ );
+ once = 1;
+ while( db_step(&q)==SQLITE_ROW ){
+ const char *zN = db_column_text(&q, 0);
+ i++;
+ if( once ){
+ once = 0;
+ @
Draft skins:
+ }
+ @
%d(i).
%h(zN)
+ @
+ }
+ db_finalize(&q);
+
+ @
+ style_finish_page();
+ db_end_transaction(0);
+}
+
+/*
+** Generate HTML for a