~aleteoryx/tclircc

4bc1948dc256b12750b8c3b405d0be8db08f867e — Aleteoryx 11 days ago 4f82377
uh, docs
8 files changed, 475 insertions(+), 172 deletions(-)

M doc/doctools/gen/irc.man
M doc/doctools/gen/main.man
R doc/doctools/gen/{threads.man => router.man}
M doc/md/irc.md
M doc/md/main.md
R doc/md/{threads.md => router.md}
M src/irc.tcl
M src/main.tcl
M doc/doctools/gen/irc.man => doc/doctools/gen/irc.man +68 -0
@@ 1,5 1,73 @@
[manpage_begin irc tclircc 0.0.1]
[titledesc {Library irc.tcl}]
[description]
[para]
This is a to-spec IRCv3 client library.
It is designed to be freely extracted.
[section {Getting a channel}]
[file irc.tcl] provides 2 ways to connect to an IRC server.
[subsection [concat [cmd irc::connect] [arg hostname] [arg port] [opt [arg usetls]]]]

Connects to [arg hostname]:[arg port], and sets up all the necessary state.
If [arg usetls] is set to true, the [package tls] module will be used to connect,
instead of the builtin [cmd socket] command. If unset, [cmd socket] will be used.

[para]
[sectref {Channel metadata}] is initialized with [arg proto], [arg hostname], [arg port], and [arg uri] set.
[subsection [concat [cmd irc::enroll] [arg chan] [opt [arg meta]]]]

Sets up the internal state necessary for chan to be used as an IRC socket.
It is called internally by [cmd irc::connect].
This command is exposed for the use-case where an IRC channel might have a more bespoke acquisition process than a simple socket connection.

[para]
[arg meta] is the initial state of the [sectref {channel metadata}].
[section {Listening to it}]
[file irc.tcl] provides an event dispatch system, via a [cmd fileevent] script registered on the IRC channel.
Events are dispatched by matching their patterns against incoming messages.
[subsection [concat [cmd irc::listen] [arg subcommand] [arg chan]]]
Enable or disable the [cmd fileevent] script for the dispatch system.

[list_begin definitions]
[call [cmd irc::listen] [const on] [arg chan]]
Apply the [cmd fileevent] wrapper to [arg chan].
Returns the previous [cmd fileevent] wrapper.
[call [cmd irc::listen] [const off] [arg chan]]
Remove the [cmd fileevent] wrapper from [arg chan].
Errors if the channel does not currently have the irc handler set.
[list_end]
[subsection [concat [cmd irc::listener] [arg subcommand] [arg chan] [opt [arg arg]...]]]
Configure [cmd listener]-type event handlers.
[para]
[cmd listener]-type handlers are scripts, executed in either a sub-interpreter or a seperate thread.
Interpreter listeners should yield themselves with [cmd after] during long operations, and never block for extended periods.
If that is infeasible, threaded listeners are recommended.
[para]
[cmd listener]-type scripts are spawned with the variable [var dispatch] set to the channel they are to recieve dispatches over.
Each line will be a [cmd dict] with contents as described in [sectref {Event Dispatch Contents}].
Interpreters are given access to the [sectref {Dispatch-aliased IRC commands}], while threads are given the variable [var parent], the ID of the main thread.
[para]
When a listener is removed, it will recieve a message of just [const end].
It should perform necessary cleanup quickly, as the application is likely exiting.
Threads may simply [cmd thread::release] themselves, while interps may call the provided [cmd selfdestruct].

