TCL/TK SAVEDEFAULT PACKAGE

Tcl/tk savedefault package

Purpose:

This is probably one of my very first utility package I wrote.

So, when attempting to create and design a (hopefully) user friendly graphical user interface (GUI), quite early, the first questions that need to be adressed are:

  • How do I save user settings, so that they are not lost when the app is closed?
  • How can I re-use settings the next time the program will be launched?

The package is a complete re-write of the original version. I replaced my own code with tcllib functionality like fileutil and inifile.

The package is quite small but turned ot to be incredibly usefull for me. In fact I am using it for almost all GUI related programs I wrote so far.

So here is the source:

So here is the source: savedefault.tcl

# -----------------------------------------------------------------------------
# savedefault.tcl ---
# -----------------------------------------------------------------------------
# (c) 2016, Johann Oberdorfer - Engineering Support | CAD | Software
#     johann.oberdorfer [at] gmail.com
#     www.johann-oberdorfer.eu
# -----------------------------------------------------------------------------
# This source file is distributed under the BSD 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.
#   See the BSD License for more details.
# -----------------------------------------------------------------------------
# Purpose:
#  Package to store & retrieve user settings.
#
# Description:
#  This utility package allows to store session related settings.
#
#  Typically an application starts with sensefull user defaults which are
#  modified on purpose later on throughout the usage of the app.
#  The next time the app will be launched, these settings are
#  retrieved from the ini file and carried over.
#
#  The package is build on fileutil and inifile from tcllib.
#  The ini file is stored underneath the system's temp directory.
#
# -----------------------------------------------------------------------------
# TclOO naming conventions:
# public methods  - starts with lower case declaration names, whereas
# private methods - starts with uppercase naming, so we are going to use CamelCase ...
# -----------------------------------------------------------------------------

package provide savedefault 1.3.1


namespace eval ::savedefault {

    # public interface
    namespace export \
        savedefault \
        readsettings \
        savesettings

    variable ini
    variable ini_defaults

    # ini sections declarations
    
    array set ini {
        DEFAULT_SECTION "Settings"
        COMMENTS        "Comments"
        INI_FILE ""
    }

    proc GetTempDirectory {} {
        # return the temporary directory

        set tempDir [fileutil::tempdir]
        set subDir ".tcl"
        set cpath [file join $tempDir $subDir]

        if { ![file isdirectory $cpath] &&
             [catch {file mkdir $cpath}] != 0 } {

            # no write access ?!..., so we use standard:
            return $tempDir
        }

        return $cpath
    }

    proc savedefault {fname ini_list} {
        #
        # this function needs to be called the 1st time to set up defaults
        # arguments:
        #    fname - ini file, where to store the settings
        #    ini_list - a dictionary list with the default ini declarations
        #
        variable ini
        variable ini_defaults

        # file declaration
        set ini(INI_FILE) [file join [GetTempDirectory] $fname]

        # default settings declaration
        set ini_defaults {}

        foreach item $ini_list {
            set name [lindex $item 0]
            set value [lindex $item 1]
            lappend ini_defaults [list $name $value]
        }
    }

