Unexpected problem with tag pills on program startup

I’ve tested it in both the latest 5.3.7 pre-release and 5.3.6 , and it’s confirmed that this issue occurs. However, I’m not sure if this is a core issue or an operational issue on my part, and if it is a core issue, it may need to be fixed before the 5.3.7 release.

I am using windows and you need to first set your computer system to dark mode.

Firstly create two entries with the following code for each. Then save the offline version of TiddlyWiki. refresh again and the problem appears in the image below.

titile: apply-dark-or-light-mode
tags: $:/tags/StartupAction/Browser

<$let apply-mode={{dark-or-light-mode}} >
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
title: dark-or-light-mode

<%if [{$:/info/darkmode}match[yes]] %>
$:/palettes/ContrastDark
<%else%>
$:/palettes/LetItSnow
<%endif%>

I am not sure if this is my problem or TiddlyWiki’s problem. Because the values for the colour scheme entries can be set at startup, it doesn’t look like my problem.

Well, it looks like my problem.

At startup, the other entries are not loaded yet and the corresponding values cannot be read. If it’s the following code, it works.

<$action-setfield $tiddler="$:/palette" $value={{{ [{$:/info/darkmode}match[yes]then[$:/palettes/ContrastDark]else[$:/palettes/Vanilla]] }}}/>

So the problem is resolved?

My problem is solved. But I don’t think it’s handled well in the core regarding tag pills. But I can’t read the syntax in there either. Or can you explain why this is happening?

The problem is not due to a startup timing issue (i.e, the availability of the $:/info/darkmode or apply-dark-or-light-mode tiddlers). Rather, it is a basic syntax problem with your usage:

In your apply-light-or-dark-mode startup tiddler, you use <$let apply-mode={{dark-or-light-mode}}> to retrieve the desired palette name, where the contents of the dark-or-light-mode tiddler uses the %if...%else%...%endif% conditional syntax to determine the palette name.

The problem is that your $let assignment simply transcludes the conditional syntax into the apply-mode variable… it does NOT evaluate that syntax. In addition, the subsequent reference to <<apply-mode>> also does not evaluate the syntax. As a result, the contents of $:/palette are set to the literal %if...%else%...%endif% syntax retrieved from dark-or-light-mode, and not the desired palette name as you had intended.