[list_begin definitions]
[call [cmd irc::listener] [const add] [arg chan] [opt [option -thread]] [arg patlist] [arg script]]
Registers [arg script] as a [cmd listener]-type handler on [arg chan], using [arg patlist] as the [sectref {Message Pattern List}].
Returns an id that can be passed to [cmd irc::listener] [const remove] or [cmd irc::patlist].
[para]
If [option -thread] is passed, it will be created as a threaded listener, otherwise it will be created in a sub-interpreter.
[call [cmd irc::listener] [const remove] [arg chan] [arg id]]
Unregisters the listener identified by [arg id] from [arg chan].
[para]
Ignores requests for nonexistent handlers or handlers of the wrong type.
[list_end]
[subsection [concat [cmd irc::handler] [arg subcommand] [arg chan] [opt [arg arg]...]]]

[subsection [concat [cmd irc::is] [arg type] [arg value]]]
Validation helper.

[para]

[manpage_end]


M doc/doctools/gen/main.man => doc/doctools/gen/main.man +12 -0
@@ 1,5 1,17 @@
[manpage_begin main_thread tclircc 0.0.1]
[titledesc {Thread main}]
[description]
[para]
This is the application entrypoint. It does the following.
[list_begin itemized]
[item]
brings up the routing system
[item]
brings up the database
[item]
loads the other core threads
[item]
opens a window
[list_end]
[manpage_end]


R doc/doctools/gen/threads.man => doc/doctools/gen/router.man +2 -2
@@ 1,5 1,5 @@
[manpage_begin threads tclircc 0.0.1]
[titledesc {Component threads.tcl}]
[manpage_begin router tclircc 0.0.1]
[titledesc {Component router.tcl}]
[description]
[manpage_end]


