D. McC: George Peter Staplin (GPS) wrote it, except I just changed the initial "pound-bang" line so it no longer reads "#!/bin/wish8.3".
#!/usr/bin/env wish package require Tk proc bind:copyClass {class newClass} { foreach binding [bind $class] { bind $newClass $binding [bind $class $binding] } } proc bind:removeAllBut {class bindList} { foreach binding $bindList { array set tmprab "<${binding}> 0" } foreach binding [bind $class] { if {[info exists tmprab($binding)]} { continue } bind $class $binding {} } array unset tmprab } proc widget:hex:updateYview {win args} { set pos [lindex $args 0] $win.hex yview moveto $pos widget:hex:resizeColumns $win widget:hex:updateASCII $win widget:hex:updateOffset $win puts "scroll set args: $args" eval $win.scroll set $args } proc widget:hex:event:Button-1 {win x y} { set pos [$win.hex index @$x,$y] $win.hex mark set insert $pos $win.hex mark set anchor insert focus $win.hex $win.hex tag remove sel 0.0 end $win.ascii tag remove sel 0.0 end set cur [$win.hex index insert] set splitIndex [split $cur .] set line [lindex $splitIndex 0] set curChar [lindex $splitIndex 1] #puts stderr $curChar if {[expr {$curChar & 1}]} { set curChar [expr {$curChar - 1}] } if {$curChar > 0} { set curChar [expr {$curChar / 2}] } set hexLine [$win.hex index @0,0] #puts "cur $cur" set offset [expr {int($line - $hexLine + 1.0)}] set cur "$offset.$curChar" set end [$win.ascii index "$cur + 1 chars"] $win.ascii tag add sel $cur $end } proc widget:hex:ascii:event:Button-1 {win x y} { set pos [$win.ascii index @$x,$y] $win.ascii mark set insert $pos $win.ascii mark set anchor insert focus $win.ascii $win.hex tag remove sel 0.0 end $win.ascii tag remove sel 0.0 end set cur [$win.hex index insert] } proc widget:hex:updateASCII {win} { set start [$win.hex index @0,0] set end [$win.hex index @0,[winfo height $win.hex]] set end [expr {double($end + 1.0)}] #puts "$start $end" set data [split [$win.hex get $start $end] \n] $win.ascii delete 1.0 end foreach line $data { set lineLength [expr {[string length $line] / 2}] set line [binary format H* $line] for {set i 0} {$i < $lineLength} {incr i} { binary scan $line @${i}a1 ascii if {[string is alnum $ascii]} { $win.ascii insert end $ascii } else { $win.ascii insert end . } } $win.ascii insert end \n } } proc widget:hex:updateOffset {win} { set viewFirst [$win.hex index @0,0] set viewLast [$win.hex index @0,[winfo height $win.hex]] set viewFirstLine [lindex [split $viewFirst .] 0] set viewLastLine [lindex [split $viewLast .] 0] incr viewFirstLine -1 $win.offset delete 1.0 end for {set i $viewFirstLine} {$i < $viewLastLine} {incr i} { set offset [expr {$i * 16}] $win.offset insert end $offset\n } $win.offset config -width [string length $offset] } proc widget:hex:resizeColumns {win} { set start [$win.hex index @0,0] set end [$win.hex index @0,[winfo height $win.hex]] set viewStartLine [lindex [split $start .] 0] set viewEndLine [lindex [split $end .] 0] #puts "viewStartLine $viewStartLine" #puts "viewEndLine $viewEndLine" for {set i $viewStartLine} {$i <= $viewEndLine} {incr i} { set lineend [$win.hex index "$i.0 lineend"] set charEnd [lindex [split $lineend .] 1] if {$charEnd < 32} { $win.hex delete $lineend } elseif {$charEnd > 32} { #delete the \n $win.hex delete "$i.$charEnd" $win.hex insert "$i.32" \n } } } proc widget:hex:event:backSpace {win} { set cur [$win.hex index insert] if {[regexp {[0-9]+\.0} $cur]} { return } if {[string compare [$win.hex tag nextrange sel 1.0 end] ""]} { $win.hex delete sel.first sel.last } elseif {[$win.hex compare insert != 1.0]} { $win.hex delete insert-1c $win.hex see insert } after idle [list widget:hex:resizeColumns $win] after idle [list widget:hex:updateASCII $win] after idle [list widget:hex:updateOffset $win] } proc widget:hex:event:delete {win} { if {[catch {$win.hex delete sel.first sel.last}]} { $win.hex delete insert } after idle [list widget:hex:resizeColumns $win] after idle [list widget:hex:updateASCII $win] after idle [list widget:hex:updateOffset $win] } proc widget:hex:event:insert {win char} { if {![regexp {[0-9a-f]} $char]} { return } $win.hex insert insert $char $win.hex see insert widget:hex:resizeColumns $win after idle [list widget:hex:updateASCII $win] after idle [list widget:hex:updateOffset $win] } proc widget:hex:instanceCmd {win cmd args} { #puts "instanceCmd $win $cmd $args" if {$cmd == "insert"} { if {[llength $args] != 1} { return -code error "insert called with more than one argument: $args" } set data [lindex $args 0] binary scan $data H* hex set newHex "" set charCount 0 set hexLen [string length $hex] for {set i 0} {$i < $hexLen} {incr i} { incr charCount append newHex [string index $hex $i] if {$charCount == 32} { append newHex \n set charCount 0 } } $win.hex insert end $newHex widget:hex:updateASCII $win widget:hex:updateOffset $win } elseif {$cmd == "clear"} { if {[llength $args] != 0} { return -code error "clear was called with arguments and doesn't accept any arguments: $args" } $win.offset delete 1.0 end $win.hex delete 1.0 end $win.ascii delete 1.0 end } elseif {$cmd == "get"} { if {[llength $args] != 0} { return -code error "get was called with arguments and doesn't accept any arguments: $args" } set data [$win.hex get 1.0 end-1c] set data [string map {"\n" ""} $data] set data [binary format H* $data] return $data } elseif {[string match "conf*" $cmd]} { if {[expr {[llength $args] & 1}] != 0} { return -code error "Invalid number of arguments given to $win\ (uneven number): $args" } array set cmdArgs $args foreach flag {foreground background} short {fg bg} { if {[info exists cmdArgs(-$flag)]} { $win.offset config -$short $cmdArgs(-$flag) $win.hex config -$short $cmdArgs(-$flag) $win.ascii config -$short $cmdArgs(-$flag) unset cmdArgs(-$flag) } if {[info exists cmdArgs(-$short)]} { $win.offset config -$short $cmdArgs(-$short) $win.hex config -$short $cmdArgs(-$short) $win.ascii config -$short $cmdArgs(-$short) unset cmdArgs(-$short) } } if {[info exists cmdArgs(-insertbackground)]} { $win.hex config -insertbackground $cmdArgs(-insertbackground) $win.ascii config -insertbackground $cmdArgs(-insertbackground) unset cmdArgs(-insertbackground) } if {[array size cmdArgs] > 0} { return -code error "1 or more arguments were not understood: [array get cmdArgs]" } } elseif {$cmd == "cget"} { set flag [lindex $args 0] switch -- $flag { -bg - -background { return [$win.hex cget -bg] } -fg - -foreground { return [$win.hex cget -fg] } -insertbackground { return [$win.hex cget -insertbackground] } default { return -code error "unknown flag given to cget: $flag" } } } } proc widget:hex {win args} { if {[expr {[llength $args] & 1}] != 0} { return -code error "Invalid number of arguments given to widget:hex\ (uneven number after window): $args" } array set cmdArgs $args text .__temp set bg [.__temp cget -bg] set fg [.__temp cget -fg] set insertbackground [.__temp cget -insertbackground] puts $insertbackground destroy .__temp foreach flag {foreground background} short {fg bg} { if {[info exists cmdArgs(-$flag)]} { set $short [set cmdArgs(-$flag)] unset cmdArgs(-$flag) } if {[info exists cmdArgs(-$short)]} { set $short [set cmdArgs(-$short)] unset cmdArgs(-$short) } } if {[info exists cmdArgs(-insertbackground)]} { set insertbackground $cmdArgs(-insertbackground) } if {[array size cmdArgs] > 0} { return -code error "1 or more arguments were not understood: [array get cmdArgs]" } bind:copyClass Text HexEdit$win bind:removeAllBut HexEdit$win [list Key-Left Key-Right \ Key-Up Key-Down Key-Next Key-Prior B1-Motion Button-2 B2-Motion] bind HexEdit$win <Button-1> [list widget:hex:event:Button-1 $win %x %y] bind HexEdit$win <Delete> [list widget:hex:event:delete $win] bind HexEdit$win <Key> [list widget:hex:event:insert $win %A] bind HexEdit$win <BackSpace> [list widget:hex:event:backSpace $win] bind HexEditASCII$win <Button-1> [list widget:hex:ascii:event:Button-1 $win %x %y] bind HexEditASCII$win <B1-Motion> [bind Text <B1-Motion>] frame $win pack [scrollbar $win.scroll -command [list $win.hex yview]] -side left -fill y pack [text $win.offset -width 2 -height 6 -wrap none -fg $fg -bg $bg] -side left -fill y bindtags $win.offset all pack [text $win.hex -width 33 -height 6 -wrap none \ -yscrollcommand [list widget:hex:updateYview $win] -fg $fg -bg $bg \ -insertbackground $insertbackground] -side left -fill y pack [text $win.ascii -width 17 -height 6 -wrap none -fg $fg -bg $bg \ -insertbackground $insertbackground] -side left -fill y bindtags $win.hex [list HexEdit$win all] bindtags $win.ascii [list HexEditASCII$win all] bind $win <Configure> {widget:hex:updateYview %W [%W.hex yview]} #The instance command rename $win _junk$win interp alias {} $win {} widget:hex:instanceCmd $win bind $win <Configure> " widget:hex:resizeColumns $win widget:hex:updateASCII $win widget:hex:updateOffset $win " return $win } proc file:open {win inFile} { $win clear set fi [open $inFile r] fconfigure $fi -translation binary -encoding binary set data [read $fi] close $fi $win insert $data } proc file:save {win} { set f [tk_getSaveFile] if {"" == $f} { return } set data [$win get] set fo [open $f w] fconfigure $fo -translation binary -encoding binary puts -nonewline $fo $data close $fo } proc file:choose {win} { set f [tk_getOpenFile] if {"" == $f} { return } file:open $win $f } proc main {argc argv} { #source bind.tcl #source cscrollbar.tcl pack [widget:hex .h] -fill both -side top -anchor w -expand 1 .h config -bg black -fg cyan -insertbackground yellow pack [frame .f] -side bottom -fill x pack [button .f.b -text Save -command [list file:save .h]] -side left pack [button .f.load -text Load -command [list file:choose .h]] -side left } main $argc $argv