Posted to tcl by hardkorebob at Fri Aug 07 17:59:10 GMT 2026view raw
-
-
- package require Tk
-
- namespace eval Core {
- variable scriptfile [file normalize [info script]]
- variable plugindir [file join [file dirname $scriptfile] plugins]
-
- variable current_buffer "scratch"
- variable buffer_list {}
- variable buffer_path [dict create] ;# buffer name -> absolute file path on disk
- variable buffer_from_path [dict create] ;# reverse: path -> buffer name
- variable buffer_widget [dict create] ;# buffer name -> internal widget id (buf1, buf2, ...)
- variable buffer_counter 0
- variable active_widget ""
- variable minibuffer_mode ""
- variable minibuffer_prompt ""
- variable plugin_state [dict create] ;# plugin name -> {file .. commands .. binds .. hooks .. modes}
- variable _reloading_plugin "" ;# name of the plugin currently being (re)sourced
-
- # === Hook system ===
- variable hooks [dict create] ;# hookName -> list of scripts
-
- # === Extensible minibuffer modes ===
- variable minibuffer_modes [dict create] ;# modeName -> handler command
- # Builtin modes are registered in Core::Init after procedures are defined
-
- file mkdir $plugindir
- }
-
- # ---- Hook API ----
- proc Core::AddHook {hookName script} {
- variable hooks
- dict lappend hooks $hookName $script
- }
-
- proc Core::RunHook {hookName args} {
- variable hooks
- if {![dict exists $hooks $hookName]} return
- foreach script [dict get $hooks $hookName] {
- catch { uplevel #0 [list {*}$script {*}$args] }
- }
- }
-
- proc Core::RunHookWithReturn {hookName args} {
- variable hooks
- if {![dict exists $hooks $hookName]} { return "" }
- foreach script [dict get $hooks $hookName] {
- if {[catch { uplevel #0 [list {*}$script {*}$args] } result]} continue
- if {$result ne "" && $result ne "continue"} {
- return $result
- }
- }
- return ""
- }
-
- # ---- Extensible minibuffer modes API ----
- proc Core::RegisterMinibufferMode {mode handler} {
- variable minibuffer_modes
- dict set minibuffer_modes $mode $handler
- }
-
- proc Core::UnregisterMinibufferMode {mode} {
- variable minibuffer_modes
- catch { dict unset minibuffer_modes $mode }
- }
-
- # ---- Builtin minibuffer mode handlers ----
- proc Core::HandleEvalMode {input} {
- if {[catch {uplevel #0 $input} result]} {
- Core::Message "Error: $result"
- } else {
- Core::Message "=> $result"
- }
- }
-
- proc Core::HandleFindFileMode {input} {
- Core::OpenFile [string trim $input]
- }
-
- proc Core::HandleSwitchBufferMode {input} {
- set target [string trim $input]
- if {$target in $Core::buffer_list} {
- Core::SwitchToBuffer $target
- } else {
- Core::Message "No such buffer: $target"
- }
- }
-
- proc Core::HandleSaveAsMode {filename} {
- set filename [string trim $filename]
- if {$filename eq ""} {
- Core::Message "Save cancelled"
- return
- }
- Core::SaveBufferToFile $filename
- }
-
- # ---- UI Construction ----
- proc Core::BuildUI {} {
- wm title . "tclme"
-
- # Global defaults flat, no borders
- option add *Background "#F5F5F5"
- option add *Foreground "#2C2C2C"
- option add *Font {Consolas 12}
- option add *borderWidth 0
- option add *relief flat
- option add *highlightThickness 0
-
- # Workspace invisible background because buffers cover it,
- # but you can see it if no buffer is open (scratch appears).
- frame .workspace -bg "#F5F5F5"
- pack .workspace -fill both -expand true
-
- # Thin separator line between workspace and status
- frame .separator1 -bg "#CCCCCC" -height 1
- pack .separator1 -fill x
-
- # Status frame no border, just a background
- frame .status_frame -bg "#E0E0E0"
- pack .status_frame -fill x
- label .status -text " tclme " -anchor w \
- -bg "#E0E0E0" -fg "#555555" -font {Consolas 10 bold}
- pack .status -fill x
-
- # Thin separator between status and minibuffer
- frame .separator2 -bg "#CCCCCC" -height 1
- pack .separator2 -fill x
-
- # Minibuffer flat, no highlight border
- entry .minibuffer -bg "#F0F0F0" -fg "#2C2C2C" \
- -insertbackground "#2C2C2C" -bd 0
- pack .minibuffer -fill x
-
- bind .minibuffer <Return> { Core::ExecuteMinibuffer }
- bind .minibuffer <Escape> { Core::CancelMinibuffer }
- bind .minibuffer <Control-g> { Core::CancelMinibuffer }
- }
-
- # ---- Buffer management ----
- proc Core::SwitchToBuffer {name} {
- variable current_buffer
- variable buffer_list
- variable active_widget
- variable buffer_widget
- variable buffer_counter
-
- if {[dict exists $buffer_widget $name]} {
- set wid [dict get $buffer_widget $name]
- } else {
- set wid "buf[incr buffer_counter]"
- dict set buffer_widget $name $wid
- }
-
- set container ".workspace.$wid"
- set txt "$container.txt"
-
- if {![winfo exists $container]} {
- frame $container -bg "#000000" -bd 0
-
- text $txt -undo true -wrap word -padx 5 -pady 5 -highlightbackground "#000000" \
- -insertbackground "#000000" -bg "#EEEEEE" -fg "#000000" \
- -font {Consolas 12} -yscrollcommand "$container.vs set"
- scrollbar "$container.vs" -orient vertical -command "$txt yview" \
- -bg "#2d2d2d" -troughcolor "#1e1e1e"
-
- pack "$container.vs" -side right -fill y
- pack $txt -side left -fill both -expand true
-
- bindtags $txt [list $txt CoreText Text [winfo toplevel $txt] all]
-
- bind $txt <<Modified>> { Core::RefreshStatus }
- bind $txt <KeyRelease> { Core::RefreshStatus }
- bind $txt <ButtonRelease-1> { Core::RefreshStatus }
-
- lappend buffer_list $name
- }
-
- foreach child [winfo children .workspace] { pack forget $child }
- pack $container -fill both -expand true
-
- set current_buffer $name
- set active_widget $txt
- Core::RefreshStatus
- focus $txt
-
- Core::RunHook switch-buffer-hook $name
- }
-
- proc Core::FindBufferForPath {full} {
- variable buffer_from_path
- if {[dict exists $buffer_from_path $full]} {
- return [dict get $buffer_from_path $full]
- }
- return ""
- }
-
- proc Core::KillBuffer {name} {
- variable buffer_list
- variable buffer_path
- variable buffer_from_path
- variable buffer_widget
- variable current_buffer
-
- if {![dict exists $buffer_widget $name]} {
- Core::Message "No such buffer: $name"
- return
- }
-
- set cancel [Core::RunHookWithReturn kill-buffer-hook $name]
- if {$cancel ne ""} {
- Core::Message "Kill cancelled: $cancel"
- return
- }
-
- set wid [dict get $buffer_widget $name]
- set container ".workspace.$wid"
- set txt "$container.txt"
-
- if {[$txt edit modified]} {
- set answer [tk_messageBox -type yesno -icon warning \
- -title "Unsaved changes" \
- -message "Buffer \"$name\" has unsaved changes. Kill?"]
- if {$answer ne "yes"} return
- }
-
- if {[dict exists $buffer_path $name]} {
- set path [dict get $buffer_path $name]
- catch { dict unset buffer_from_path $path }
- }
-
- set buffer_list [lsearch -all -inline -not -exact $buffer_list $name]
- catch { dict unset buffer_path $name }
- catch { dict unset buffer_widget $name }
-
- if {$current_buffer eq $name} {
- if {[llength $buffer_list] > 0} {
- Core::SwitchToBuffer [lindex $buffer_list end]
- } else {
- Core::SwitchToBuffer "scratch"
- }
- }
-
- after idle [list destroy $container]
- Core::Message "Killed buffer: $name"
- }
-
- # ---- Status and message helpers ----
- proc Core::UpdateStatus {msg} { .status configure -text " $msg " }
-
- proc Core::Message {msg} {
- .minibuffer delete 0 end
- .minibuffer insert 0 $msg
- }
-
- proc Core::RefreshStatus {} {
- variable current_buffer
- variable active_widget
- variable buffer_path
-
- if {$active_widget eq "" || ![winfo exists $active_widget]} { return }
-
- set dirty [expr {[$active_widget edit modified] ? "*" : " "}]
- set pos [$active_widget index insert]
- set line [lindex [split $pos .] 0]
- set col [expr {[lindex [split $pos .] 1] + 1}]
-
- set loc ""
- if {[dict exists $buffer_path $current_buffer]} {
- set loc " [dict get $buffer_path $current_buffer]"
- }
-
- Core::UpdateStatus "${current_buffer}${dirty} Ln $line, Col $col${loc}"
-
- set extra [Core::RunHookWithReturn status-line-hook $current_buffer]
- if {$extra ne ""} {
- .status configure -text " ${current_buffer}${dirty} Ln $line, Col $col${loc} $extra "
- }
- }
-
- # ---- Minibuffer prompts ----
- proc Core::PromptMinibuffer {mode prompt} {
- variable minibuffer_mode
- variable minibuffer_prompt
- set minibuffer_mode $mode
- set minibuffer_prompt $prompt
- .minibuffer delete 0 end
- .minibuffer insert 0 $prompt
- focus .minibuffer
- .minibuffer icursor end
- }
-
- proc Core::SetMinibufferMode {mode} {
- Core::PromptMinibuffer $mode "$mode: "
- }
-
- # ---- Minibuffer handling ----
- proc Core::ExecuteMinibuffer {} {
- variable minibuffer_mode
- variable minibuffer_prompt
- variable active_widget
- variable minibuffer_modes
-
- set input [.minibuffer get]
-
- if {[string match {:*} $input] && $minibuffer_mode eq ""} {
- set cmd [string range $input 1 end]
- .minibuffer delete 0 end
- Core::RunExCommand [string trim $cmd]
- focus $active_widget
- return
- }
-
- set mode $minibuffer_mode
- set arg $input
- if {$minibuffer_prompt ne "" && [string first $minibuffer_prompt $arg] == 0} {
- set arg [string range $arg [string length $minibuffer_prompt] end]
- }
- set minibuffer_mode ""
- set minibuffer_prompt ""
- .minibuffer delete 0 end
-
- if {[dict exists $minibuffer_modes $mode]} {
- {*}[dict get $minibuffer_modes $mode] $arg
- }
-
- focus $active_widget
- Core::RunHook minibuffer-execute-hook $mode $arg
- }
-
- proc Core::CancelMinibuffer {} {
- variable minibuffer_mode
- variable minibuffer_prompt
- set minibuffer_mode ""
- set minibuffer_prompt ""
- .minibuffer delete 0 end
- if {[winfo exists $Core::active_widget]} {
- focus $Core::active_widget
- }
- }
-
- # ---- Ex-command dispatcher ----
- proc Core::RunExCommand {cmd} {
- variable buffer_list
- variable current_buffer
-
- set words [split $cmd]
- set verb [lindex $words 0]
- switch -- $verb {
- "e" - "edit" {
- Core::OpenFile [join [lrange $words 1 end]]
- }
- "w" - "write" {
- Core::SaveCurrentBuffer
- }
- "q" - "quit" {
- Core::Quit
- }
- "b" - "buffer" {
- set arg [join [lrange $words 1 end]]
- if {$arg eq ""} {
- # list buffers with 1â¬based indices
- set idx 0
- set lines {}
- foreach buf $buffer_list {
- incr idx
- lappend lines "$idx: $buf"
- }
- if {[llength $lines] == 0} {
- Core::Message "No buffers open"
- } else {
- Core::Message [join $lines " | "]
- }
- } elseif {[string is integer -strict $arg]} {
- set idx $arg
- if {$idx >= 1 && $idx <= [llength $buffer_list]} {
- Core::SwitchToBuffer [lindex $buffer_list [expr {$idx - 1}]]
- } else {
- Core::Message "Buffer index out of range (1â¬[llength $buffer_list])"
- }
- } else {
- # treat as exact name or substring (original behaviour)
- if {$arg in $buffer_list} {
- Core::SwitchToBuffer $arg
- } else {
- Core::Message "No such buffer: $arg"
- }
- }
- }
- "ls" {
- Core::Message "Buffers: [join $buffer_list {, }]"
- }
- "bd" - "kill" {
- set target [lindex $words 1]
- if {$target eq ""} { set target $current_buffer }
- Core::KillBuffer $target
- }
- "reload" {
- Core::ReloadPlugins [lindex $words 1]
- }
- "eval" {
- set code [join [lrange $words 1 end]]
- if {[catch {uplevel #0 $code} result]} {
- Core::Message "Error: $result"
- } else {
- Core::Message "=> $result"
- }
- }
- "help" {
- Core::Message ":e :w :q :b[#|name] :ls :bd\[name\] :reload\[plugin\] :eval | C-x C-s/C-f/C-e/b/k, C-x C-r"
- }
- default {
- Core::Message "Unknown command: $verb (try :help)"
- }
- }
- }
-
- # ---- Plugins ----
- proc Core::RegisterPlugin {name file} {
- variable plugin_state
- dict set plugin_state $name file $file
- }
-
- proc Core::UnregisterPlugin {name} {
- variable plugin_state
- if {![dict exists $plugin_state $name]} return
-
- if {[dict exists $plugin_state $name commands]} {
- foreach cmd [dict get $plugin_state $name commands] {
- catch { rename $cmd {} }
- }
- }
- if {[dict exists $plugin_state $name binds]} {
- foreach entry [dict get $plugin_state $name binds] {
- lassign $entry widget key script
- catch { bind $widget $key {} }
- }
- }
- if {[dict exists $plugin_state $name hooks]} {
- foreach entry [dict get $plugin_state $name hooks] {
- lassign $entry hookName script
- if {[dict exists $Core::hooks $hookName]} {
- set idx [lsearch -exact [dict get $Core::hooks $hookName] $script]
- if {$idx >= 0} {
- dict set Core::hooks $hookName [lreplace [dict get $Core::hooks $hookName] $idx $idx]
- }
- }
- }
- }
- if {[dict exists $plugin_state $name modes]} {
- foreach mode [dict get $plugin_state $name modes] {
- Core::UnregisterMinibufferMode $mode
- }
- }
- catch { dict unset plugin_state $name }
- }
-
- proc Core::ReloadPlugins {{name ""}} {
- variable plugin_state
- variable plugindir
-
- if {$name ne ""} {
- if {![dict exists $plugin_state $name]} {
- Core::Message "Plugin not registered: $name"
- return
- }
- Core::ReloadOnePlugin $name
- Core::Message "Reloaded plugin: $name"
- return
- }
-
- set current_files [glob -nocomplain -directory $plugindir *.tcl]
- set current_names {}
- foreach f $current_files {
- lappend current_names [file rootname [file tail $f]]
- }
-
- foreach name [dict keys $plugin_state] {
- if {$name ni $current_names} {
- Core::UnregisterPlugin $name
- }
- }
-
- foreach f $current_files {
- set name [file rootname [file tail $f]]
- if {![dict exists $plugin_state $name]} {
- Core::RegisterPlugin $name $f
- if {[catch {Core::SourcePlugin $name $f} err]} {
- Core::Message "Error loading plugin $name: $err"
- }
- } else {
- dict set plugin_state $name file $f
- Core::ReloadOnePlugin $name
- }
- }
- Core::Message "Plugins reloaded."
- }
-
- proc Core::ReloadOnePlugin {name} {
- variable plugin_state
- set file [dict get $plugin_state $name file]
-
- if {[dict exists $plugin_state $name commands]} {
- foreach cmd [dict get $plugin_state $name commands] {
- catch { rename $cmd {} }
- }
- }
- if {[dict exists $plugin_state $name binds]} {
- foreach entry [dict get $plugin_state $name binds] {
- lassign $entry widget key script
- catch { bind $widget $key {} }
- }
- }
- if {[dict exists $plugin_state $name hooks]} {
- foreach entry [dict get $plugin_state $name hooks] {
- lassign $entry hookName script
- if {[dict exists $Core::hooks $hookName]} {
- set idx [lsearch -exact [dict get $Core::hooks $hookName] $script]
- if {$idx >= 0} {
- dict set Core::hooks $hookName [lreplace [dict get $Core::hooks $hookName] $idx $idx]
- }
- }
- }
- }
- if {[dict exists $plugin_state $name modes]} {
- foreach mode [dict get $plugin_state $name modes] {
- Core::UnregisterMinibufferMode $mode
- }
- }
- dict set plugin_state $name commands {}
- dict set plugin_state $name binds {}
- dict set plugin_state $name hooks {}
- dict set plugin_state $name modes {}
-
- if {[catch {Core::SourcePlugin $name $file} err]} {
- Core::Message "Error loading plugin $name: $err"
- }
- }
-
- # ---- Plugin API extensions ----
- proc Core::PluginAddCommand {cmd} {
- variable plugin_state
- variable _reloading_plugin
- if {$_reloading_plugin eq ""} return
- dict lappend plugin_state $_reloading_plugin commands $cmd
- }
-
- proc Core::PluginBindKey {widget key script} {
- variable plugin_state
- variable _reloading_plugin
- if {$_reloading_plugin eq ""} return
- bind $widget $key "$script; break"
- dict lappend plugin_state $_reloading_plugin binds [list $widget $key $script]
- }
-
- proc Core::PluginAddHook {hookName script} {
- variable plugin_state
- variable _reloading_plugin
- if {$_reloading_plugin eq ""} return
- dict lappend plugin_state $_reloading_plugin hooks [list $hookName $script]
- Core::AddHook $hookName $script
- }
-
- proc Core::PluginAddMinibufferMode {mode handler} {
- variable plugin_state
- variable _reloading_plugin
- if {$_reloading_plugin eq ""} return
- dict lappend plugin_state $_reloading_plugin modes $mode
- Core::RegisterMinibufferMode $mode $handler
- }
-
- proc Core::SourcePlugin {name file} {
- variable _reloading_plugin
- set _reloading_plugin $name
- try {
- uplevel #0 [list source $file]
- } finally {
- set _reloading_plugin ""
- }
- }
-
- # ---- Plugin loading on startup ----
- proc Core::LoadPlugins {} {
- variable plugindir
- foreach file [lsort [glob -nocomplain -directory $plugindir *.tcl]] {
- set name [file rootname [file tail $file]]
- Core::RegisterPlugin $name $file
- if {[catch {Core::SourcePlugin $name $file} err]} {
- Core::Message "Error loading plugin $name: $err"
- }
- }
- }
-
- # ---- File operations ----
- proc Core::OpenFile {filename} {
- variable buffer_path
- variable buffer_from_path
- variable active_widget
-
- set filename [string trim $filename]
- if {$filename eq ""} return
- set full [file normalize $filename]
-
- set existing [Core::FindBufferForPath $full]
- if {$existing ne ""} {
- Core::SwitchToBuffer $existing
- Core::Message "Switched to $existing"
- return
- }
-
- set bname [file tail $full]
- if {[dict exists $buffer_path $bname]} {
- set n 2
- while {[dict exists $buffer_path "$bname<$n>"]} { incr n }
- set bname "$bname<$n>"
- }
-
- Core::SwitchToBuffer $bname
- dict set buffer_path $bname $full
- dict set buffer_from_path $full $bname
-
- if {![file exists $full]} {
- Core::Message "(New file) $filename"
- Core::RunHook find-file-hook $full
- return
- }
- if {[catch {
- set fp [open $full r]
- $active_widget delete 1.0 end
- $active_widget insert end [read $fp]
- close $fp
- $active_widget edit modified 0
- Core::Message "Read $filename"
- } err]} {
- Core::Message "Open error: $err"
- }
- Core::RefreshStatus
- Core::RunHook find-file-hook $full
- }
-
- proc Core::SaveCurrentBuffer {} {
- variable current_buffer
- variable buffer_path
-
- if {![dict exists $buffer_path $current_buffer]} {
- # No file associated prompt for save-as
- Core::PromptMinibuffer "save-as" "Save as (filename): "
- return
- }
-
- set filename [dict get $buffer_path $current_buffer]
- Core::SaveBufferToFile $filename
- }
-
- proc Core::SaveBufferToFile {filename} {
- variable current_buffer
- variable active_widget
- variable buffer_path
- variable buffer_from_path
-
- set norm [file normalize $filename]
-
- set cancel [Core::RunHookWithReturn before-save-hook $norm]
- if {$cancel ne ""} {
- Core::Message "Save cancelled: $cancel"
- return
- }
-
- if {[catch {
- set fp [open $norm w]
- puts -nonewline $fp [$active_widget get 1.0 "end-1c"]
- close $fp
- $active_widget edit modified 0
-
- # Update buffer-path mappings
- if {[dict exists $buffer_path $current_buffer]} {
- set old [dict get $buffer_path $current_buffer]
- if {$old ne $norm} {
- catch { dict unset buffer_from_path $old }
- }
- }
- dict set buffer_path $current_buffer $norm
- dict set buffer_from_path $norm $current_buffer
-
- Core::Message "Wrote $norm"
- } err]} {
- Core::Message "Save error: $err"
- }
- Core::RefreshStatus
- Core::RunHook after-save-hook $norm
- }
-
- proc Core::Quit {} {
- variable buffer_list
- variable buffer_widget
-
- set cancel [Core::RunHookWithReturn before-quit-hook]
- if {$cancel ne ""} {
- Core::Message "Quit cancelled: $cancel"
- return
- }
-
- set dirty {}
- foreach name $buffer_list {
- if {![dict exists $buffer_widget $name]} continue
- set wid [dict get $buffer_widget $name]
- set txt ".workspace.$wid.txt"
- if {[winfo exists $txt] && [$txt edit modified]} {
- lappend dirty $name
- }
- }
-
- if {[llength $dirty] > 0} {
- set answer [tk_messageBox -type yesno -icon warning \
- -title "Unsaved changes" \
- -message "Unsaved changes in: [join $dirty {, }]\n\nQuit?"]
- if {$answer ne "yes"} return
- }
- exit 0
- }
-
- # ---- Initialization ----
- proc Core::Init {} {
-
- Core::RegisterMinibufferMode "eval" {Core::HandleEvalMode}
- Core::RegisterMinibufferMode "find-file" {Core::HandleFindFileMode}
- Core::RegisterMinibufferMode "switch-buffer" {Core::HandleSwitchBufferMode}
- Core::RegisterMinibufferMode "save-as" {Core::HandleSaveAsMode}
-
- Core::BuildUI
-
- bind CoreText <Control-g> { Core::CancelMinibuffer }
- bind CoreText <Control-x><Control-c> { Core::Quit }
- bind CoreText <Control-x><Control-r> { Core::ReloadPlugins }
- bind CoreText <Control-x><Control-s> { Core::SaveCurrentBuffer }
- bind CoreText <Control-x><Control-f> { Core::PromptMinibuffer "find-file" "Path: " }
- bind CoreText <Control-x><Control-e> { Core::PromptMinibuffer "eval" "Eval: " }
- bind CoreText <Control-x>b { Core::PromptMinibuffer "switch-buffer" "Switch to buffer: "; break }
- bind CoreText <Control-x>k { Core::KillBuffer $Core::current_buffer ; break}
- bind CoreText <Escape> {
- Core::CancelMinibuffer
- if {[winfo exists $Core::active_widget]} { focus $Core::active_widget }
- }
-
- Core::SwitchToBuffer "scratch"
- Core::LoadPlugins
- focus $Core::active_widget
- }
-
- # ---- Start ----
- Core::Init
-
Add a comment