M doc/md/irc.md => doc/md/irc.md +114 -0
@@ 14,6 14,120 @@ irc \- Library irc\.tcl

  - [Table Of Contents](#toc)

  - [Synopsis](#synopsis)

  - [Description](#section1)

  - [Getting a channel](#section2)

      - [__irc::connect__ *hostname* *port*
        ?*usetls*?](#subsection1)

      - [__irc::enroll__ *chan* ?*meta*?](#subsection2)

  - [Listening to it](#section3)

      - [__irc::listen__ *subcommand* *chan*](#subsection3)

      - [__irc::listener__ *subcommand* *chan*
        ?*arg*\.\.\.?](#subsection4)

      - [__irc::handler__ *subcommand* *chan*
        ?*arg*\.\.\.?](#subsection5)

      - [__irc::is__ *type* *value*](#subsection6)

# <a name='synopsis'></a>SYNOPSIS

[__irc::listen__ __on__ *chan*](#1)  
[__irc::listen__ __off__ *chan*](#2)  
[__irc::listener__ __add__ *chan* ?__\-thread__? *patlist* *script*](#3)  
[__irc::listener__ __remove__ *chan* *id*](#4)  

# <a name='description'></a>DESCRIPTION

This is a to\-spec IRCv3 client library\. It is designed to be freely extracted\.

# <a name='section2'></a>Getting a channel

"irc\.tcl" provides 2 ways to connect to an IRC server\.

## <a name='subsection1'></a>__irc::connect__ *hostname* *port* ?*usetls*?

Connects to *hostname*:*port*, and sets up all the necessary state\. If
*usetls* is set to true, the __tls__ module will be used to connect,
instead of the builtin __socket__ command\. If unset, __socket__ will be
used\.

__Channel metadata__ is initialized with *proto*, *hostname*, *port*,
and *uri* set\.

## <a name='subsection2'></a>__irc::enroll__ *chan* ?*meta*?

Sets up the internal state necessary for chan to be used as an IRC socket\. It is
called internally by __irc::connect__\. This command is exposed for the
use\-case where an IRC channel might have a more bespoke acquisition process than
a simple socket connection\.

*meta* is the initial state of the __channel metadata__\.

# <a name='section3'></a>Listening to it

"irc\.tcl" provides an event dispatch system, via a __fileevent__ script
registered on the IRC channel\. Events are dispatched by matching their patterns
against incoming messages\.

## <a name='subsection3'></a>__irc::listen__ *subcommand* *chan*

Enable or disable the __fileevent__ script for the dispatch system\.

  - <a name='1'></a>__irc::listen__ __on__ *chan*

    Apply the __fileevent__ wrapper to *chan*\. Returns the previous
    __fileevent__ wrapper\.

  - <a name='2'></a>__irc::listen__ __off__ *chan*

    Remove the __fileevent__ wrapper from *chan*\. Errors if the channel
    does not currently have the irc handler set\.

## <a name='subsection4'></a>__irc::listener__ *subcommand* *chan* ?*arg*\.\.\.?

Configure __listener__\-type event handlers\.

__listener__\-type handlers are scripts, executed in either a sub\-interpreter
or a seperate thread\. Interpreter listeners should yield themselves with
__after__ during long operations, and never block for extended periods\. If
that is infeasible, threaded listeners are recommended\.

__listener__\-type scripts are spawned with the variable __dispatch__ set
to the channel they are to recieve dispatches over\. Each line will be a
__dict__ with contents as described in __Event Dispatch Contents__\.
Interpreters are given access to the __Dispatch\-aliased IRC commands__,
while threads are given the variable __parent__, the ID of the main thread\.

When a listener is removed, it will recieve a message of just __end__\. It
should perform necessary cleanup quickly, as the application is likely exiting\.
Threads may simply __thread::release__ themselves, while interps may call
the provided __selfdestruct__\.

  - <a name='3'></a>__irc::listener__ __add__ *chan* ?__\-thread__? *patlist* *script*

    Registers *script* as a __listener__\-type handler on *chan*, using
    *patlist* as the __Message Pattern List__\. Returns an id that can be
    passed to __irc::listener__ __remove__ or __irc::patlist__\.

    If __\-thread__ is passed, it will be created as a threaded listener,
    otherwise it will be created in a sub\-interpreter\.

  - <a name='4'></a>__irc::listener__ __remove__ *chan* *id*

    Unregisters the listener identified by *id* from *chan*\.

    Ignores requests for nonexistent handlers or handlers of the wrong type\.

## <a name='subsection5'></a>__irc::handler__ *subcommand* *chan* ?*arg*\.\.\.?

## <a name='subsection6'></a>__irc::is__ *type* *value*

Validation helper\.

M doc/md/main.md => doc/md/main.md +10 -0
@@ 17,3 17,13 @@ main\_thread \- Thread main
  - [Description](#section1)

# <a name='description'></a>DESCRIPTION

This is the application entrypoint\. It does the following\.

  - brings up the routing system

  - brings up the database

  - loads the other core threads

  - opens a window

R doc/md/threads.md => doc/md/router.md +3 -3
@@ 2,13 2,13 @@
toc: false
---

[//000000001]: # (threads \- )
[//000000001]: # (router \- )
[//000000002]: # (Generated from file 'doctools' by tcllib/doctools with format 'markdown')
[//000000003]: # (threads\(tclircc\) 0\.0\.1  "")
[//000000003]: # (router\(tclircc\) 0\.0\.1  "")

# NAME

threads \- Component threads\.tcl
router \- Component router\.tcl

# <a name='toc'></a>Table Of Contents


M src/irc.tcl => src/irc.tcl +248 -167
@@ 2,6 2,9 @@
# [manpage_begin irc tclircc 0.0.1]
# [titledesc {Library irc.tcl}]
# [description]
# [para]
# This is a to-spec IRCv3 client library.
# It is designed to be freely extracted.

# handler types:
#   chan <dispatch> <interp> // irc::listener


@@ 21,6 24,10 @@

package require Thread

#***
# [section {Getting a channel}]
# [file irc.tcl] provides 2 ways to connect to an IRC server.

namespace eval ::irc {
  variable log [logger::init irc]
  variable logd [logger::init irc::dispatch]


@@ 29,8 36,247 @@ namespace eval ::irc {
  variable chan.handlers
  variable chan.interceptors

  # documented
  proc is {type value {cap {}}} {
  #***
  # [subsection [concat [cmd irc::connect] [arg hostname] [arg port] [opt [arg usetls]]]]
  #
  # Connects to [arg hostname]:[arg port], and sets up all the necessary state.
  # If [arg usetls] is set to true, the [package tls] module will be used to connect,
  # instead of the builtin [cmd socket] command. If unset, [cmd socket] will be used.
  #
  # [para]
  # [sectref {Channel metadata}] is initialized with [arg proto], [arg hostname], [arg port], and [arg uri] set.
  proc connect {hostname port {usetls 0}} {
    if $usetls {
      if {[info commands ::tls::socket] == ""} { package require tls }
      set chan [::tls::socket $hostname $port]
      set proto ircs
    } else {
      set chan [socket $hostname $port]
      set proto irc
    }

    irc::enroll $chan [dict create uri      $proto://$hostname:$port \
                                   proto    $proto \
                                   hostname $hostname \
                                   port     $port]

    return $chan
  }

  #***
  # [subsection [concat [cmd irc::enroll] [arg chan] [opt [arg meta]]]]
  #
  # Sets up the internal state necessary for chan to be used as an IRC socket.
  # It is called internally by [cmd irc::connect].
  # This command is exposed for the use-case where an IRC channel might have a more bespoke acquisition process than a simple socket connection.
  #
  # [para]
  # [arg meta] is the initial state of the [sectref {channel metadata}].
  proc enroll {chan {meta {}}} {
    variable chan.meta
    variable chan.handlers
    variable chan.interceptors
    fconfigure $chan -translation crlf -blocking 0a
    set chan.meta($chan) $meta
    set chan.handlers($chan) {}
    set chan.interceptors($chan) {}
  }


  #***
  # [section {Listening to it}]
  # [file irc.tcl] provides an event dispatch system, via a [cmd fileevent] script registered on the IRC channel.
  # Events are dispatched by matching their patterns against incoming messages.

  #***
  # [subsection [concat [cmd irc::listen] [arg subcommand] [arg chan]]]
  # Enable or disable the [cmd fileevent] script for the dispatch system.
  #
  # [list_begin definitions]
  proc listen {subcommand chan} {
    switch -- $subcommand {
      on {
        #***
        # [call [cmd irc::listen] [const on] [arg chan]]
        # Apply the [cmd fileevent] wrapper to [arg chan].
        # Returns the previous [cmd fileevent] wrapper.
        fileevent $chan readable [list ::irc::int-onmsg $chan]
      }
      off {
        #***
        # [call [cmd irc::listen] [const off] [arg chan]]
        # Remove the [cmd fileevent] wrapper from [arg chan].
        # Errors if the channel does not currently have the irc handler set.
        set oldfe [fileevent $chan readable]
        if {[fileevent $chan readable] != [list ::irc::int-onmsg $chan]} {
          return -code error "channel \"$chan\" not listening for irc"
        } else { fileevent $chan readable "" }
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be off or on" }
    }
  }
  #***
  # [list_end]

  #***
  # [subsection [concat [cmd irc::listener] [arg subcommand] [arg chan] [opt [arg arg]...]]]
  # Configure [cmd listener]-type event handlers.
  # [para]
  # [cmd listener]-type handlers are scripts, executed in either a sub-interpreter or a seperate thread.
  # Interpreter listeners should yield themselves with [cmd after] during long operations, and never block for extended periods.
  # If that is infeasible, threaded listeners are recommended.
  # [para]
  # [cmd listener]-type scripts are spawned with the variable [var dispatch] set to the channel they are to recieve dispatches over.
  # Each line will be a [cmd dict] with contents as described in [sectref {Event Dispatch Contents}].
  # Interpreters are given access to the [sectref {Dispatch-aliased IRC commands}], while threads are given the variable [var parent], the ID of the main thread.
  # [para]
  # When a listener is removed, it will recieve a message of just [const end].
  # It should perform necessary cleanup quickly, as the application is likely exiting.
  # Threads may simply [cmd thread::release] themselves, while interps may call the provided [cmd selfdestruct].
  #
  # [list_begin definitions]
  proc listener {subcommand chan args} {
    variable chan.handlers
    switch -- $subcommand {
      add {
        #***
        # [call [cmd irc::listener] [const add] [arg chan] [opt [option -thread]] [arg patlist] [arg script]]
        # Registers [arg script] as a [cmd listener]-type handler on [arg chan], using [arg patlist] as the [sectref {Message Pattern List}].
        # Returns an id that can be passed to [cmd irc::listener] [const remove] or [cmd irc::patlist].
        # [para]
        # If [option -thread] is passed, it will be created as a threaded listener, otherwise it will be created in a sub-interpreter.
        set thread false
        if {[lindex $args 0] == "-thread"} {
          set thread true
          set args [lrange $args 1 end]
        }

        if {[llength $args] != 2} { return -code error "wrong # args: should be \"irc::listener add chan ?-thread? patlist script\"" }
        lassign $args patlist script
        set id [format "%016x" [expr {round(rand() * (2**64))}]]

        if !$thread {
          set interp [interp create]
          irc::int-setaliases $interp
          interp share {} $chan $interp

          lassign [chan pipe] reader writer
          interp transfer {} $reader $interp
          $interp alias selfdestruct ::irc::int-rminterp $interp
          $interp eval [list set dispatch $reader]
          $interp eval [list after idle $script]

          lappend chan.handlers($chan) [list $patlist chan $id $writer $interp]
        } else {
          set thread [thread::create -preserved]
          lassign [chan pipe] reader writer

          thread::transfer $thread $reader
          thread::send -async $thread [list set dispatch $reader]
          thread::send -async $thread [list set parent [thread::id]]
          thread::send -async $thread $script

          lappend chan.handlers($chan) [list $patlist tchan $id $writer $thread]
        }

        return $id
      }
      remove {
        #***
        # [call [cmd irc::listener] [const remove] [arg chan] [arg id]]
        # Unregisters the listener identified by [arg id] from [arg chan].
        # [para]
        # Ignores requests for nonexistent handlers or handlers of the wrong type.
        if {[llength $args] != 1} { return -code error "wrong # args: should be \"irc::listener remove chan id\"" }
        lassign $args rmid
        set newlist ""
        foreach handler [set chan.handlers($chan)] {
          lassign $handler _ type handlerid writer iot
          if {$handlerid != $rmid || $type ni {chan tchan}} {
            lappend newlist $handler
          } elseif {$type == "chan"} {
            puts $writer end
            flush $writer
          } elseif {$type == "tchan"} {
            puts $writer end
            flush $writer
            thread::release $iot
          }
        }
        set chan.handlers($chan) $newlist
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be add or remove" }
    }
  }
  #***
  # [list_end]

  #***
  # [subsection [concat [cmd irc::handler] [arg subcommand] [arg chan] [opt [arg arg]...]]]
  # 
  proc handler {subcommand chan args} {
    variable chan.handlers
    switch -- $subcommand {
      add {
        set thread false
        if {[lindex $args 0] == "-thread"} {
          set thread true
          set args [lrange $args 1 end]
        }

        if {[llength $args] ni {2 3}} { return -code error "wrong # args: should be \"irc::handlers add chan ?-thread? patlist script ?interp-or-thread?\"" }
        set iot [lassign $args patlist script]
        set id [format "%016x" [expr {round(rand() * (2**64))}]]

        if !$thread {
          if [llength $iot] {
            irc::int-setaliases {*}$iot
            interp share {} $chan {*}$iot
          }

          lappend chan.handlers($chan) [list $patlist script $id $script {*}$iot]
        } else {
          if ![llength $iot] {
            set iot [list [thread::create -preserved]]
          } else {
            thread::preserve {*}$iot
          }

          thread::send -async $iot [list set parent [thread::id]]

          lappend chan.handlers($chan) [list $patlist tscript $id $script {*}$iot]
        }

        return $id
      }
      remove {
        if {[llength args] != 1} { return -code error "wrong # args: should be \"irc::listener remove chan id\"" }
        lassign $args rmid
        set newlist ""
        foreach handler [set chan.handlers($chan)] {
          set iot [lassign $handler _ type handlerid _]
          if {$handlerid != $rmid || $type ni {script tscript}} {
            lappend newlist $handler
          } elseif {$type == "script" && [llength $iot]} {
            interp delete {*}$iot
          } elseif {$type == "tscript"} {
            thread::release {*}$iot
          }
        }
        set chan.handlers($chan) $newlist
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be add or remove" }
    }
  }


  #***
  # [subsection [concat [cmd irc::is] [arg type] [arg value]]]
  # Validation helper.
  #
  # [para]
  # 
  proc is {type value} {
    # validation helper.
    # cap is a list of negotiated capabilities.
    switch -- $type {


@@ 111,52 357,6 @@ namespace eval ::irc {
    }
  }

  # documented
  proc connect {hostname port {usetls 0}} {
    if $usetls {
      if {[info commands ::tls::socket] == ""} { package require tls }
      set chan [::tls::socket $hostname $port]
      set proto ircs
    } else {
      set chan [socket $hostname $port]
      set proto irc
    }

    irc::enroll $chan [dict create uri      $proto://$hostname:$port \
                                   proto    $proto \
                                   hostname $hostname \
                                   port     $port]

    return $chan
  }

  # documented
  proc enroll {chan {meta {}}} {
    variable chan.meta
    variable chan.handlers
    variable chan.interceptors
    fconfigure $chan -translation crlf -blocking 0
    set chan.meta($chan) $meta
    set chan.handlers($chan) {}
    set chan.interceptors($chan) {}
  }

  # documented
  proc listen {subcommand chan} {
    switch -- $subcommand {
      on {
        fileevent $chan readable [list ::irc::int-onmsg $chan]
      }
      off {
        set oldfe [fileevent $chan readable]
        if {[fileevent $chan readable] != [list ::irc::int-onmsg $chan]} {
          return -code error "channel \"$chan\" not listening for irc"
        } else { fileevent $chan readable "" }
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be off or on" }
    }
  }

  # nodoc
  # helper function that rebrands dict command errors
  proc int-dictsub args {


@@ 322,125 522,6 @@ namespace eval ::irc {
  proc int-rminterp {interp} {
    interp delete $interp
  }
  # documented
  proc listener {subcommand chan args} {
    variable chan.handlers
    switch -- $subcommand {
      add {
        set thread false
        if {[lindex $args 0] == "-thread"} {
          set thread true
          set args [lrange $args 1 end]
        }

        if {[llength $args] != 2} { return -code error "wrong # args: should be \"irc::listener add chan ?-thread? patlist script\"" }
        lassign $args patlist script
        set id [format "%016x" [expr {round(rand() * (2**64))}]]

        if !$thread {
          set interp [interp create]
          irc::int-setaliases $interp
          interp share {} $chan $interp

          lassign [chan pipe] reader writer
          interp transfer {} $reader $interp
          $interp alias selfdestruct ::irc::int-rminterp $interp
          $interp eval [list set dispatch $reader]
          $interp eval [list after idle $script]

          lappend chan.handlers($chan) [list $patlist chan $id $writer $interp]
        } else {
          set thread [thread::create -preserved]
          lassign [chan pipe] reader writer

          thread::transfer $thread $reader
          thread::send -async $thread [list set dispatch $reader]
          thread::send -async $thread [list set parent [thread::id]]
          thread::send -async $thread $script

          lappend chan.handlers($chan) [list $patlist tchan $id $writer $thread]
        }

        return $id
      }
      remove {
        if {[llength $args] != 1} { return -code error "wrong # args: should be \"irc::listener remove chan id\"" }
        lassign $args rmid
        set newlist ""
        foreach handler [set chan.handlers($chan)] {
          lassign $handler _ type handlerid writer iot
          if {$handlerid != $rmid || $type ni {chan tchan}} {
            lappend newlist $handler
          } elseif {$type == "chan"} {
            puts $writer end
            flush $writer
          } elseif {$type == "tchan"} {
            puts $writer end
            flush $writer
            thread::release $iot
          }
        }
        set chan.handlers($chan) $newlist
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be add or remove" }
    }
  }

  # documented
  proc handler {subcommand chan args} {
    variable chan.handlers
    switch -- $subcommand {
      add {
        set thread false
        if {[lindex $args 0] == "-thread"} {
          set thread true
          set args [lrange $args 1 end]
        }

        if {[llength $args] ni {2 3}} { return -code error "wrong # args: should be \"irc::handlers add chan ?-thread? patlist script ?interp-or-thread?\"" }
        set iot [lassign $args patlist script]
        set id [format "%016x" [expr {round(rand() * (2**64))}]]

        if !$thread {
          if [llength $iot] {
            irc::int-setaliases {*}$iot
            interp share {} $chan {*}$iot
          }

          lappend chan.handlers($chan) [list $patlist script $id $script {*}$iot]
        } else {
          if ![llength $iot] {
            set iot [list [thread::create -preserved]]
          } else {
            thread::preserve {*}$iot
          }

          thread::send -async $iot [list set parent [thread::id]]

          lappend chan.handlers($chan) [list $patlist tscript $id $script {*}$iot]
        }

        return $id
      }
      remove {
        if {[llength args] != 1} { return -code error "wrong # args: should be \"irc::listener remove chan id\"" }
        lassign $args rmid
        set newlist ""
        foreach handler [set chan.handlers($chan)] {
          set iot [lassign $handler _ type handlerid _]
          if {$handlerid != $rmid || $type ni {script tscript}} {
            lappend newlist $handler
          } elseif {$type == "script" && [llength $iot]} {
            interp delete {*}$iot
          } elseif {$type == "tscript"} {
            thread::release {*}$iot
          }
        }
        set chan.handlers($chan) $newlist
      }
      default { return -code error "unknown subcommand \"$subcommand\": must be add or remove" }
    }
  }

  # nodoc
  proc int-onextern {ichan chan} {

M src/main.tcl => src/main.tcl +18 -0
@@ 4,6 4,11 @@
# [manpage_begin main_thread tclircc 0.0.1]
# [titledesc {Thread main}]
# [description]
# [para]
# This is the application entrypoint. It does the following.
# [list_begin itemized]



set path [file dirname [dict get [info frame 0] file]]
set version v0.0.1


@@ 21,6 26,9 @@ package require sqlite3
${log}::info "tclircc $version <https://amehut.dev/~aleteoryx/tclircc>"
${log}::info "running from $path"

#***
# [item]
# brings up the routing system
src router.tcl

proc on_routes_update {} {


@@ 48,6 56,9 @@ proc start_thread {name} {
  ${log}::debug "started $name thread."
}

#***
# [item]
# brings up the database
start_thread db

thread::send [r::ns tclircc::db] {path_to_core} core_db_path


@@ 56,11 67,17 @@ src migrate_core.tcl

src plugins.tcl

#***
# [item]
# loads the other core threads
start_thread irc
start_thread ui

plugins::load [file join $path .. testplugin]

#***
# [item]
# opens a window
${log}::debug "opening initial window..."
r::exec tclircc::ui {
  mk_toplevel name


@@ 73,4 90,5 @@ ${log}::info "entering event loop"
vwait nil

#***
# [list_end]
# [manpage_end]