Tcl/tk savedefault package

Purpose

This package is quite small but turned out to be incredibly useful for me.
In fact I am using it for almost all GUI related programs I wrote so far. This is probably also one of my very first tcl/tk utility package I wrote.

When trying to create a user friendly GUI, these are the following questions which need to be addressed quite early in the development process:

  • How can 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 following package can be used to solve this issue. It is a complete re-write of my original version and based on the fileutil and inifile tcllib packages.

Implementation

Here is the source:

Package 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

Ini file features:

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

  • The ini file can be safely deleted at time. Once the ini-file is not available, the user settings will fall back to the default settings.

  • The ini file settings can be deleted any time as well, e.g to introduce a new ini setting on purpose. If this is the case, the user settings will fall back to the default settings.