hook

hook is a Tcllib package tht implements the subject/observer (publish/subscribe) pattern.

What the Critics Say

hook has become an essential part of my toolbox. I can't imagine developing a medium size app without having such functionality
Emiliano Gavilán, Tcl Chatroom, 2022-04-08

Description

hook allows subjects, which may be modules, objects, widgets, and so forth, to synchronously call hooks which may be bound to an arbitrary number of subscribers, called observers. A subject may call any number of distinct hooks, and any number of observers can bind callbacks to a particular hook called by a particular subject. Hook bindings can be queried and deleted.

Below is an incomplete but illustrative example from Emiliano Gavilán. The Reader module (code inside a namespace which manages the connection and communication with an instrument) issues

hook call Reader <<Connecting>>

when trying to connect, and

hook call Reader <<Connected>> $conn

upon connection/disconnection, with $conn being 1 on connect and 0 on disconnect:

set connbutton [ttk::button $f.startstop \
    -text Connect \
    -command [fqcmd connect] \
    -image ::img::connect0]

hook bind Reader <<Connecting>> $connbutton [lambda {w} {
    $w state disabled
    status set-clear "Connecting to reader"
} $connbutton]

hook bind Reader <<Connected>> $connbutton [lambda {w conn} {
    $w state !disabled
    if {$conn} {
        set spec [list Disconnect [fqcmd disconnect] ::img::connect4]
        status set-clear "Reader connected"
    } else {
        set spec [list Connect [fqcmd connect] ::img::connect0]
        status set-clear "Reader disconnected"
    }
    foreach {txt cmd img} $spec {
        $w configure -text $txt -command $cmd -image $img
    }
} $connbutton]

A simple but more complete example. In this case a clock (a TclOO object) is created with just two public methods: start and stop. It provides three hooks: <<ClockRunning>>, <<ClockUpdate>> and <<ClockDestroy>>. Two observers are defined: a simple one which just prints the value and a more elaborated one with gui, all defined inside a namespace.

package require hook

oo::class create Clock {

    constructor {interval {shift 0}} {
        variable ms $interval
        variable sh $shift
        variable aid ""
    }

    method Tick {} {
        variable ms
        variable sh
        variable aid

        set aid [after $ms [namespace code {my Tick}]]
        set base [clock seconds]
        set time [clock format [clock add $base $sh hours] -format "%H:%M:%S"]
        hook call [self] <<ClockUpdate>> $time
    }

    method start {} {
        variable aid

        if {$aid ne ""} {
            after cancel $aid
        }
        hook call [self] <<ClockRunning>> 1
        my Tick
    }

    method stop {} {
        variable aid

        if {$aid ne ""} {
            after cancel $aid
            set aid ""
        }
        hook call [self] <<ClockRunning>> 0
    }

    destructor {
        my stop
        hook call [self] <<ClockDestroy>>
        hook forget [self]
    }
}

set clock1 [Clock new 200]

# simple observer
set obs [hook bind $clock1 <<ClockUpdate>> {} [list apply {{val} {
    puts $val
}}]]


# let's add a second, shifted clock and a gui
namespace eval gui {
    proc main {} {
        package require Tk
        global clock1
        set clock(1) $clock1
        set clock(2) [Clock new 500 -3]

        foreach c {1 2} {
            set l [label .l$c -width 12]
            set b [button .b$c \
                -command [namespace code [list start $clock($c)]] \
                -text "Start" -width 10]
            set db [button .db$c -text "Destroy" \
                -command [list catch [list $clock($c) destroy]]]
            grid $b $l $db -sticky news

            hook bind $clock($c) <<ClockUpdate>> $l [list apply {{l val} {
                $l configure -text $val
            }} $l]
            hook bind $clock($c) <<ClockDestroy>> $l [list apply {{l} {
                $l configure -text ""
            }} $l]
            bind $l <Destroy> [list hook forget $l]

            hook bind $clock($c) <<ClockRunning>> $b [list apply [list {b clock start} {
                if {$start} {
                    $b configure \
                        -command [namespace code [list stop $clock]] \
                        -text Stop
                } else {
                    $b configure \
                        -command [namespace code [list start $clock]] \
                        -text Start
                }
            } [namespace current]] $b $clock($c)]
            hook bind $clock($c) <<ClockDestroy>> $b [list apply {{b} {
                    $b configure \
                        -command {} \
                        -text "---"
            }} $b]
            bind $b <Destroy> [list hook forget $b]
        }
    }

    proc start {clock} {
        $clock start 
    }

    proc stop {clock} {
        $clock stop 
    }
}

gui::main