Keith Vetter 2016-05-22 : There are probably a gazillion Rubik Cube timer programs out there for all platforms, but here's my version with a few tweaks for exactly what I wanted. For example, you can display it in a minimalistic version or add more panels with more functionality; you can select different categories to time, e.g. 3x3x3 or 4x4x4, or even add your own categories.
##+########################################################################## # # RubiksTimer.tcl -- rubik's cube timer # by Keith Vetter 2016-05-04 # package require Tk package require Img set S(title) "Rubik's Cube Timer" set S(font) {Helvetica 124 bold} set S(display,text) "" set S(scramble) "" set S(scrambles,old) {} set S(state) idle proc DoDisplay {} { global S wm title . $S(title) frame .left -bg navyblue -bd 2m frame .left.bottom -bg navyblue pack .left -side left -fill both -expand 1 pack .left.bottom -side bottom -fill both -expand 1 ::ttk::frame .history -borderwidth 5 -relief raised ::History::DoDisplay .history if {"displayFont" in [font names]} { font delete displayFont } font create displayFont {*}[font actual $S(font)] set S(display,text) [PrettyTenths 0 1] label .ticks -font displayFont -textvariable S(display,text) -background cyan pack .ticks -in .left -side top -fill x label .scramble -textvariable S(scramble) -bd 2 -relief ridge lappend S(scrambles,old) $S(scramble) set S(scramble) [Scramble] pack .scramble -in .left -side top -fill x -pady {0 2m} button .start -text "Start" -command ToggleTimer -font {Helvetica 48 bold} pack .start -in .left.bottom -side left -expand 1 -fill x -padx 1i -pady 1m button .showStart -image ::bmp::chevrons_down -padx 1m -pady 1m -command ToggleStartButton button .hideStart -image ::bmp::chevrons_up -padx 1m -pady 1m -command ToggleStartButton button .showHistory -image ::bmp::chevrons -padx 1m -command ToggleHistoryPanel place .showStart -in .ticks -relx 1 -rely 1 -x -2m -y -2m -anchor se place .hideStart -in .left.bottom -relx 1 -rely 0 -x -2m -anchor ne place .showHistory -in .left.bottom -relx 1 -rely 1 -x -2m -y -2m -anchor se ToggleHistoryPanel bind .start <Button-1> {DoButton down} bind .ticks <Button-1> {DoButton down} bind .ticks <ButtonRelease-1> {DoButton up} bind all <Key-Escape><Key-Escape><Key-Escape> { ::History::Erase 0 } focus .start } proc PrettyTenths {tenths {long_format 0}} { if {$tenths eq ""} { return "" } set minutes [expr {$tenths / 600}] set tenths [expr {$tenths % 600}] set seconds [expr {$tenths / 10}] set tenths [expr {$tenths % 10}] if {$long_format} { return [format "%02d:%02d.%d" $minutes $seconds $tenths] } if {$minutes > 0} { return [format "%d:%02d.%d" $minutes $seconds $tenths] } return [format "%d.%d" $seconds $tenths] } proc DoButton {how} { global S if {$how eq "down" && $S(state) eq "idle"} { ResetTimer } if {$how eq "up"} { ToggleTimer } } proc ToggleTimer {} { global S focus .start if {$S(state) eq "idle"} { set S(start) [clock milliseconds] set S(state) "timing" .start config -text "Stop" set S(aid) [after idle Timer] } else { after cancel $S(aid) set S(state) "idle" set S(scramble) [Scramble] .start config -text "Start" ::History::AddTime $S(tenths) } } proc ResetTimer {} { set ::S(start) [clock milliseconds] set ::S(display,text) [PrettyTenths 0 1] } proc Timer {} { global S if {$S(state) ne "timing"} return set S(now) [clock milliseconds] set S(tenths) [expr {($S(now) - $S(start)) / 100}] set S(display,text) [PrettyTenths $S(tenths) 1] set S(aid) [after 100 Timer] } proc Scramble {{length 25}} { set MOVES {R L U D F B} set OPPOSITES {"" "" R L L R U D D U F B B F} set scramble {} set last "" set last2 "" for {set i 0} {$i < $length} {incr i} { while {1} { set move [lindex $MOVES [expr {int(rand() * 6)}]] if {$move eq $last} continue if {$move eq [dict get $OPPOSITES $last] && $move eq $last2} continue set last2 $last set last $move break } set modifier [lindex {"" "\u2019" "\uB2"} [expr {int(rand() * 3)}]] lappend scramble $move$modifier } return $scramble } proc ToggleHistoryPanel {} { lower .showStart if {[winfo ismapped .history]} { pack forget .history raise .hideStart raise .showHistory } else { pack .history -side left -fill y lower .hideStart lower .showHistory } } proc ToggleStartButton {} { if {[winfo ismapped .left.bottom]} { pack forget .left.bottom raise .showStart } else { pack .left.bottom -fill x lower .showStart } } proc ToggleErasePanel {} { set f .eraseFrame if {[winfo exists $f] && [winfo ismapped $f]} { grid forget $f grid .history.hideHistory -row 100 -column 0 -pady 1m -padx 1m -sticky w grid .history.showErase -row 100 -column 1 -pady 1m -padx 1m -sticky e foreach w [winfo child .history.lastTimes] { $w config -borderwidth 1 -relief flat destroy $w.x } } else { foreach w [winfo child .history.lastTimes] { regexp {[0-9]+$} $w who $w config -borderwidth 1 -relief solid label $w.x -image ::img::x -bd 1 -relief solid bind $w.x <ButtonRelease-1> [list ::History::Erase $who] place $w.x -relx 1 -y 0 -anchor ne } grid forget .history.hideHistory grid forget .history.showErase grid $f -in .history -row 1 -column 2 -rowspan 102 -sticky ns -padx 1m } focus .start } proc UniqueTrace {var func} { foreach old [trace info variable $var] { trace remove variable $var {*}$old } if {$func ne ""} { trace variable $var w $func } } image create bitmap ::bmp::chevrons -data { #define chevron_width 14 #define chevron_height 9 static char chevron_bits = { 0x33, 0x03, 0x66, 0x06, 0xcc, 0x0c, 0x98, 0x19, 0x30, 0x33, 0x98, 0x19, 0xcc, 0x0c, 0x66, 0x06, 0x33, 0x03 } } image create bitmap ::bmp::chevrons_left -data { #define chevron_width 14 #define chevron_height 9 static char chevron_bits = { 0x30, 0x33, 0x98, 0x19, 0xcc, 0x0c, 0x66, 0x06, 0x33, 0x03, 0x66, 0x06, 0xcc, 0x0c, 0x98, 0x19, 0x30, 0x33 } } image create bitmap ::bmp::chevrons_up -data { #define chevrons_up_width 9 #define chevrons_up_height 14 static char chevrons_up_bits = { 0x10, 0x00, 0x38, 0x00, 0x6c, 0x00, 0xc6, 0x00, 0x93, 0x01, 0x39, 0x01, 0x6c, 0x00, 0xc6, 0x00, 0x93, 0x01, 0x39, 0x01, 0x6c, 0x00, 0xc6, 0x00, 0x83, 0x01, 0x01, 0x01 } } image create bitmap ::bmp::chevrons_down -data { #define chevrons_down_width 9 #define chevrons_down_height 14 static char chevrons_down_bits = { 0x01, 0x01, 0x83, 0x01, 0xc6, 0x00, 0x6c, 0x00, 0x39, 0x01, 0x93, 0x01, 0xc6, 0x00, 0x6c, 0x00, 0x39, 0x01, 0x93, 0x01, 0xc6, 0x00, 0x6c, 0x00, 0x38, 0x00, 0x10, 0x00 } } image create photo ::img::x -data { iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAAGzVWdFAAAABGdBTUEAAYagMeiWXwAAADFJREFUCJljYG Bg+M+ADP6jMP6jS/3HUIeu9D8jsiwTsgEwDiOMQDYEhc+IzUVE6QQAxBwP/TlB3jEAAAAASUVORK5CYII= } proc About {} { tk_messageBox -message "$::S(title)" -detail "by Keith Vetter\nMay 2016" -parent . \ -title "About $::S(title)" } namespace eval ::History { variable times variable H variable rc_file "~/.rubikstimer_rc" variable categories {3x3x3 2x2x2 4x4x4 Cross F2L} variable category 3x3x3 variable undo {} variable minimums unset -nocomplain times if {$::tcl_interactive} { lappend categories debug } foreach i $categories { set times($i) {} } set minimums(3x3x3) 10 set minimums(debug) 10 unset -nocomplain H set H(best) ? set H(5,ave) ? set H(5,times) ? set H(lifetime,ave) ? set H(last,count) 10 } proc ::History::DoDisplay {f} { variable H set args {-borderwidth 2 -relief sunken -anchor c -width 5} tk_optionMenu $f.category ::History::category {*}$::History::categories ::ttk::label $f.l_best -text Best: -anchor e ::ttk::label $f.best -textvariable ::History::H(best) {*}$args ::ttk::label $f.l_lifetime -text Average: ::ttk::label $f.lifetime -textvariable ::History::H(lifetime,ave) {*}$args ::ttk::label $f.l_5 -text "Last 5: " ::ttk::label $f.5 -textvariable ::History::H(5,ave) {*}$args ::ttk::label $f.l_drop -text "Drop hi/lo: " ::ttk::label $f.drop -textvariable ::History::H(drop,ave) {*}$args ::ttk::frame $f.lastTimes -borderwidth 2 -relief sunken for {set i 0} {$i < $H(last,count)} {incr i} { set w $f.lastTimes.$i ::ttk::label $w -textvariable ::History::H(last,$i) -anchor c -borderwidth 1 -relief flat bind $w <Double-1> [list ::History::Erase $i] grid $w -row [expr {$i / 2}] -column [expr {$i % 2}] -sticky ew } grid columnconfigure $f.lastTimes all -weight 1 -uniform same button $f.showErase -image ::bmp::chevrons -padx 1m -command ToggleErasePanel button $f.hideHistory -image ::bmp::chevrons_left -padx 1m -command ToggleHistoryPanel grid $f.category - - -sticky ew -pady {1m 2m} grid $f.l_best $f.best grid $f.l_lifetime $f.lifetime grid $f.l_5 $f.5 grid $f.l_drop $f.drop grid $f.lastTimes - -pady 2m -sticky ew grid rowconfigure $f 99 -weight 1 grid $f.hideHistory -row 100 -column 0 -pady 1m -padx 1m -sticky w grid $f.showErase -row 100 -column 1 -pady 1m -padx 1m -sticky e UniqueTrace ::History::category ::History::Tracer UniqueTrace ::History::undo ::History::Tracer set ff .eraseFrame ::ttk::frame $ff ::ttk::button $ff.about -text About -command About ::ttk::button $ff.erase_last -text "Erase Last" -command {::History::Erase 0} ::ttk::button $ff.erase_all -text "Erase All" -command {::History::Erase all} ::ttk::button $ff.undo -text "Undo Erase" -command ::History::Undo -state disabled grid $ff.about -sticky ew grid $ff.erase_last -sticky ew grid $ff.erase_all -sticky ew grid $ff.undo -sticky ew button $ff.hideEraseFrame -image ::bmp::chevrons_left -padx 1m -command ToggleErasePanel grid rowconfigure $ff 99 -weight 1 grid $ff.hideEraseFrame - -row 100 -pady 1m -padx 1m -sticky e } proc ::History::Tracer {var1 var2 op} { if {$var1 eq "::History::category"} { wm title . "$::S(title) -- $::History::category" ::History::ComputeStats return } if {$var1 eq "undo" && [winfo exists .eraseFrame.undo]} { .eraseFrame.undo config -state [expr {$::History::undo eq "" ? "disabled" : "normal"}] return } } proc ::History::Erase {which} { variable times variable category variable undo if {$which eq "last"} { set which 0 } lappend undo [list $category $times($category)] if {$which eq "all"} { set times($category) {} } else { set times($category) [lreplace $times($category) end-$which end-$which] } ::History::ComputeStats after idle ::History::SaveStats } proc ::History::Undo {} { variable times variable undo if {$undo eq {}} return lassign [lindex $undo end] category data set undo [lrange $undo 0 end-1] set times($category) $data ::History::ComputeStats after idle ::History::SaveStats } proc ::History::AddTime {tenths} { variable times variable category variable minimums set minimum 0 if {[info exists minimums($category)]} { set minimum $minimums($category) } if {[string is double -strict $minimum] && $tenths < 10*$minimum} return lappend times($category) $tenths ::History::ComputeStats after idle ::History::SaveStats } proc ::History::ComputeStats {} { variable times variable H variable category if {$times($category) eq ""} { set H(best) [set H(lifetime,ave) [set H(5,ave) [PrettyTenths 0]]] } else { set H(best) [PrettyTenths [lindex [lsort -integer $times($category)] 0]] set H(lifetime,ave) [PrettyTenths [expr ([join $times($category) +]) / [llength $times($category)]]] set last_5 [lrange $times($category) end-4 end] set H(5,ave) [PrettyTenths [expr round(([join $last_5 +]) / 5.0)]] if {[llength $last_5] < 5} { set H(drop,ave) - } else { set mid_3 [lrange [lsort -integer $last_5] 1 end-1] set H(drop,ave) [PrettyTenths [expr round(([join $mid_3 +]) / 3.0)]] } } for {set i 0} {$i < $H(last,count)} {incr i} { set tenths [lindex $times($category) end-$i] set H(last,$i) [PrettyTenths $tenths] } } proc ::History::NewMode {newMode} { variable categories variable times if {$newMode in $categories} return lappend categories $newMode set w [winfo child .history.category] $w add radiobutton -label $newMode -variable [$w entrycget 0 -variable] set times($newMode) {} } proc ::History::ReadStatsFromRCFile {} { variable times variable categories if {! [file exists $::History::rc_file]} return if {[catch {set fin [open $::History::rc_file r]}]} return set lines [split [string trim [read $fin]] \n] close $fin foreach line $lines { if {[regexp {^current: (.*)$} $line . category]} { ::History::NewMode $category set ::History::category $category } elseif {[regexp {^([a-zA-Z0-9_-]+): ?([0-9 ]+)$} $line . category data]} { ::History::NewMode $category set times($category) [string trim $data] } } } proc ::History::SaveStats {} { variable times set output {} foreach category [lsort -dictionary [array names times]] { if {$times($category) ne {}} { lappend output "$category: $times($category)" } } if {$output eq ""} { file delete $::History::rc_file } else { set n [catch {set fout [open $::History::rc_file w]}] if {! $n} { puts $fout [join $output \n] puts $fout "current: $::History::category" close $fout } } } ################################################################ DoDisplay ::History::ReadStatsFromRCFile if {$tcl_interactive} { set ::History::category debug } ::History::ComputeStats return