Then, when the tag-pill-inner macro (part of the <<tag>> macro) is invoked to display tags in tiddlers, it tries to get the tag pill color value using the current $:/palette value (which, as noted above, does not contain an actual palette name). Thus, the tag-pill-inner parameters that are supposed to transclude index values from the palette (e.g., {{$:/palettes/ContrastDark##tag-background}}) are corrupted because the $:/palette tiddler content isn’t a valid palette tiddler name.

Then, there is also a subtle issue with the “broken” output that you are seeing, because when the corrupted tag-pill-inner parameters are displayed, the $:/palette tiddler’s value (i.e., the conditional syntax) is rendered, so it appears to show the desired palette tiddler title.

As you’ve discovered, the correct way to retrieve the desired palette name is to use “filtered transclusion” to apply the conditional logic, which DOES get evaluated to resolve to a palette name. Thus, in your apply-light-or-dark-mode tiddler you can write:

<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then[$:/palettes/ContrastDark]else[$:/palettes/Vanilla]] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>

You can also make this more configurable by creating two separate tiddlers (e.g., “light-mode-palette” and “dark-mode-palette”) whose contents each specify their desired palette name. Then, in your apply-light-or-dark-mode tiddler you can write:

<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{dark-mode-palette}else{light-mode-palette}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>

You can even go just a little bit further and create an interface tiddler (e.g., “ChoosePalettes”) for interactively selecting the desired palettes, like this:

\procedure selectpalette(tid)
<<tid>>:
<$select tiddler=<<tid>>>
   <$list filter="[all[tiddler+shadows]prefix[$:/palettes]]">
      <option><<currentTiddler>></option>
   </$list>
</$select>
\end
<<selectpalette light-mode-palette>>
<<selectpalette dark-mode-palette>>

enjoy,
-e

1 Like

Thank you so much for the detailed explanation, it was just what I needed and I always learn so much from your answers.

Here’s an updated version of my solution that keeps everything contained in just one tiddler:

\procedure selectpalette(f)
<<f>>:
<$select field=<<f>>>
   <$list filter="[all[tiddler+shadows]prefix[$:/palettes]color-scheme<f>]">
      <option><<currentTiddler>></option>
   </$list>
</$select>
\end

\procedure setpalette()
<$tiddler tiddler="apply-light-or-dark-mode">
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{!!dark}else{!!light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
\end

<$eventcatcher $change=<<setpalette>>>
<<selectpalette light>> <<selectpalette dark>>
</$eventcatcher>
<<setpalette>>

Notes:

  • selectpalette(f) shows a droplist interface for choosing a palette.
    • The f parameter is either “light” or “dark”, and indicates the field name to be set by the selection.
    • The f parameter is also used in the filter that lists the palettes, so that it only shows palettes whose color-scheme field matches either “light” or “dark”
    • The indicated field name is stored directly in the apply-light-or-dark-mode tiddler to avoid creating any extra tiddlers.
  • setpalette() applies the chosen light or dark palette.
  • The main body of the tiddler displays two droplists, one for light palettes, and one for dark palettes
    • These droplists are enclosed in an $eventcatcher widget, so that whenever you change a selected palette, <<setpalette> is invoked to immediately apply that change.
  • The main body also directly invokes <<setpalette> at startup because the tiddler is tagged with $:/tags/StartupAction/Browser.

Also note that when processed at startup, the droplist interface elements are effectively ignored, since startup tiddlers are not rendered, and when displayed as a regular tiddler in the StoryRiver, the direct <<setpalette>> does nothing since $action widgets are not invoked without a corresponding $button or $eventcatcher widget.

enjoy,
-e

\procedure selectpalette(t,f)
<<f>>:
<$select tiddler=<<t>> index=<<f>> style.width="80%">
  <optgroup label={{{ [<f>titlecase[]] }}}>
    <$list filter="[all[tiddlers+shadows]tag[$:/tags/Palette]color-scheme<f>sort[]]">
      <option value=<<currentTiddler>>><$view field="name"/> -- <$view field="description"/></option>
    </$list>
  </optgroup>
  <optgroup label="No Color Scheme or Other Scheme">
   <$list filter="[all[tiddlers+shadows]tag[$:/tags/Palette]!has[color-scheme]sort[]]">
      <option value=<<currentTiddler>>><$view field="name"/> -- <$view field="description"/></option>
   </$list>
  </optgroup>
</$select>
\end

\procedure setpalette()
<$tiddler tiddler="apply-light-or-dark-mode">
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{light-or-dark-mode-palette##dark}else{light-or-dark-mode-palette##light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
</$tiddler>
\end

<$eventcatcher $change=<<setpalette>>>
<<selectpalette "light-or-dark-mode-palette" "light">><br>
<<selectpalette "light-or-dark-mode-palette" "dark">>
</$eventcatcher>

<<setpalette>>

* [[$:/palette]]
** {{$:/palette}}
* [[light-or-dark-mode-palette]]
** light-mode {{light-or-dark-mode-palette##light}}
** dark-mode {{light-or-dark-mode-palette##dark}}

This is a modified version. There are several changes. The first change is that I like to store modifiable values in data entries so that I can avoid creating too many fields. Fields are better left for those generic entries to use. So I’ve added an entry light-or-dark-mode-palette and any changes to the modification will show up on that entry.

The other change is a filter improvement, fixing a word error in the original, which was tiddlers, not tiddler, which would only filter out system entries, not all of them. Secondly use tag label filters instead of prefix filters. This is because some users create their own colour palettes and may not use the same prefix headings.

One more minor change is to use name and description instead of title for display. And add a width style to select.

If it needs to be handled based on judgement at startup, then you need to add the tag with $:/tags/StartupAction/Browser.


There are also plugins in the community that provide similar functionality. For example, shiraz, the same can be referred to. But the above code doesn’t seem to be suitable for wrapping into a button because its judging the light and dark modes instead of controlling it manually. In fact, my idea at the beginning was to not require user action at all, like most note-taking software, just set it once, no need to manually refresh the process yourself. But this may require additional js operations, so I had to put it aside for now.Anyway,thanks again.


Updated.

Added filter expression without colour-scheme field. Also added sorting, which looked confusing without it.

Since you prefer to use a separate tiddler to store the light/dark palette selections, I recommend defining that tiddler title once, at the top of the code, rather than embedding it as literal text in many places. This makes it easier to change later on and makes the code easier to read. I also suggest using a tiddler title such as $:/config/light-or-dark-mode so it won’t appear in the normal list of tiddlers and is consistent with the way other TiddlyWiki configuration settings are stored.

Also, in selectpalette() there’s a few coding “tips” you can use to make it easier to read:

  • Use a variable (e.g., palettes) to get the list of palette tiddlers once. While this isn’t strictly needed, it can make the code somewhat more efficient, especially if there are thousands of tiddlers to sift through.
  • Define a local macro (e.g., opt()) to use as an abbreviation for the <option>...</option> syntax that is repeated when populating the $select list.
  • Enclose the “No Color Scheme” optgroup within conditional syntax so it only appears in the list when there actually are palettes that don’t have a color-scheme.

So, your code can be written like this:

\define config() $:/config/light-or-dark-mode

\procedure selectpalette(f)
\define opt() <option value=<<currentTiddler>>>{{!!name}} -- {{!!description}}</option>
<$set name=palettes filter="[all[tiddlers+shadows]tag[$:/tags/Palette]]">
<<f>>:
<$select tiddler=<<config>> index=<<f>> style.width="80%">
  <optgroup label={{{ [<f>titlecase[]] }}}>
    <$list filter="[enlist<palettes>color-scheme<f>sort[]]"><<opt>></$list>
  </optgroup>
  <%if [enlist<palettes>!has[color-scheme]] %>
  <optgroup label="No Color Scheme">
    <$list filter="[enlist<palettes>!has[color-scheme]sort[]]"><<opt>></$list>
  </optgroup>
  <%endif%>
</$select>
\end

\procedure setpalette()
<$tiddler tiddler=<<config>>>
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{##dark}else{##light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
</$tiddler>
\end

<$eventcatcher $change=<<setpalette>>>
<<selectpalette light>><br>
<<selectpalette dark>>
</$eventcatcher>

<<setpalette>>

* [[$:/palette]]
** {{$:/palette}}
* <$link to=<<config>>/>
** light-mode {{{ [<config>getindex[light]] }}}
** dark-mode {{{ [<config>getindex[dark]] }}}

enjoy,
-e

Thanks, learned a lot of tips. The set widget in the code above, though, doesn’t close.

I often forget to write closure tags or write them incorrectly sometimes too, which also makes me think that maybe the community needs a similar plugin to help with this. Because what makes IDE software like vscode so popular is that users don’t have to think about this on purpose, as well as auto-checking and auto-organising the code without having to manually tweak it individually.

When a macro, procedure or tiddler ends, all widgets that occur at the top-level (i.e., not nested inside another widget) are automatically closed. Thus, as long as the <$set...> widget occurs at the top-level, we can safely omit the matching </$set>.

For example, the setpalette() procedure could be written like this:

\procedure setpalette()
<$tiddler tiddler=<<config>>>
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{##dark}else{##light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
\end

While it may look like a coding error, it works just fine, and I do this quite often, especially if there are a whole bunch of $set (or $vars or $let) widgets at the start of the macro, procedure, or tiddler. When done consistently, it can save a lot of excess code.

-e

2 Likes

This is the first time I’ve learnt about it. There are still so many things I don’t know about TiddlyWiki, even though I’ve been exposed to it for three years now. Still, out of programming habit, I usually add closure tags.

Hi I just want to point out here that Eric likes to avoid closing tags such as the above $let and $tiddler widgets, whilst this works if understood well it can create the problems if you return later and make changes, without reviewing such unclosed tags/widgets.

  • I am not saying dont do this, just be aware of it or close them.
\procedure setpalette()
<$tiddler tiddler=<<config>>>
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{##dark}else{##light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
</$tiddler>
\end
  • Note the order of closure needs to be nested.

In many instances, consistently omitting the top-level closing tags can help avoid problems if you make changes later on. For example, using the setpalette() code posted previously, suppose you DO include the top-level closing tags and then you add another top-level $let widget at the start of the procedure; then, as you’ve noted, the “order of closure needs to be [correctly] nested”, like this:

\procedure setpalette()
<$let config="$:/config/light-or-dark-mode">
<$tiddler tiddler=<<config>>>
<$let apply-mode={{{ [{$:/info/darkmode}match[yes]then{##dark}else{##light}] }}}>
<$action-setfield $tiddler="$:/palette" $value=<<apply-mode>> $timestamp="no"/>
</$let>
</$tiddler>
</$let>
\end

However, if you routinely omit the top-level closing tags, then you can simply add the one line containing the new $let at the start of the procedure, and you are done… without any concern about getting the nested “order of closure” correct.

While it may take a little bit of practice to properly apply this “omit the closing tags” code pattern, once you get used to it, it becomes second nature. Nonetheless, if you are more comfortable with the standard “always use closing tags” code pattern, then by all means do that.

-e