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:
# -----------------------------------------------------------------------------
# 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 ---
# -----------------------------------------------------------------------------
# (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.