histlist

<JK:2006-04-19> The following is taken from AlphaTcl. As everybody knows, AlphaTcl is a gold mine of clean and well-documented Tcl code. Note that the code uses dict from Tcl 8.5 and lvarpop from TclX. Pure Tcl 8.4 dict implementations can be found on this Wiki or in Tcllib. An emulation of lvarpop is given at the bottom of this page.

 ############################################################
 #        histlist
 #############################################################
 # 
 # NAME
 #        histlist - Create, maintain, and query history lists
 # 
 # SYNOPSIS
 #        histlist create hlName ?size? ?item item ...?
 #        histlist size hlName
 #        histlist clear hlName
 #        histlist destroy hlName
 #        histlist update hlName ?item item ...?
 #        histlist read hlName
 #        histlist back hlName ?pattern?
 #        histlist forth hlName ?pattern?
 #        
 # _________________________________________________________________
 # 
 # 
 # DESCRIPTION
 #        This command performs one of several operations on the variable
 #        given by hlName, which is a history list.  A history list is a list 
 #        where new entries are appended to the end, and eventual previous 
 #        occurrences of the item are deleted.  When the list has reached a
 #        specified maximal size, the oldest entry is deleted when a new one 
 #        is inserted.  One can query this list with the commands ``back'' and 
 #        ``forth'', or ask for the whole list.  Typical examples are familiar 
 #        notions like a ``recent items menu'' or the command history in a 
 #        shell.  Except for the ``create'' sub-command, hlName must be the 
 #        name of an existing history list variable in the current namespace
 #        (or a fully qualified name).  The possible sub-commands (which may
 #        be abbreviated) are:
 #        
 #        histlist create hlName size ?args?
 #               Create a history list named hlName.  This list is of size size, 
 #               and all subsequent arguments are interpreted as items to insert 
 #               into the list one by one.  So for example the command
 #                      histlist create T 3 a b c d c
 #               will create a history list whose content is {b d c}.  If the
 #               argument size is not given the default size 15 is assumed.
 #               (Note: up to AlphaTcl 8.0.1, the created variable would be
 #               in the callers namespace, and hence an error would be thrown
 #               if a local variable of the same name already existed.  The
 #               present version creates the variable in accordance with 
 #               standard Tcl practice: it will be a local variable unless
 #               fully qualified, and unless the variable has previously been
 #               declared by a ``variable'' or ``global'' statement.)
 # 
 #               A history list is implemented as an dict with three entries:
 #                 size (the size of the history list)
 #                 content (a list of length <= size)
 #                 current (the read position: somewhere in the list, or just
 #               outside its range).
 #         
 #        histlist size hlName ?num?
 #               Set the size of the history list hlName to num.  If the
 #               argument num is not given, the current size is returned.
 #               (In any case the present (previous) size is returned.)
 # 
 #        histlist clear hlName
 #               Clear all entries in the history list hlName, but leave the
 #               size of it alone.
 # 
 #        histlist destroy hlName
 #               Permanently removes the whole history list hlName.
 #               
 #        histlist update hlName ?args?
 #               Append the items args to the history list given by hlName,
 #               one by one, removing any duplicates, and respecting the size
 #               specification of hlName.  It also sets the current read position
 #               equal to the length of the list, so that a subsequent call to
 #               histlist back will yield the last item.  (If no arguments are
 #               given after hlName, the current read position is simply reset.)
 #               Examples:
 #                      histlist create T 3 a b c
 #                         --> a b c
 #                      histlist update T x
 #                         --> b c x
 #                      histlist update c
 #                         --> b x c
 # 
 #        histlist read hlName
 #               Return the list content of the history list hlName.
 # 
 #        histlist back hlName ?pattern?
 #               Return the last (previous) entry of hlName, relative to the 
 #               read position, and moves the read position one step back.
 #               (If the previous call to histlist was update or create, then
 #               the read position is the length of the list, and histlist back
 #               will then return the previously inserted item.  If the previous
 #               call to histlist was back then another call to back will return
 #               the second to last item, etc.)  If there are no previous entries
 #               in the list, the empty string is returned.  If pattern is
 #               given, only entries matching pattern are considered (glob matching).
 #               That is, the ``back'' step is taken inside the sublist of matching
 #               items.
 # 
 #        histlist forth hlName ?pattern?
 #               Return the next entry of the history list hlName, relative 
 #               to the read position.  if there is no next item, an empty string 
 #               is returned.  Since the calls ``histlist update'' and ``histlist 
 #               create'' set the read position equal to the length of the list, 
 #               ``histlist forth'' is only useful after one some calls to 
 #               ``histlist back'', and works mainly as an ``undo'' for back.  
 #               The optional pattern argument is treated just as for the back 
 #               sub-command.
 # 
 # EXAMPLES:
 #        histlist create T 6 abc b aa c a
 #        histlist read T
 #           --> abc b aa c a
 #        histlist back T
 #           --> a
 #        histlist back T a*
 #           --> aa
 #        histlist forth T
 #           --> c
 #        histlist forth T c*
 #           -->  ""
 #        histlist update T x
 #        histlist update T y
 #        histlist back T
 #           --> y
 #        histlist read T
 #           --> b aa c a x y
 #        histlist destroy T
 #        
 # REMARKS:
 #        Obviously there is some fine tuning to do, for example with respect 
 #        to return values and error messages.  If there is any need for it, 
 #        a future version of back and forth might also accept -regexp, 
 #        -nocase, and -exact, etc...
 #        



 proc histlist { subCmd hist args } {
 #     uplevel 1 [list variable $hist]
     if { $subCmd == "create" } {
         # Create array in namespace of calling proc and initialise parameters:
         uplevel 1 [list dict set $hist content ""]
         if { [string is integer -strict [lindex $args 0]] && [lindex $args 0] > 0 } {
             uplevel 1 [list dict set $hist size [lvarpop args 0]]
             # (Here we modified args, so that the remaining entries will
             # be appended to the content when we come into the update switch)
         } else {
             uplevel 1 [list dict set $hist size 15]
         }
     }

     upvar 1 $hist A        

     switch -- $subCmd {        
         "update" - "create" {
             foreach item $args {
                 # If the item is already in the list, delete it:
                 if { [set rep [lsearch -exact [dict get $A content] $item]] > -1 } {
                     set L [dict get $A content]
                     lvarpop L $rep
                     dict set A content $L
                 }
                 # Insert the item in the list:  
                 dict lappend A content $item
                 # Truncate:
                 if { [llength [dict get $A content]] > [dict get $A size] } {
                     set L [dict get $A content]
                     lvarpop L 0
                     dict set A content $L
                 } 
             }
             # Reset the read position:
             dict set A current [llength [dict get $A content]]
         }
         "back" {
             set newCurrent [expr {[dict get $A current] - 1}]
             if { [llength $args] } {
                 # Find occurrences:
                 set indices [lsearch -all -glob [lrange [dict get $A content] 0 $newCurrent] [lindex $args 0]]
                 # Pick the last one:
                 set newCurrent [lindex $indices end]
             }
             if { $newCurrent < 0 || $newCurrent == "" } {
                 dict set A current -1
                 return ""
             }
             dict set A current $newCurrent
             return [lindex [dict get $A content] [dict get $A current]]
         }
         "forth" {
             set newCurrent [expr [dict get $A current] + 1]
             if { [llength $args] } {
                 # Find next match:
                 set newCurrent [lsearch -glob -start $newCurrent [dict get $A content] [lindex $args 0]]
             }
             if { $newCurrent >= [llength [dict get $A content]] || $newCurrent == -1 } {
                 dict set A current [llength [dict get $A content]]
                 return ""
             }          
             dict set A current $newCurrent
             return [lindex [dict get $A content] [dict get $A current]]
         }
         "read" {
             return [dict get $A content]
         }
         "size" {
             set oldSize [dict get $A size]
             set newSize [lindex $args 0]
             if { [string is integer -strict $newSize] && $newSize > 0 } {
                 dict set A size $newSize
             }
             return $oldSize
         }
         "clear" {
             dict set A content ""
             dict set A current 0
         }
         "destroy" {
             uplevel 1 [list unset $hist]
         }
         default {
             error "Unknown sub-command to histlist"
         }
     }
     return ""
 }



 # If TclX is loaded (if the user has installed a batteries-included
 # Tcl distribution) then we will have 'lvarpop', but otherwise, we need
 # to define it here.  AlphaTcl doesn't require TclX, so we must
 # include this to be sure this package will work.  This implementation
 # can probably be made more efficient using 'lset'.
 if {![llength [info commands lvarpop]]} {
     proc lvarpop { listname {index ""} {newentry ""} } {
 #         set listname [uplevel 1 [list namespace which -variable $listname]]
         upvar 1 $listname L
         if { ![string length $index] } {
             set index 0
         } elseif { $index == "len" } {
             set index [llength $L]
         } elseif { $index == "end" } {
             set index [expr [llength $L] - 1]
         }
         set res [lindex $L $index]
         if { [string length $newentry] } {
             set L [lreplace $L $index $index $newentry]
         } else {
             set L [lreplace $L $index $index]
         }
         return $res
     }   
 }