#!/bin/env wish
set confdir ~/.config/ntalk
set scriptpath "${confdir}/cscript.tcl"
set sixelpath "${confdir}/sixels.txt"
file mkdir $confdir
proc quit {} {exit 0}
proc restart {} {
global argv0
exec [info nameofexecutable] $argv0 &
exit 0
}
wm title . ". o ( ntalk ) o ."
tk appname ntalk
### ICON ###
# don't worry!! it is just an image!!!!
set nanooo {
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
AAAAAElFTkSuQmCC}
image create photo nanooo -data [binary decode base64 $nanooo]
wm iconphoto . nanooo
### SIXEL PARSING LIB ###
set images {}
proc char2n {char} {
binary scan $char c n
expr {($n&0x7F)-0x3F}
}
proc chars2bytes {sixels} {
set ret {}
set nums {}
foreach sixel $sixels { lappend nums [char2n $sixel] }
for {set i 0} {$i < 6} {incr i} {
set s ""
foreach n $nums {
append s [expr {($n>>$i)&1}]
}
append s [string repeat "0" [expr {7 - (([string length $s] + 7) % 8)}]]
binary scan [binary format b* $s] c* bytes
foreach byte $bytes {
lappend ret [format "%02x" [expr {$byte&0xff}]]
}
}
return $ret
}
proc splitsixels {str} {
set ret {}
set row {}
foreach c [split $str {}] {
switch -regexp -- $c {
- {
lappend ret $row
set row {}
}
[?-~] {
lappend row $c
}
default { # ignored }
}
}
if {$row != {}} { lappend ret $row }
return $ret
}
proc sixels2xbm {sixels} {
set rows [splitsixels $sixels]
set ba {}
if {[llength $rows] == 0} { return {} }
# if {[llength $rows] > 3} { set rows [lrange $rows 0 2] }
set height [expr {min(48, [llength $rows]*6)}]
set width 0
foreach row $rows {
set width [expr {max($width, [llength $row])}]
}
if {$width == 0} { return {} }
foreach row $rows {
append row [string repeat " ?" [expr {$width - [llength $row]}]]
lappend bytes {*}[chars2bytes $row]
}
set nbytes [expr {int(ceil($width / 8.0)) * $height}]
set bytes [lrange $bytes 0 $nbytes-1]
set ret "#define img_width $width\n#define img_height $height\n"
append ret "static unsigned char img_bits\[\] = {\n\t"
foreach byte $bytes {
append ret "0x" $byte ", "
}
set ret [string range $ret 0 end-2]
append ret "\n\t};\n"
return $ret
}
proc sixels2image {sixels} {
global images
if {[dict exists $images "sixel:$sixels"]} {
return [dict get $images "sixel:$sixels"]
}
set xbm [sixels2xbm $sixels]
if {[dict exists $images "xbm:$xbm"]} {
return [dict get $images "xbm:$xbm"]
}
set image [image create bitmap -data $xbm]
dict set images "sixel:$sixels" $image
dict set images "xbm:$xbm" $image
dict set images "image:$image" $sixels
return $image
}
### FONT STUFF ###
font create testingFont -size 100
proc make16 {} {
foreach font {TkDefaultFont TkFixedFont TkTextFont} {
font configure testingFont -family [font configure $font -family]
set fontsize [expr {int(ceil(16.0 / [font metrics testingFont -linespace] * 100))}]
font configure $font -size $fontsize
}
}
make16
### UI SETUP ###
set motd "no MOTD yet! maybe you should send one..."
label .head -textvariable motd
frame .foot
entry .foot.input
label .foot.msgs -textvariable lastmsg
label .foot.sep -text " // "
label .foot.ppl -textvariable clients
label .foot.name
scrollbar .scroll -command {.buffer yview}
text .buffer -height 24 -width 128 -yscrollcommand {.scroll set}
pack .foot.ppl .foot.sep .foot.msgs -side right
pack .foot.name -side left
pack .foot.input -side bottom -fill x
pack .head -side top
pack .foot -side bottom -fill x
pack .scroll -side right -fill y
pack .buffer -fill both
if {[catch {package require history}] == 0} {
history::init .foot.input
bind .foot.input <Return> {
if {[.foot.input get] != {}} {
history::add .foot.input [.foot.input get]
}
}
}
bind . <Control-q> quit
bind . <Control-R> restart
bind . <Control-s> {.menu.opt invoke "show raw sixel codes"}
.buffer tag configure rawsixel -elide true -foreground DarkSlateGrey
.buffer tag configure motd -foreground DarkOliveGreen -justify center -spacing1 5 -spacing3 5 -underline 1
.buffer tag configure mention -foreground DarkOrchid4
### MENU ###
menu .menu
menu .menu.nt -tearoff 0
menu .menu.opt -tearoff 1
menu .menu.sixels -tearoff 1 -title "sixel picker"
menu .menu.sixels.rm -tearoff 0
.menu add cascade -label "ntalk" -menu .menu.nt
.menu.nt add command -label "about ntalk" -command {
tk_dialog .about "about ntalk" \
"ntalk\nby aleteoryx\nlast updated 2025-09-20" \
"info" \
0 okay
}
.menu.nt add separator
.menu.nt add command -label "restart" -command restart -accelerator "Ctrl-Shift-R"
.menu.nt add command -label "quit" -command quit -accelerator "Ctrl-Q"
.menu add cascade -label "sixels" -menu .menu.sixels
.menu.sixels add separator
.menu.sixels add cascade -label "delete a sixel..." -menu .menu.sixels.rm
.menu add cascade -label "options" -menu .menu.opt
.menu.opt add checkbutton -label "show raw sixel codes" \
-accelerator "Ctrl-s" -variable showsixel -command {
.buffer tag configure rawsixel -elide [expr {!$showsixel}]
}
. configure -menu .menu
### USER SIXEL LIBRARY ###
set sixellib {}
proc savesixels {} {
global sixellib sixelpath
set fp [open $sixelpath w]
foreach line [lreverse $sixellib] {
if {$line == {}} {
puts $fp ""
continue
}
lassign $line name data
puts $fp "$name = $data"
}
close $fp
}
proc rmsixel {n} {
global sixellib
set sixellib [lreplace $sixellib $n $n]
savesixels
regensixelmenu
}
proc getsubmenu {menu name} {
while {[set idx [string first / $name]] != -1} {
set chunk [string trim [string range $name 0 $idx-1]]
set name [string trim [string range $name $idx+1 end]]
set submenu "${menu}.u$chunk"
catch {
menu $submenu -tearoff 0
$menu insert 0 cascade -menu $submenu -label $chunk
}
set menu $submenu
}
return [list $menu $name]
}
proc regensixelmenu {} {
global sixellib
.menu.sixels delete 0 [expr {[.menu.sixels index end]-2}]
.menu.sixels.rm delete 0 end
foreach menu [winfo children .menu.sixels] {
if {$menu == ".menu.sixels.rm"} continue
destroy $menu
}
destroy {*}[winfo children .menu.sixels.rm]
for {set i 0} {$i < [llength $sixellib]} {incr i} {
if {[lindex $sixellib $i] == {}} continue
lassign [lindex $sixellib $i] name data
set name [string trim $name]
set data [string trim $data]
set escdata [list "\\($data)"]
lassign [getsubmenu .menu.sixels $name] menu label
$menu insert 0 command -image [sixels2image $data] -hidemargin 1 -command [subst {
.foot.input insert insert $escdata
}]
lassign [getsubmenu .menu.sixels.rm $name] menu label
$menu insert 0 command -image [sixels2image $data] -hidemargin 1 -command [subst {
rmsixel $i
}]
}
}
if {[file readable $sixelpath]} {
set fp [open $sixelpath]
while {![eof $fp]} {
gets $fp line
if {$line == {}} {
lappend sixellib {}
continue
}
set idx [string first "=" $line]
set name [string trim [string range $line 0 $idx-1]]
set data [string trim [string range $line $idx+1 end]]
lappend sixellib [list $name $data]
}
set sixellib [lreverse $sixellib]
close $fp
} else {
set sixellib {}
}
regensixelmenu
set clickedimage {}
proc finishsixel {} {
global clickedimage images sixellib
set data [dict get $images "image:$clickedimage"]
regsub "=" [.namesixel.entry get] ":" name
lappend sixellib [list $name $data]
savesixels
regensixelmenu
destroy .namesixel
}
menu .savesixel -tearoff 0
.savesixel add command -label "save sixel..." -command {
bind . <Button> {}
toplevel .namesixel
wm title .namesixel "< | name sixel | >"
entry .namesixel.entry
button .namesixel.ok -text ok -command finishsixel
pack .namesixel.ok -side bottom -padx 5 -pady 5
pack .namesixel.entry -side bottom -padx 5 -pady 5 -fill x
canvas .namesixel.img -height [image height $clickedimage] \
-width [image width $clickedimage]
.namesixel.img create image 1 1 -anchor nw -image $clickedimage
label .namesixel.blurb -text "choose what to save this sixel as..."
pack .namesixel.img -side left -padx 5 -pady 5
pack .namesixel.blurb -side right -padx 5 -pady 5
bind .namesixel <Destroy> { set clickedimage "" }
}
bind .buffer <Button-3> {
if {$clickedimage == {}} {catch {
set clickedimage [.buffer image cget @%x,%y -image]
.savesixel post %X %Y
bind . <Button> {
.savesixel unpost
bind . <Button> {}
set clickedimage {}
}
}}
}
### CONNECTING ###
set user marmalade
set cmds {}
set sok {}
set server "the series of tubes"
if [file readable $scriptpath] {
set fp [open $scriptpath]
.buffer insert 1.0 [read $fp]
close $fp
} else {
.buffer insert 1.0 {# input connection script, then hit C-RET. your changes will be saved.
set server localhost
set sok [socket $server 44322]
set user marmalade
}
}
bind .buffer <Control-Return> {
eval [.buffer get 1.0 end]
make16
if {$sok != {}} { set cscript [.buffer get 1.0 end] }
}
.buffer mark set insert end
focus .buffer
vwait cscript
bind . <Control-Return> {}
.buffer configure -state disabled
set fp [open $scriptpath w]
puts $fp [string trim $cscript]
close $fp
fconfigure $sok -translation lf; # dammit
set user [string trim $user]
.foot.name configure -text "${user}:"
wm title . ". o ( nanochatting on $server ) o ."
### NETCODE ###
proc sendl {line} {
global sok
regsub "\n" $line " " line
if [catch {
puts $sok $line
flush $sok
}] { restart }
}
proc recvl {} {
global sok
if [catch { gets $sok ret }] { restart }
return $ret
}
set inrecv 0
proc recvlines {{bd 0}} {
global lastmsg inrecv
if {$inrecv} return
set inrecv 1
set n [recvl]
for {set i 0} {$i < $n} {incr i} {
bufpush [recvl]
if $bd bufdown
}
set lastmsg [recvl]
set inrecv 0
}
proc bufpush {line} {
global images motd user
set tag {}
if {[string first "MOTD:" $line] == 0} {
set motd [string trim [string range $line 5 end]]
set line "<<< $motd >>>"
set tag motd
} elseif {[string first "MOTD" $line] == 0} {
set motd [string trim [string range $line 4 end]]
set line "<<< $motd >>>"
set tag motd
} elseif {[regexp "\[^\\w.\]${user}\[^\\w.\]" $line]} {
set tag mention
}
.buffer configure -state normal
set idx1 -1
set idx2 -1
while {[set idx2 [string first "\\(" $line $idx1]] != -1} {
# insert the prefix text
.buffer insert end [string range $line $idx1 $idx2-1] $tag
# get the sixels
set idx1 $idx2
set idx2 [string first ")" $line $idx1+2]
if {$idx2 == -1} { set idx2 [string length $line] }
# insert them
.buffer insert end [string range $line $idx1 $idx2] rawsixel
set image [sixels2image [string range $line $idx1+2 $idx2-1]]
.buffer image create end -image $image
set idx1 [expr {$idx2 + 1}]
}
.buffer insert end [string range $line $idx1 end] $tag
.buffer insert end "\n" $tag
.buffer configure -state disabled
}
proc bufdown {} {
.buffer yview moveto 1
update
}
proc bufclear {} {
.buffer configure -state normal
.buffer replace 1.0 end {}
.buffer configure -state disabled
update
}
proc send {line} {
sendl "SEND $line"
recvl
}
proc poll {} {
global lastmsg
sendl "POLL $lastmsg"
recvl
}
proc hist {} {
bufclear
sendl HIST
recvlines 1
}
proc last {n} {
bufclear
sendl "LAST $n"
recvlines 1
}
proc skip {} {
global lastmsg
sendl "SKIP $lastmsg"
recvlines
}
proc quit {} {
sendl QUIT
exit 0
}
proc stat {} {
global clients
sendl STAT
lassign [recvl] msgs
lassign [recvl] bytes
lassign [recvl] clients
}
### ACTUAL CLIENT CODE LMAO ###
proc sendmsg {msg} {
global lastmsg
set msgid [send $msg]
if {$msgid == $lastmsg+1} {
set lastmsg $msgid
bufpush "$msg"
bufdown
} else { skip }
}
proc pollmsgs {} {
global inrecv
if !$inrecv {
stat
skip
}
after 10000 pollmsgs
}
proc n64k_secs {msgcount} {
# 2025-09-19 01:59:38 GMT
set proto_epoch 1758247178
expr {$proto_epoch + (([clock seconds] - $proto_epoch) * 65535 / $msgcount)}
}
proc n64k_date {} {
global lastmsg
clock format [n64k_secs $lastmsg]
}
### BOOT ###
set lastmsg 0
last 64
stat
after 10000 pollmsgs
bind .foot.input <Return> [concat [bind .foot.input <Return>] ";" {
set line [.foot.input get]
.foot.input delete 0 end
switch -glob -- $line [concat $cmds {
/hist {
hist
}
/quit {
quit
}
/restart {
restart
}
{/last *} {
last [string range $line 6 end]
}
{/send *} {
sendmsg [string range $line 6 end]
}
{/motd *} {
sendmsg "MOTD [string range $line 6 end]"
}
/me* {
sendmsg "${user}[string range $line 3 end]"
}
{/my *} {
sendmsg "${user}'s [string range $line 4 end]"
}
{/nick *} {
set user [string trim [string range $line 6 end]]
.foot.name configure -text "${user}:"
}
{/eval *} {
.foot.input insert 0 [eval [string range $line 6 end]]
}
{/exec *} {
.foot.input insert 0 [exec sh -c [string range $line 6 end]]
}
{/calc *} {
.foot.input insert 0 [expr [string range $line 6 end]]
}
{/n64k} {
.foot.input insert 0 [n64k_date]
}
default {
sendmsg "$user: $line"
}
}]
}]
focus .foot.input