    proc savesettings {ini_list} {
        #
        # save settings to file
        # ini_list - dictionary list containing {name value} sub-lists
        #            for each setting declaration
        #
        variable ini
        variable ini_defaults

        # file must at least be there / erase previous file using "w" option
        set buff [open $ini(INI_FILE) "w"]
        close $buff
        
        set istream [::ini::open $ini(INI_FILE)]
        
        # comments:
        set section $ini(COMMENTS)
        ::ini::set $istream $section "DATE" \
                [clock format [clock seconds] -format "%Y-%m-%d-%H:%M"]
        ::ini::comment $istream $section "DATE" \
                "This file was generated by: savedefault, User: $::tcl_platform(user)" \
                "*** DO NOT MODIFY ***"
        
        set section $ini(DEFAULT_SECTION)
        foreach item $ini_list {
            ::ini::set $istream $section [lindex $item 0] [lindex $item 1]
        }
        
        ::ini::commit $istream
        ::ini::close $istream

        return
    }

    
    proc readsettings {ini_array} {
        #
        # read settings from the ini file
        #
        # ini_array - array handling all values retrieved from the configuration file,
        #             or otherwise use the given default values (ini_defaults)
        #
        # Hint: When reading in the option values from the configuration file,
        # each option is validated against the corresponding default option
        # if the name is not matching, it is not taken into account
        #
        upvar $ini_array arr
        variable ini
        variable ini_defaults
        
        array set arr {}
        
        if {![file exists $ini(INI_FILE)] ||
            ![file readable $ini(INI_FILE)] } {
            
            foreach item $ini_defaults {
                set name [lindex $item 0]
                set val  [lindex $item 1]
                set arr($name) $val
            }
            return
        }
        
        set istream  [::ini::open $ini(INI_FILE)]
        set sections [::ini::sections $istream]
        set cnt 0

        foreach s $sections {
            if {[string trim $s] == $ini(DEFAULT_SECTION)} {
                
                # retrieve all available key values ...
                #   (returns a list of all they key names in the
                #    section and file specified)
                set keyval_lst [::ini::keys $istream $s]
                
                foreach item $ini_defaults {
                    set sec_name [string trim [lindex $item 0]]
                    
                    # puts "--> $keyval_lst :: $sec_name :: [lsearch $keyval_lst $sec_name] <--"
                    if {[set idx [lsearch $keyval_lst $sec_name]] != -1} {
                        
                        set keyval    [lindex $keyval_lst $idx]
                        set sec_value [::ini::value $istream $s $keyval]
                        #  puts "==> $keyval :: $sec_value"
                        
                        if {$sec_value == ""} {
                            # set default value, just in case the value is empty (?!...)
                            set arr($keyval) [lindex $item end]
                        } else {
                            set arr($keyval) $sec_value
                        }
                        incr cnt
                    }
                }
            }
        }

        ::ini::close $istream
        
        # one more comparison:
        if {$cnt != [llength $ini_defaults]} {
            array set arr {}

            foreach item $ini_defaults {
                set name [lindex $item 0]
                set arr($name) [lindex $item 1]
            }
        }
        
        return
    }
}

To understand, how it can be used, it’s probably best to look at the following code which basically shows:

  • how to initialize a default array holding all user specific variables.
  • required call to save the array to a file
  • required call to read the settings back to an array.
savedefault_test.tcl

# -----------------------------------------------------------------------------
# savedefault_test.tcl ---
# -----------------------------------------------------------------------------
# (c) 2016, Johann Oberdorfer - Engineering Support | CAD | Software
#     johann.oberdorfer [at] gmail.com
#     www.johann-oberdorfer.eu
# -----------------------------------------------------------------------------
# This source file is distributed under the BSD 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.
#   See the BSD License for more details.
# -----------------------------------------------------------------------------
# Purpose:
#  Package to store & retrieve user settings.
#
# -----------------------------------------------------------------------------
# TclOO naming conventions:
# public methods  - starts with lower case declaration names, whereas
# private methods - starts with uppercase naming, so we are going to use CamelCase ...
# -----------------------------------------------------------------------------

# where to find required tcllib packages:
set dir [file dirname [info script]]

lappend auto_path [file join $dir "."]
lappend auto_path [file join $dir "../tcllib"]

package require fileutil
package require inifile
package require savedefault


# test starts here ...

package require Tk
catch {console show}


# initializing default settings
        
set ini_defaults {
    {"geometry" "950x570+140+170"}
    {"fontsize" 10}
    {"paneorient" "vertical"}
    {"test" ""}
    {"test1" ""}
    {"test2" ""}
}


::savedefault::savedefault \
    "SaveDefault_Test.ini" \
    $ini_defaults

# overwrite existing array names (if any)
#    - with the values retrieved from the configuration file
#    - otherwise use the given default values (ini_defaults)

array set this_array {}

::savedefault::readsettings this_array
parray this_array


puts "------------------------"

set ini_list {}
lappend ini_list [list "geometry" "800x500"]
lappend ini_list [list "fontsize" 12]
lappend ini_list [list "paneorient" horizontal]
lappend ini_list [list "test" test]
lappend ini_list [list "test1" 123]
lappend ini_list [list "test2" XYZ]

::savedefault::savesettings $ini_list


::savedefault::readsettings this_array
parray this_array

Technically the ini file has the well known ini-format and is stored underneath the operating system’s temp directory.

Once the ini-file is deleted, the user settings (should) fall back to the default settings.

-Have fun-