List of last visited tiddlers

It’s already in the TW core!

The $:/core/save/all shadow tiddler contains a definition for saveTiddlerFilter(), which is used to determine which tiddlers are actually retained when saving the file.

This filter already includes “-[prefix[$:/HistoryList]]” which effectively discards all tiddlers that begin with $:/HistoryList.

-e

Oh, I didn’t know that @EricShulman

Thank you very much!
I was worrying about bloating the wikies

Best wishes,
Simon

If you did not change the $:/core/save/all tiddler it should be prefixed already. We did implement that quite some time ago

See: https://tiddlywiki.com/#%24%3A%2Fcore%2Fsave%2Fall

Another approach I have employed is to create a form of logout button, the last thing you do before you leave the wiki after a bit of work. Once you develop this habbit you can place a lot of logic in there to save and maintain anything you want. Of course most times doing this when you start a wiki with wiki start up actions is almost equivalent and with that you dont need to trigger.

  • Thus you allow the history list to be saved and before its cleared save it for later perhaps with a size limit.
  • There are few details you need to address such as the history list will contain tiddlers that have being renamed or deleted.
    • A simple trick is to test that the actual tiddler exists before listing it, and here you could trim those that are older.
  • I actaly find the history list being cleared on reload useful, say once I finish work in one project and start another. If you build a longer term history I suggest keeping the existing mechanisium but collate the longer history in a seperate data tiddler.

It can be done by parsing the historylist like this:

\define title-string() "title": "
\define end-title() "
<$list filter="[{$:/HistoryList}split<title-string>splitbefore<end-title>removesuffix<end-title>reverse[]is[tiddler]]">

</$list>

see https://tiddlytools.com/#TiddlyTools%2FHistory, which adds a “Recently Visited” button to the tiddler ViewToolbar and provides a dropdown to navigate to any previously visited tiddler.

Specifically, check out the definition of the history() macro, which is a filter for getting a useful list of tiddlers visited during the current session:

\define history()
[subfilter{$:/DefaultTiddlers}] [enlist{$:/StoryList!!list}]
[{$:/HistoryList}splitregexp[\n]trim[]trim[,]removeprefix["title": "]removesuffix["]]
+[unique[]!prefix[Draft]search-replace:g[\"],["]]
\end

Note that this filter includes the $:/DefaultTiddlers and the $:/StoryList!!list, as well as parsing the contents of the $:/HistoryList. It then removes duplicates, excludes draft tiddlers (which are added to the $:/HistoryList whenever a tiddler is edited), and finally decodes any quotes that may occur within a tiddler title (and were JSON encoded when added to the $:/HistoryList)

enjoy,
-e

2 Likes

Thanks to all for the excellent discussion. I will study this later today. I’ll post my final solution once I have one.

I still don’t understand the behavior of my previous attempt, but I found if I switch from action-listops to action-setfield widget, then it works as I expect:

<$let lbrace="[" rbrace="]">
<$action-setfield $tiddler="$:/HistoryList" text={{{
  [{$:/HistoryList}jsonindexes[]last[3]]
  :map[{$:/HistoryList}jsonextract<currentTiddler>]
  :and[join[, ]addprefix<lbrace>addsuffix<rbrace>]
 }}}/>
</$let>

I used last[3] to make it easy to test. For real use a much higher number would be useful.

@HistoryBuff, the following might get what you want (I did not test):

  1. Create a global publishFilter variable containing $:/HistoryList. This way the history list will be persisted.
  2. Add a startup tiddler with the let/action-setfield code I shared above. This will prevent unbounded growth of the history list.
  3. Use <$list filter="[{$:/HistoryList}jsonindexes[]] :map[{$:/HistoryList}jsonget<currentTiddler>,[title]]"/> to display the titles in your default tiddler

You may want to add [unique[]!prefix[Draft]] to the filter as @EricShulman demonstrated in his solution.

Ok, so I ended up doing a mix of what @btheado and @EricShulman posted.

I added $:/HistoryList to the publish filter. I then created a startup tiddler with the code from @btheado which is repeated here with a limit of 20.

<$let lbrace="[" rbrace="]">
<$action-setfield $tiddler="$:/HistoryList" text={{{
  [{$:/HistoryList}jsonindexes[]last[20]]
  :map[{$:/HistoryList}jsonextract<currentTiddler>]
  :and[join[, ]addprefix<lbrace>addsuffix<rbrace>]
 }}}/>
</$let>

I then used the code from @EricShulman with the reverse operator added so that the most recent is on top:

\define history()
[subfilter{$:/DefaultTiddlers}] [enlist{$:/StoryList!!list}]
[{$:/HistoryList}splitregexp[\n]trim[]trim[,]removeprefix["title": "]removesuffix["]]
+[reverse[]unique[]!prefix[Draft]!is[missing]search-replace:g[\"],["]]
\end

I then added this code to the tiddler I wanted it displayed in:

<fieldset style="margin: 10px; padding: 5px 5px 5px 10px;">
  <legend style="font-weight: bold; padding: 0 8px 0 8px;">Most Recently Visited</legend>
  <$list filter="[subfilter<history>]">
    <$link><<currentTiddler>></$link><br>
  </$list>
</fieldset>

