#!/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 } proc settitle {title} { wm title . ". o ( $title ) o ." } settitle ntalk 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 { - { set row [string trimright $row "? "] 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] image create bitmap motd$image -data $xbm -foreground DarkOliveGreen image create bitmap mention$image -data $xbm -foreground DarkOrchid4 dict set images "sixel:$sixels" $image dict set images "xbm:$xbm" $image dict set images "image:$image" $sixels dict set images "image:motd$image" $sixels dict set images "image:mention$image" $sixels return $image } proc parseline {line} { set chunks {} set idx1 -1 set idx2 -1 while {[set idx2 [string first "\\(" $line $idx1]] != -1} { # insert the prefix text lappend chunks [string range $line $idx1 $idx2-1] # get the sixels set idx1 $idx2 set idx2 [string first ")" $line $idx1+2] if {$idx2 == -1} { set idx2 [string length $line] } # insert them set image [sixels2image [string range $line $idx1+2 $idx2-1]] lappend chunks [list [string range $line $idx1 $idx2] $image] set idx1 [expr {$idx2 + 1}] } lappend chunks [string range $line $idx1 end] return $chunks } ### 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 ### frame .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 .motd -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 { if {[.foot.input get] != {}} { history::add .foot.input [.foot.input get] } } } bind . quit bind . restart bind . {.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_messageBox -title "about ntalk" \ -message "ntalk\nby aleteoryx" \ -detail "last updated 2025-09-21" \ -icon "info" } .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 ### MOTD ### proc setmotd {newmotd} { global motd destroy {*}[winfo children .motd] foreach {text image} [parseline $newmotd] { set tlabel [label .motd.[incr i] -text $text] pack $tlabel -side left if {$image != {}} { lassign $image _ image set ilabel [label .motd.[incr i] -image $image] pack $ilabel -side left } } set motd $newmotd } ### 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 .