I changed my mind on having this in my default tiddler and, instead, added to my custom recents tiddler which I have in the sidebar. A screenshot is below showing the final product.

My last question is can the date be added like in the recently edited list? I’m guessing not, at least not without a lot of headache.

Thanks again to everyone.

I’d feel uneasy using this code exactly. It looks like it depends on the json in the history list being formatted exactly the way the navigator widget formats it. However, even the startup tiddler code I provided does not format the json in this way.

In practice it might not ever cause you problems. I’d feel better properly parsing the json using the official json operators rather than depending on the json being formatted just-so. IOW, use this in the latter part of the history macro:

[{$:/HistoryList}jsonindexes[]]
:map[{$:/HistoryList}jsonget<currentTiddler>,[title]]
+[reverse[]unique[]!prefix[Draft]!is[missing]]

rather than

[{$:/HistoryList}splitregexp[\n]trim[]trim[,]removeprefix["title": "]removesuffix["]]
+[reverse[]unique[]!prefix[Draft]!is[missing]search-replace:g[\"],["]]

Oops, but now I see introduction of the :map filter run would break the earlier default tiddlers and story list part of the filter. To stay compatible with that you can use something like this:

\function history.titles()
  [{$:/HistoryList}jsonindexes[]]
  :map[{$:/HistoryList}jsonget<currentTiddler>,[title]]
\end

\procedure history()
  [subfilter{$:/DefaultTiddlers}]
  [enlist{$:/StoryList!!list}]
  [history.titles[]]
  +[reverse[]unique[]!prefix[Draft]!is[missing]]
\end

<fieldset style="margin: 10px; padding: 5px 5px 5px 10px;">
  <legend style="font-weight: bold; padding: 0 8px 0 8px;">Most Recently Visited</legend>
  <$list filter="[subfilter<history>]">
    <$link><<currentTiddler>></$link><br>
  </$list>
</fieldset>

By extracting history.titles into a function, the :map will no longer be applied to the default tiddlers and storylist part of the filter.

It would require a completely different approach. Probably a custom pagetemplate which intercepted link clicks and implemented its own history mechanism.

Thanks. That does make a lot of sense. I changed it and it works exactly the same.

I’ve been thinking about this and another alternative is to override the navigator widget. The overridden widget would intercept the navigate events and record the timestamp and then forward the navigate event so the navigator widget can perform its tasks.

That doesn’t address the issue of how to store the information about the visit history in a way which makes it easy to truncate to a certain number of days.

Here is some code which does what I suggest:

\procedure record-and-forward-navigation()
<!-- Forward the navigation event to the real navigator widget -->
<$action-navigate $to=<<event-navigateTo>>/>

<!-- Append the navigated tiddler to the list and also record -->
<!-- the current timestamp. Use 'enlist-raw' and ':all' to avoid -->
<!-- deduplication. The two lists must stay in sync! -->
<$action-listops $tiddler="TimestampedHistory" $field=tiddlers $filter="[enlist:raw{TimestampedHistory!!tiddlers}] :all[<event-navigateTo>]"/>
<$action-listops $tiddler="TimestampedHistory" $field=visited $filter="[enlist:raw{TimestampedHistory!!visited}] :all[<now [UTC]YYYY0MM0DD0hh0mm0ssXXX
>]"/>
\end

\widget $navigator()
<!-- Override the navigator widget to intercept navigate events -->
<$parameters $params="@params">

<$genesis
  $type="$navigator"
  $remappable="no"
  $names="[<@params>jsonindexes[]]"
  $values="[<@params>jsonindexes[]] :map[<@params>jsonget<currentTiddler>]">
<$messagecatcher $tm-navigate=<<record-and-forward-navigation>>>
<$slot $name=ts-raw/>
</$messagecatcher>
</$genesis>

</$parameters>
\end

See a demo of the above code at this share site link.

In the demo, I redefine the navigator widget only within a single tiddler. To have it apply to the entire wiki, a tag to mark it global is required.

2 Likes

Wonderful! This working example also shows how to overwrite an intrinsic widget!

Hi @btheado great idea!

What about this slightly off-topic idea:
Do you think the $:/HistoryList might be (ab)used to store the timestamp in a supplemental JSON value?
The record-and-forward procedure could

  • find the newest instance of the navigated tiddler in $:/HistoryList
  • check if it already has a timestamp
  • if not, add a timestamp

This should not interfere with the normal operation of the History. I know it’s a two-edged sword to mess with the HistoryList, but it’s a backwards-compatible mess. :wink:

A possible usage scenario could be to trim a displayed history by time instead of by size. I often have a wiki open over a span of several days (I set the computer to only sleep overnight), so it might make sense to not show older stuff a history list…

@btheado

What’s targeting this?

<$slot $name=ts-raw/>

Something the house cleaner missed? :slight_smile:

Wow. Not that is going to take me some time to wrap my head around. There are a few things there that I’ve never used or studied before. Very interesting! Thanks.

This is how the custom widget can access the body content the user passes in the $navigator

https://tiddlywiki.com/#Custom%20Widgets - see the section on “Accessing Content of Custom Widgets”.

Ah. That’s why there’s no $fill.

Thanks!

Yes it is in fact the default fill for the body of the calling widgets.

We need to improve where this is documented.