├── _examples ├── background.png ├── all.bash ├── clean.bash ├── trace.go ├── image.go ├── async.go ├── channels.go ├── colors.go ├── feettometers.go ├── nonblocking.go └── calc.go ├── bufferpool.go ├── handles.go ├── interpreter_test.go ├── LICENSE ├── interpreter.h ├── README ├── fmt_test.go ├── interpreter.c ├── fmt.go └── interpreter.go /_examples/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nsf/gothic/HEAD/_examples/background.png -------------------------------------------------------------------------------- /_examples/all.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in *.go; do 4 | go build -o ${i%.go} $i 5 | done 6 | -------------------------------------------------------------------------------- /_examples/clean.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in *.go; do 4 | rm -f ${i%.go} 5 | done 6 | rm -f *.[568] 7 | -------------------------------------------------------------------------------- /_examples/trace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import tk "github.com/nsf/gothic" 4 | import "fmt" 5 | 6 | func main() { 7 | ir := tk.NewInterpreter(` 8 | namespace eval go {} 9 | ttk::entry .e -textvariable go::etext 10 | trace add variable go::etext write go::onchange 11 | pack .e -fill x -expand true 12 | `) 13 | 14 | ir.RegisterCommand("go::onchange", func() { 15 | var s string 16 | ir.EvalAs(&s, "set go::etext") 17 | fmt.Println(s) 18 | }) 19 | <-ir.Done 20 | } 21 | -------------------------------------------------------------------------------- /_examples/image.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | import "image/png" 5 | import "image" 6 | import "os" 7 | 8 | func loadPNG(filename string) image.Image { 9 | f, err := os.Open(filename) 10 | if err != nil { 11 | panic(err) 12 | } 13 | 14 | img, err := png.Decode(f) 15 | if err != nil { 16 | panic(err) 17 | } 18 | return img 19 | } 20 | 21 | func initGUI(ir *gothic.Interpreter) { 22 | ir.UploadImage("bg", loadPNG("background.png")) 23 | ir.Eval(`ttk::label .l -image bg`) 24 | ir.Eval(`pack .l -expand true`) 25 | } 26 | 27 | func main() { 28 | ir := gothic.NewInterpreter(initGUI) 29 | <-ir.Done 30 | } 31 | -------------------------------------------------------------------------------- /bufferpool.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | type buffer_pool_type struct { 9 | sync.Mutex 10 | buffers []bytes.Buffer 11 | } 12 | 13 | // always calls buffer.Reset() before returning it 14 | func (bp *buffer_pool_type) get() bytes.Buffer { 15 | bp.Lock() 16 | if len(bp.buffers) == 0 { 17 | bp.Unlock() 18 | return bytes.Buffer{} 19 | } 20 | 21 | b := bp.buffers[len(bp.buffers)-1] 22 | bp.buffers = bp.buffers[:len(bp.buffers)-1] 23 | bp.Unlock() 24 | b.Reset() 25 | return b 26 | } 27 | 28 | func (bp *buffer_pool_type) put(b bytes.Buffer) { 29 | bp.Lock() 30 | bp.buffers = append(bp.buffers, b) 31 | bp.Unlock() 32 | } 33 | 34 | var buffer_pool buffer_pool_type 35 | -------------------------------------------------------------------------------- /_examples/async.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | import "time" 5 | 6 | func main() { 7 | ir := gothic.NewInterpreter(` 8 | pack [ttk::progressbar .bar1] -padx 20 -pady 20 9 | pack [ttk::progressbar .bar2] -padx 20 -pady 20 10 | `) 11 | 12 | go func() { 13 | i := 0 14 | inc := -1 15 | for { 16 | if i > 99 || i < 1 { 17 | inc = -inc 18 | } 19 | i += inc 20 | time.Sleep(5e7) 21 | ir.Eval(`.bar1 configure -value %{}`, i) 22 | } 23 | }() 24 | 25 | go func() { 26 | i := 0 27 | inc := -1 28 | 29 | for { 30 | if i > 99 || i < 1 { 31 | inc = -inc 32 | } 33 | i += inc 34 | time.Sleep(1e8) 35 | ir.Eval(`.bar2 configure -value %{}`, i) 36 | } 37 | }() 38 | 39 | <-ir.Done 40 | } 41 | -------------------------------------------------------------------------------- /handles.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | type handle struct { 4 | ID int 5 | Value interface{} 6 | } 7 | 8 | type handles []handle 9 | 10 | func (h *handles) init_maybe() { 11 | if *h == nil { 12 | *h = make(handles, 1) 13 | } 14 | if len(*h) == 0 { 15 | *h = append(*h, handle{0, nil}) 16 | } 17 | } 18 | 19 | func (h *handles) get_handle_for_value(value interface{}) int { 20 | h.init_maybe() 21 | hh := *h 22 | free_id := hh[0].ID 23 | if free_id != 0 { 24 | hh[0].ID = hh[free_id].ID 25 | hh[free_id].Value = value 26 | return free_id 27 | } 28 | 29 | *h = append(hh, handle{-1, value}) 30 | return len(*h) - 1 31 | } 32 | 33 | func (h *handles) free_handle(id int) { 34 | hh := *h 35 | hh[id].Value = nil 36 | hh[id].ID = hh[0].ID 37 | hh[0].ID = id 38 | } 39 | -------------------------------------------------------------------------------- /_examples/channels.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | import "github.com/nsf/gothic" 5 | 6 | func dispatcher(c <-chan string) { 7 | for v := range c { 8 | switch v { 9 | case "button1": 10 | fmt.Println("Button 1!") 11 | case "button2": 12 | fmt.Println("Button 2!") 13 | case "button3": 14 | fmt.Println("Button 3!") 15 | } 16 | } 17 | } 18 | 19 | func main() { 20 | ir := gothic.NewInterpreter(` 21 | ttk::button .b1 -text "Button 1" -command {dispatcher <- button1} 22 | ttk::button .b2 -text "Button 2" -command {dispatcher <- button2} 23 | ttk::button .b3 -text "Button 3" -command {dispatcher <- button3} 24 | pack .b1 .b2 .b3 25 | `) 26 | 27 | c := make(chan string) 28 | go dispatcher(c) 29 | 30 | ir.RegisterCommand("dispatcher", func(_, arg string) { 31 | c <- arg 32 | }) 33 | 34 | <-ir.Done 35 | } 36 | -------------------------------------------------------------------------------- /interpreter_test.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | var ir *Interpreter 9 | 10 | func irinit(b *testing.B) { 11 | if ir == nil { 12 | ir = NewInterpreter(nil) 13 | time.Sleep(200 * time.Millisecond) 14 | } 15 | b.ResetTimer() 16 | } 17 | 18 | func BenchmarkTcl(b *testing.B) { 19 | irinit(b) 20 | 21 | ir.Set("N", b.N) 22 | ir.Eval(` 23 | for {set i 0} {$i < $N} {incr i} { 24 | set x 10 25 | } 26 | `) 27 | } 28 | 29 | func BenchmarkForeignGo(b *testing.B) { 30 | irinit(b) 31 | 32 | for i := 0; i < b.N; i++ { 33 | ir.Eval(`set x 10`) 34 | } 35 | } 36 | 37 | func BenchmarkNativeGo(b *testing.B) { 38 | irinit(b) 39 | 40 | ir.UnregisterCommand("test") 41 | ir.RegisterCommand("test", func() { 42 | for i := 0; i < b.N; i++ { 43 | ir.Eval(`set x 10`) 44 | } 45 | }) 46 | ir.Eval(`test`) 47 | } 48 | -------------------------------------------------------------------------------- /_examples/colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | 5 | func main() { 6 | ir := gothic.NewInterpreter(` 7 | ttk::style configure My.TFrame -background #000000 8 | ttk::frame .frame -width 100 -height 30 -relief sunken -style My.TFrame 9 | 10 | ttk::scale .scaleR -from 0 -to 255 -length 200 -command {scaleUpdate 0} 11 | ttk::scale .scaleG -from 0 -to 255 -length 200 -command {scaleUpdate 1} 12 | ttk::scale .scaleB -from 0 -to 255 -length 200 -command {scaleUpdate 2} 13 | 14 | pack .frame -fill both -expand true 15 | pack .scaleR .scaleG .scaleB -fill both 16 | `) 17 | 18 | var RGB [3]int 19 | ir.RegisterCommand("scaleUpdate", func(idx int, x float64) { 20 | if RGB[idx] == int(x) { 21 | return 22 | } 23 | RGB[idx] = int(x) 24 | ir.Eval(`ttk::style configure My.TFrame -background `+ 25 | `#%{%02X}%{%02X}%{%02X}`, RGB[0], RGB[1], RGB[2]) 26 | }) 27 | 28 | <-ir.Done 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 nsf 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /_examples/feettometers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | 5 | const init_script = ` 6 | wm title . "Feet to Meters" 7 | grid [ttk::frame .c -padding "3 3 12 12"] -column 0 -row 0 -sticky nwes 8 | grid columnconfigure . 0 -weight 1; grid rowconfigure . 0 -weight 1 9 | 10 | grid [ttk::entry .c.feet -width 7 -textvariable feet] -column 2 -row 1 -sticky we 11 | grid [ttk::label .c.meters -textvariable meters] -column 2 -row 2 -sticky we 12 | grid [ttk::button .c.calc -text "Calculate" -command calculate] -column 3 -row 3 -sticky w 13 | 14 | grid [ttk::label .c.flbl -text "feet"] -column 3 -row 1 -sticky w 15 | grid [ttk::label .c.islbl -text "is equivalent to"] -column 1 -row 2 -sticky e 16 | grid [ttk::label .c.mlbl -text "meters"] -column 3 -row 2 -sticky w 17 | 18 | foreach w [winfo children .c] {grid configure $w -padx 5 -pady 5} 19 | focus .c.feet 20 | bind . {calculate} 21 | ` 22 | 23 | func main() { 24 | ir := gothic.NewInterpreter(init_script) 25 | ir.RegisterCommand("calculate", func() { 26 | var f float64 27 | ir.EvalAs(&f, "set feet") 28 | ir.Eval("set meters %{%.3f}", f * 0.3048) 29 | }) 30 | 31 | <-ir.Done 32 | } 33 | -------------------------------------------------------------------------------- /interpreter.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | typedef struct { 6 | int go_interp; // go tcl/tk interpreter handle, that's a global handle 7 | int h0; // first handle to Go object (callback or receiver) 8 | int h1; // second handle to Go object (method if h0 is receiver) 9 | } GoTkClientData; 10 | 11 | 12 | void _gotk_c_tcl_set_result(Tcl_Interp *interp, char *result); 13 | GoTkClientData *_gotk_c_client_data_new(int go_interp, int h0, int h1); 14 | 15 | //------------------------------------------------------------------------------ 16 | // Command 17 | //------------------------------------------------------------------------------ 18 | 19 | int _gotk_c_command_handler(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]); 20 | void _gotk_c_command_deleter(ClientData cd); 21 | void _gotk_c_add_command(Tcl_Interp *interp, const char *name, int go_interp, int cb); 22 | void _gotk_c_add_method(Tcl_Interp *interp, const char *name, int go_interp, 23 | int recv, int meth); 24 | 25 | //------------------------------------------------------------------------------ 26 | // Async 27 | //------------------------------------------------------------------------------ 28 | 29 | typedef struct { 30 | Tcl_Event header; 31 | int go_interp; 32 | } GoTkAsyncEvent; 33 | 34 | Tcl_Event *_gotk_c_new_async_event(int go_interp); 35 | -------------------------------------------------------------------------------- /_examples/nonblocking.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | import "time" 5 | 6 | func proc(ir *gothic.Interpreter, num string) { 7 | button := ".b" + num 8 | progress := ".p" + num 9 | channame := "proc" + num 10 | recv := make(chan int) 11 | 12 | // register channel and enable button 13 | ir.RegisterCommand(channame, func(_ string, arg int){ 14 | recv <- arg 15 | }) 16 | ir.Eval(`%{} configure -state normal`, button) 17 | 18 | for { 19 | // wait for an event 20 | <-recv 21 | 22 | // simulate activity 23 | ir.Eval(`%{} configure -state disabled -text "In Progress %{}"`, button, num) 24 | for i := 0; i <= 100; i += 2 { 25 | ir.Eval(`%{%s} configure -value %{}`, progress, i) 26 | time.Sleep(5e7) 27 | } 28 | 29 | // reset button state and progress value 30 | ir.Eval(`%{} configure -value 0`, progress) 31 | ir.Eval(`%{} configure -state normal -text "Start %{}"`, button, num) 32 | } 33 | } 34 | 35 | const init_script = ` 36 | foreach n {1 2 3} row {0 1 2} { 37 | ttk::button .b$n -text "Start $n" -command "proc$n <- 0" -state disabled -width 10 38 | grid .b$n -column 0 -row $row -padx 2 -pady 2 -sticky nwse 39 | grid [ttk::progressbar .p$n] -column 1 -row $row -padx 2 -pady 2 -sticky nwse 40 | grid rowconfigure . $row -weight 1 41 | } 42 | foreach col {0 1} { 43 | grid columnconfigure . $col -weight 1 44 | } 45 | ` 46 | 47 | func main() { 48 | ir := gothic.NewInterpreter(init_script) 49 | go proc(ir, "1") 50 | go proc(ir, "2") 51 | go proc(ir, "3") 52 | <-ir.Done 53 | } 54 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Tcl/Tk Go bindings. 2 | 3 | VERSION NOTICE 4 | 5 | Recently Tcl/Tk 8.6 were released. I use them as a default, if you still have 6 | Tcl/Tk 8.5 use `go get -tags tcl85 github.com/nsf/gothic`. 7 | 8 | DESCRIPTION 9 | 10 | In its current state the bindings are a bit Tk-oriented. You can't create an 11 | interpreter instance without Tk. In future it's likely it will be changed. 12 | 13 | The API is very simple. In the package you have one type and one function: 14 | 15 | type Interpreter struct 16 | func NewInterpreter (init interface{}) *Interpreter 17 | 18 | In order to launch an interpreter you have to call the "NewInterpreter" 19 | function, it will make a new instance of a tcl/tk interpreter in a separate 20 | goroutine, execute "init", block in Tk's main loop and then the function 21 | returns a pointer to the new instance of an "Interpreter". 22 | 23 | "init" could be a string with tcl commands that are executed before Tk's main 24 | loop, or a function with this signature: "func (*Interpreter)". This function 25 | gets executed the same way as the string, that is - before Tk's main loop. 26 | 27 | Here are the methods of the "Interpreter": 28 | 29 | func (*Interpreter) ErrorFilter(filt func(error) error) 30 | func (*Interpreter) Eval(args ...interface{}) error 31 | func (*Interpreter) EvalAs(out interface{}, args ...interface{}) error 32 | func (*Interpreter) Set(name string, val interface{}) error 33 | func (*Interpreter) UploadImage(name string, img image.Image) error 34 | func (*Interpreter) RegisterCommand(name string, cbfunc interface{}) error 35 | func (*Interpreter) UnregisterCommand(name string) error 36 | func (*Interpreter) RegisterCommands(name string, val interface{}) error 37 | func (*Interpreter) UnregisterCommands(name string) error 38 | 39 | As it was stated before, the "Interpreter" is being executed in a separate 40 | goroutine and each method is completely thread-safe. Also every method is 41 | synchronous. It will queue commands for execution and wait for their 42 | completion. 43 | 44 | That's it. See "examples" directory it has the use cases for most of the API. 45 | -------------------------------------------------------------------------------- /fmt_test.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | func test_format(t *testing.T, gold, format string, args ...interface{}) { 10 | var buf bytes.Buffer 11 | err := sprintf(&buf, format, args...) 12 | if err != nil { 13 | t.Error(err) 14 | return 15 | } 16 | s := buf.String() 17 | if gold != s { 18 | t.Errorf("%q != %q", gold, s) 19 | } 20 | } 21 | 22 | func must_contain(t *testing.T, err error, re string) { 23 | if err == nil { 24 | t.Error("non-nil error expected") 25 | return 26 | } 27 | r := regexp.MustCompile(re) 28 | if !r.MatchString(err.Error()) { 29 | t.Errorf("%q doesn't contain %q", err, re) 30 | } 31 | } 32 | 33 | func test_error(t *testing.T, gold, format string, args ...interface{}) { 34 | var buf bytes.Buffer 35 | err := sprintf(&buf, format, args...) 36 | must_contain(t, err, gold) 37 | } 38 | 39 | func TestFormat(t *testing.T) { 40 | am := ArgMap{"i": 10, "j": 5} 41 | test_format(t, "simple as is %{oops}", "simple as is %{oops}") 42 | test_format(t, "10 = 5 + 5", "%{} = %{} + %{1}", 10, 5) 43 | test_format(t, "10 5 5", "%{i} %{j} %{j}", am) 44 | test_format(t, "3.14", "%{%.2f}", 3.1415) 45 | test_format(t, "005", "%{j%03d}", am) 46 | test_format(t, `"\[command \$variable\]"`, "%{%q}", "[command $variable]") 47 | test_error(t, "missing enclosing bracket", "%{} %{", 10, 5) 48 | test_error(t, "not-a-number", "%{oops}", 10, 5) 49 | test_error(t, "there is no.+index -100", "%{-100}", 1, 2, 3) 50 | test_error(t, "there is no.+index 100", "%{100}", 1, 2, 3) 51 | test_error(t, "empty format tag", "%{}", am) 52 | test_error(t, `no argument "x "`, "%{i} %{x }", am) 53 | } 54 | 55 | func test_quote(t *testing.T, gold, s string) { 56 | var buf bytes.Buffer 57 | quote(&buf, s) 58 | s2 := buf.String() 59 | if gold != s2 { 60 | t.Errorf("%s != %s (%q)", gold, s2, s) 61 | } 62 | } 63 | 64 | func TestQuote(t *testing.T) { 65 | test_quote(t, `"\[command \$variable\]"`, "[command $variable]") 66 | test_quote(t, `"\{1 2 3\}"`, "{1 2 3}") 67 | test_quote(t, `"\a\b\f\n\r\t\v\x00"`, "\a\b\f\n\r\t\v\x00") 68 | } 69 | -------------------------------------------------------------------------------- /interpreter.c: -------------------------------------------------------------------------------- 1 | #include "interpreter.h" 2 | 3 | static void free_string(char *c) { 4 | free(c); 5 | } 6 | 7 | void _gotk_c_tcl_set_result(Tcl_Interp *interp, char *result) { 8 | Tcl_SetResult(interp, result, free_string); 9 | } 10 | 11 | GoTkClientData *_gotk_c_client_data_new(int go_interp, int h0, int h1) { 12 | GoTkClientData *cd = malloc(sizeof(GoTkClientData)); 13 | cd->go_interp = go_interp; 14 | cd->h0 = h0; 15 | cd->h1 = h1; 16 | return cd; 17 | } 18 | 19 | //------------------------------------------------------------------------------ 20 | // Command 21 | //------------------------------------------------------------------------------ 22 | 23 | extern int _gotk_go_command_handler(GoTkClientData*, int, Tcl_Obj**); 24 | extern int _gotk_go_method_handler(GoTkClientData*, int, Tcl_Obj**); 25 | extern void _gotk_go_command_deleter(GoTkClientData*); 26 | 27 | int _gotk_c_command_handler(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { 28 | return _gotk_go_command_handler((GoTkClientData*)cd, objc, (Tcl_Obj**)objv); 29 | } 30 | 31 | int _gotk_c_method_handler(ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[]) { 32 | return _gotk_go_method_handler((GoTkClientData*)cd, objc, (Tcl_Obj**)objv); 33 | } 34 | 35 | void _gotk_c_command_deleter(ClientData cd) { 36 | GoTkClientData *clidata = (GoTkClientData*)cd; 37 | _gotk_go_command_deleter(clidata); 38 | free(cd); 39 | } 40 | 41 | void _gotk_c_method_deleter(ClientData cd) { 42 | free(cd); 43 | } 44 | 45 | void _gotk_c_add_command(Tcl_Interp *interp, const char *name, int go_interp, int f) 46 | { 47 | GoTkClientData *cd = _gotk_c_client_data_new(go_interp, f, 0); 48 | Tcl_CreateObjCommand(interp, name, _gotk_c_command_handler, 49 | (ClientData)cd, _gotk_c_command_deleter); 50 | } 51 | 52 | void _gotk_c_add_method(Tcl_Interp *interp, const char *name, int go_interp, 53 | int recv, int meth) 54 | { 55 | GoTkClientData *cd = _gotk_c_client_data_new(go_interp, recv, meth); 56 | Tcl_CreateObjCommand(interp, name, _gotk_c_method_handler, 57 | (ClientData)cd, _gotk_c_method_deleter); 58 | } 59 | 60 | //------------------------------------------------------------------------------ 61 | // Async 62 | //------------------------------------------------------------------------------ 63 | 64 | extern int _gotk_go_async_handler(Tcl_Event*, int); 65 | 66 | Tcl_Event *_gotk_c_new_async_event(int go_interp) { 67 | GoTkAsyncEvent *ev = (GoTkAsyncEvent*)Tcl_Alloc(sizeof(GoTkAsyncEvent)); 68 | ev->header.proc = _gotk_go_async_handler; 69 | ev->header.nextPtr = 0; 70 | ev->go_interp = go_interp; 71 | return (Tcl_Event*)ev; 72 | } 73 | -------------------------------------------------------------------------------- /_examples/calc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/nsf/gothic" 4 | import "math/big" 5 | 6 | type calc struct { 7 | *gothic.Interpreter 8 | args [2]*big.Int 9 | lastOp string 10 | afterOp bool 11 | } 12 | 13 | func (c *calc) TCL_ApplyOp(op string) { 14 | if c.afterOp && c.lastOp != "=" { 15 | return 16 | } 17 | 18 | var num string 19 | c.EvalAs(&num, "set calcText") 20 | if c.args[0] == nil { 21 | if op != "=" { 22 | c.args[0] = big.NewInt(0) 23 | c.args[0].SetString(num, 10) 24 | } 25 | } else { 26 | c.args[1] = big.NewInt(0) 27 | c.args[1].SetString(num, 10) 28 | } 29 | 30 | c.afterOp = true 31 | 32 | if c.args[1] == nil { 33 | c.lastOp = op 34 | c.Eval("set lastOp %{}", c.lastOp) 35 | return 36 | } 37 | 38 | switch c.lastOp { 39 | case "+": 40 | c.args[0] = c.args[0].Add(c.args[0], c.args[1]) 41 | case "-": 42 | c.args[0] = c.args[0].Sub(c.args[0], c.args[1]) 43 | case "/": 44 | c.args[0] = c.args[0].Div(c.args[0], c.args[1]) 45 | case "*": 46 | c.args[0] = c.args[0].Mul(c.args[0], c.args[1]) 47 | } 48 | 49 | c.lastOp = op 50 | c.args[1] = nil 51 | 52 | c.Eval("set lastOp %{}", c.lastOp) 53 | c.Eval("set calcText %{}", c.args[0]) 54 | if op == "=" { 55 | c.args[0] = nil 56 | } 57 | } 58 | 59 | func (c *calc) TCL_AppendNum(n string) { 60 | if c.afterOp { 61 | c.afterOp = false 62 | c.Eval("set calcText {}") 63 | } 64 | c.Eval("append calcText %{}", n) 65 | } 66 | 67 | func (c *calc) TCL_ClearAll() { 68 | c.args[0] = nil 69 | c.args[1] = nil 70 | c.afterOp = true 71 | c.lastOp = "" 72 | c.Eval("set lastOp {}; set calcText 0") 73 | } 74 | 75 | func (c *calc) TCL_PlusMinus() { 76 | var text string 77 | c.EvalAs(&text, "set calcText") 78 | if len(text) == 0 || text[0] == '0' { 79 | return 80 | } 81 | 82 | if text[0] == '-' { 83 | c.Eval("set calcText %{}", text[1:]) 84 | } else { 85 | c.Eval("set calcText -%{}", text) 86 | } 87 | } 88 | 89 | func main() { 90 | ir := gothic.NewInterpreter(` 91 | set lastOp {} 92 | set calcText 0 93 | wm title . "GoCalculator" 94 | 95 | ttk::frame .f 96 | ttk::entry .f.lastop -textvariable lastOp -justify center -state readonly -width 3 97 | ttk::entry .f.entry -textvariable calcText -justify right -state readonly 98 | 99 | grid .f.lastop .f.entry -sticky we 100 | grid columnconfigure .f 1 -weight 1 101 | 102 | ttk::button .0 -text 0 -command { go::AppendNum 0 } 103 | ttk::button .1 -text 1 -command { go::AppendNum 1 } 104 | ttk::button .2 -text 2 -command { go::AppendNum 2 } 105 | ttk::button .3 -text 3 -command { go::AppendNum 3 } 106 | ttk::button .4 -text 4 -command { go::AppendNum 4 } 107 | ttk::button .5 -text 5 -command { go::AppendNum 5 } 108 | ttk::button .6 -text 6 -command { go::AppendNum 6 } 109 | ttk::button .7 -text 7 -command { go::AppendNum 7 } 110 | ttk::button .8 -text 8 -command { go::AppendNum 8 } 111 | ttk::button .9 -text 9 -command { go::AppendNum 9 } 112 | ttk::button .pm -text +/- -command go::PlusMinus 113 | ttk::button .clear -text C -command go::ClearAll 114 | ttk::button .eq -text = -command { go::ApplyOp = } 115 | ttk::button .plus -text + -command { go::ApplyOp + } 116 | ttk::button .minus -text - -command { go::ApplyOp - } 117 | ttk::button .mul -text * -command { go::ApplyOp * } 118 | ttk::button .div -text / -command { go::ApplyOp / } 119 | 120 | grid .f - - .div -sticky nwes 121 | grid .7 .8 .9 .mul -sticky nwes 122 | grid .4 .5 .6 .minus -sticky nwes 123 | grid .1 .2 .3 .plus -sticky nwes 124 | grid .0 .pm .clear .eq -sticky nwes 125 | 126 | grid configure .f -sticky we 127 | 128 | foreach w [winfo children .] {grid configure $w -padx 3 -pady 3} 129 | 130 | foreach i {1 2 3 4} { grid rowconfigure . $i -weight 1 } 131 | foreach i {0 1 2 3} { grid columnconfigure . $i -weight 1 } 132 | 133 | bind . 0 { go::AppendNum 0 } 134 | bind . { go::AppendNum 0 } 135 | bind . 1 { go::AppendNum 1 } 136 | bind . { go::AppendNum 1 } 137 | bind . 2 { go::AppendNum 2 } 138 | bind . { go::AppendNum 2 } 139 | bind . 3 { go::AppendNum 3 } 140 | bind . { go::AppendNum 3 } 141 | bind . 4 { go::AppendNum 4 } 142 | bind . { go::AppendNum 4 } 143 | bind . 5 { go::AppendNum 5 } 144 | bind . { go::AppendNum 5 } 145 | bind . 6 { go::AppendNum 6 } 146 | bind . { go::AppendNum 6 } 147 | bind . 7 { go::AppendNum 7 } 148 | bind . { go::AppendNum 7 } 149 | bind . 8 { go::AppendNum 8 } 150 | bind . { go::AppendNum 8 } 151 | bind . 9 { go::AppendNum 9 } 152 | bind . { go::AppendNum 9 } 153 | bind . + { go::ApplyOp + } 154 | bind . { go::ApplyOp + } 155 | bind . - { go::ApplyOp - } 156 | bind . { go::ApplyOp - } 157 | bind . * { go::ApplyOp * } 158 | bind . { go::ApplyOp * } 159 | bind . / { go::ApplyOp / } 160 | bind . { go::ApplyOp / } 161 | bind . { go::ApplyOp = } 162 | bind . { go::ApplyOp = } 163 | bind . { go::ClearAll } 164 | `) 165 | ir.RegisterCommands("go", &calc{ 166 | Interpreter: ir, 167 | afterOp: true, 168 | }) 169 | <-ir.Done 170 | } 171 | -------------------------------------------------------------------------------- /fmt.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | // A special type which can be passed to Interpreter.Eval method family as the 14 | // only argument and in that case you can use named abbreviations within format 15 | // tags. 16 | type ArgMap map[string]interface{} 17 | 18 | func split_tag(tag string) (abbrev, format string) { 19 | abbrev = tag 20 | format = "" 21 | if i := strings.Index(tag, "%"); i != -1 { 22 | abbrev = tag[:i] 23 | format = tag[i:] 24 | } 25 | return 26 | } 27 | 28 | func write_arg_quoted(buf *bytes.Buffer, arg interface{}) { 29 | switch a := arg.(type) { 30 | case string: 31 | quote(buf, a) 32 | case error: 33 | quote(buf, a.Error()) 34 | case fmt.Stringer: 35 | quote(buf, a.String()) 36 | default: 37 | // TODO: it doesn't work in all cases, we still need to escape 38 | // various $ { } [ ] symbols 39 | fmt.Fprintf(buf, "%q", arg) 40 | } 41 | } 42 | 43 | func write_arg(buf *bytes.Buffer, arg interface{}, format string) { 44 | if format != "" { 45 | if format == "%q" { 46 | write_arg_quoted(buf, arg) 47 | return 48 | } else { 49 | fmt.Fprintf(buf, format, arg) 50 | } 51 | } else { 52 | fmt.Fprint(buf, arg) 53 | } 54 | } 55 | 56 | func write_tag(buf *bytes.Buffer, tag string, counter *int, args []interface{}) error { 57 | argnum := 0 58 | if tag == "" || strings.HasPrefix(tag, "%") { 59 | // no abbrev, means use counter 60 | } 61 | 62 | abbrev, format := split_tag(tag) 63 | if abbrev == "" { 64 | // no abbrev, means use the counter 65 | argnum = *counter 66 | (*counter)++ 67 | } else { 68 | // non-empty abbrev, let's convert it to the integer 69 | i, err := strconv.ParseInt(abbrev, 10, 16) 70 | if err != nil { 71 | return errors.New("gothic.sprintf: not-a-number tag abbrev") 72 | } 73 | 74 | argnum = int(i) 75 | } 76 | 77 | if argnum < 0 || argnum >= len(args) { 78 | return fmt.Errorf("gothic.sprintf: there is no argument with index %d", argnum) 79 | } 80 | 81 | write_arg(buf, args[argnum], format) 82 | return nil 83 | } 84 | 85 | func write_tag_argmap(buf *bytes.Buffer, tag string, argmap ArgMap) error { 86 | if tag == "" || strings.HasPrefix(tag, "%") { 87 | return errors.New("gothic.sprintf: empty format tag abbrev on gothic.ArgMap call form") 88 | } 89 | 90 | key, format := split_tag(tag) 91 | arg, found := argmap[key] 92 | if !found { 93 | return fmt.Errorf("gothic.sprintf: no argument %q in the ArgMap", key) 94 | } 95 | 96 | write_arg(buf, arg, format) 97 | return nil 98 | } 99 | 100 | func sprintf(buf *bytes.Buffer, format string, args ...interface{}) error { 101 | if len(args) == 0 { 102 | // quick path 103 | buf.WriteString(format) 104 | return nil 105 | } 106 | 107 | counter := 0 108 | argmap := false 109 | argmapvar := ArgMap(nil) 110 | if len(args) == 1 { 111 | argmapvar, argmap = args[0].(ArgMap) 112 | } 113 | 114 | offset := 0 115 | for { 116 | i := strings.Index(format[offset:], "%{") 117 | if i == -1 { 118 | // no more format tags, write the rest and return 119 | buf.WriteString(format[offset:]) 120 | break 121 | } 122 | 123 | // write everything before the formatter 124 | buf.WriteString(format[offset : offset+i]) 125 | 126 | // now let's fine the ending "}" 127 | b := offset + i + 2 128 | j := strings.Index(format[b:], "}") 129 | if j == -1 { 130 | return errors.New("gothic.sprintf: missing enclosing bracket in a formatter tag") 131 | } 132 | e := b + j 133 | 134 | var err error 135 | tag := format[b:e] 136 | if argmap { 137 | err = write_tag_argmap(buf, tag, argmapvar) 138 | } else { 139 | err = write_tag(buf, tag, &counter, args) 140 | } 141 | if err != nil { 142 | return err 143 | } 144 | offset = e + 1 145 | } 146 | return nil 147 | } 148 | 149 | func quote_rune(buf *bytes.Buffer, r rune, size int) { 150 | const lowerhex = "0123456789abcdef" 151 | if size == 1 && r == utf8.RuneError { 152 | // invalid rune, write the byte as is 153 | buf.WriteString(`\x`) 154 | buf.WriteByte(lowerhex[r>>4]) 155 | buf.WriteByte(lowerhex[r&0xF]) 156 | return 157 | } 158 | 159 | // first check for special TCL escaping cases 160 | switch r { 161 | case '{', '}', '[', ']', '"', '$', '\\': 162 | buf.WriteString("\\") 163 | buf.WriteRune(r) 164 | return 165 | } 166 | 167 | // other printable characters 168 | if unicode.IsPrint(r) { 169 | buf.WriteRune(r) 170 | return 171 | } 172 | 173 | // non-printable characters 174 | switch r { 175 | case '\a': 176 | buf.WriteString(`\a`) 177 | case '\b': 178 | buf.WriteString(`\b`) 179 | case '\f': 180 | buf.WriteString(`\f`) 181 | case '\n': 182 | buf.WriteString(`\n`) 183 | case '\r': 184 | buf.WriteString(`\r`) 185 | case '\t': 186 | buf.WriteString(`\t`) 187 | case '\v': 188 | buf.WriteString(`\v`) 189 | default: 190 | switch { 191 | case r < ' ': 192 | buf.WriteString(`\x`) 193 | buf.WriteByte(lowerhex[r>>4]) 194 | buf.WriteByte(lowerhex[r&0xF]) 195 | case r >= 0x10000: 196 | r = 0xFFFD 197 | fallthrough 198 | case r < 0x10000: 199 | buf.WriteString(`\u`) 200 | for s := 12; s >= 0; s -= 4 { 201 | buf.WriteByte(lowerhex[r>>uint(s)&0xF]) 202 | } 203 | } 204 | } 205 | } 206 | 207 | func quote(buf *bytes.Buffer, s string) { 208 | buf.WriteString(`"`) 209 | size := 0 210 | for offset := 0; offset < len(s); offset += size { 211 | r := rune(s[offset]) 212 | size = 1 213 | if r >= utf8.RuneSelf { 214 | r, size = utf8.DecodeRuneInString(s[offset:]) 215 | } 216 | 217 | quote_rune(buf, r, size) 218 | } 219 | buf.WriteString(`"`) 220 | } 221 | 222 | // Works exactly like Eval("%{%q}"), but instead of evaluating returns a quoted 223 | // string. 224 | func Quote(s string) string { 225 | var tmp bytes.Buffer 226 | quote(&tmp, s) 227 | return tmp.String() 228 | } 229 | 230 | // Quotes the rune just like if it was passed through Quote, the result is the 231 | // same as: Quote(string(r)). 232 | func QuoteRune(r rune) string { 233 | var tmp bytes.Buffer 234 | size := utf8.RuneLen(r) 235 | tmp.WriteString(`"`) 236 | quote_rune(&tmp, r, size) 237 | tmp.WriteString(`"`) 238 | return tmp.String() 239 | } 240 | -------------------------------------------------------------------------------- /interpreter.go: -------------------------------------------------------------------------------- 1 | package gothic 2 | 3 | /* 4 | #cgo !tcl85 LDFLAGS: -ltcl8.6 -ltk8.6 5 | #cgo !tcl85 CFLAGS: -I/usr/include/tcl8.6 6 | #cgo tcl85 LDFLAGS: -ltcl8.5 -ltk8.5 7 | #cgo tcl85 CFLAGS: -I/usr/include/tcl8.5 8 | #cgo darwin tcl85 CFLAGS: -I/opt/X11/include 9 | 10 | #include "interpreter.h" 11 | */ 12 | import "C" 13 | import ( 14 | "bytes" 15 | "errors" 16 | "fmt" 17 | "image" 18 | "reflect" 19 | "runtime" 20 | "strings" 21 | "sync" 22 | "unsafe" 23 | ) 24 | 25 | const ( 26 | debug = false 27 | alot = 999999 28 | ) 29 | 30 | //------------------------------------------------------------------------------ 31 | // Utils 32 | //------------------------------------------------------------------------------ 33 | 34 | // A handle that is used to manipulate a TCL interpreter. All handle methods 35 | // can be safely invoked from different threads. Each method invocation is 36 | // synchronous, it means that the method will be blocked until the action is 37 | // actually executed. 38 | // 39 | // `Done` field returns 0 when Tk's main loop exits. 40 | type Interpreter struct { 41 | ir *interpreter 42 | Done <-chan int 43 | } 44 | 45 | // Creates a new instance of the *gothic.Interpreter. But before interpreter 46 | // enters the Tk's main loop it will execute `init`. Init argument could be a 47 | // string or a function with this signature: "func(*gothic.Interpreter)". 48 | func NewInterpreter(init interface{}) *Interpreter { 49 | initdone := make(chan int) 50 | done := make(chan int) 51 | 52 | ir := new(Interpreter) 53 | ir.Done = done 54 | 55 | go func() { 56 | var err error 57 | runtime.LockOSThread() 58 | ir.ir, err = new_interpreter() 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | switch realinit := init.(type) { 64 | case string: 65 | err = ir.ir.eval([]byte(realinit)) 66 | if err != nil { 67 | panic(err) 68 | } 69 | case func(*Interpreter): 70 | realinit(ir) 71 | } 72 | 73 | initdone <- 0 74 | C.Tk_MainLoop() 75 | done <- 0 76 | }() 77 | 78 | <-initdone 79 | return ir 80 | } 81 | 82 | // Queue script for evaluation and wait for its completion. This function uses 83 | // printf-like formatting style, except that all the formatting tags are 84 | // enclosed within %{}. The reason for this is because tcl/tk uses %-based 85 | // formatting tags for its own purposes. Also it provides several extensions 86 | // like named and positional arguments. When no arguments are provided it will 87 | // evaluate the format string as-is. 88 | // 89 | // Another important difference between fmt.Sprintf and Eval is that when using 90 | // Eval, invalid format/arguments combination results in an error, while 91 | // fmt.Sprintf simply ignores misconfiguration. All the formatter generated 92 | // errors go through ErrorFilter, just like any other tcl error. 93 | // 94 | // The syntax for formatting tags is: 95 | // %{[[]]} 96 | // 97 | // Where: 98 | // 99 | // could be a number of the function argument (starting from 0) or a 100 | // name of the key in the provided gothic.ArgMap argument. It can 101 | // also be empty, in this case it uses internal counter, takes the 102 | // corresponding argument and increments that counter. 103 | // 104 | // Is the fmt.Sprintf format specifier, passed directly to 105 | // fmt.Sprintf as is (except for %q, see additional notes). 106 | // 107 | // Additional notes: 108 | // 109 | // 1. Formatter is extended to do TCL-specific quoting on %q format specifier. 110 | // 2. Named abbrev is only allowed when there is one argument and the type of 111 | // this argument is gothic.ArgMap. 112 | // 113 | // Examples: 114 | // 1. gothic.Eval("%{0} = %{1} + %{1}", 10, 5) 115 | // "10 = 5 + 5" 116 | // 2. gothic.Eval("%{} = %{%d} + %{1}", 20, 10) 117 | // "20 = 10 + 10" 118 | // 3. gothic.Eval("%{0%.2f} and %{%.2f}", 3.1415) 119 | // "3.14 and 3.14" 120 | // 4. gothic.Eval("[myfunction %{arg1} %{arg2}]", gothic.ArgMap{ 121 | // "arg1": 5, 122 | // "arg2": 10, 123 | // }) 124 | // "[myfunction 5 10]" 125 | // 5. gothic.Eval("%{%q}", "[command $variable]") 126 | // `"\[command \$variable\]"` 127 | func (ir *Interpreter) Eval(format string, args ...interface{}) error { 128 | // interpreter thread 129 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 130 | ir.ir.cmdbuf.Reset() 131 | err := sprintf(&ir.ir.cmdbuf, format, args...) 132 | if err != nil { 133 | return ir.ir.filt(err) 134 | } 135 | err = ir.ir.eval(ir.ir.cmdbuf.Bytes()) 136 | return ir.ir.filt(err) 137 | } 138 | 139 | // foreign thread 140 | buf := buffer_pool.get() 141 | err := sprintf(&buf, format, args...) 142 | if err != nil { 143 | buffer_pool.put(buf) 144 | return ir.ir.filt(err) 145 | } 146 | script := buf.Bytes() 147 | err = ir.ir.run_and_wait(func() error { 148 | return ir.ir.filt(ir.ir.eval(script)) 149 | }) 150 | buffer_pool.put(buf) 151 | return err 152 | } 153 | 154 | // Works the same way as Eval("%{}", byte_slice), but avoids unnecessary 155 | // buffering. 156 | func (ir *Interpreter) EvalBytes(s []byte) error { 157 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 158 | return ir.ir.filt(ir.ir.eval(s)) 159 | } 160 | return ir.ir.run_and_wait(func() error { 161 | return ir.ir.filt(ir.ir.eval(s)) 162 | }) 163 | } 164 | 165 | // Works exactly as Eval with exception that it writes the result of executed 166 | // code into `out`. 167 | func (ir *Interpreter) EvalAs(out interface{}, format string, args ...interface{}) error { 168 | // interpreter thread 169 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 170 | ir.ir.cmdbuf.Reset() 171 | err := sprintf(&ir.ir.cmdbuf, format, args...) 172 | if err != nil { 173 | return ir.ir.filt(err) 174 | } 175 | err = ir.ir.eval_as(out, ir.ir.cmdbuf.Bytes()) 176 | return ir.ir.filt(err) 177 | } 178 | 179 | // foreign thread 180 | buf := buffer_pool.get() 181 | err := sprintf(&buf, format, args...) 182 | if err != nil { 183 | buffer_pool.put(buf) 184 | return ir.ir.filt(err) 185 | } 186 | script := buf.Bytes() 187 | err = ir.ir.run_and_wait(func() error { 188 | return ir.ir.filt(ir.ir.eval_as(out, script)) 189 | }) 190 | buffer_pool.put(buf) 191 | return err 192 | } 193 | 194 | // Shortcut for `var i int; err := EvalAs(&i, format, args...)` 195 | func (ir *Interpreter) EvalAsInt(format string, args ...interface{}) (int, error) { 196 | var as int 197 | err := ir.EvalAs(&as, format, args...) 198 | return as, err 199 | } 200 | 201 | // Shortcut for `var s string; err := EvalAs(&s, format, args...)` 202 | func (ir *Interpreter) EvalAsString(format string, args ...interface{}) (string, error) { 203 | var as string 204 | err := ir.EvalAs(&as, format, args...) 205 | return as, err 206 | } 207 | 208 | // Shortcut for `var f float64; err := EvalAs(&f, format, args...)` 209 | func (ir *Interpreter) EvalAsFloat(format string, args ...interface{}) (float64, error) { 210 | var as float64 211 | err := ir.EvalAs(&as, format, args...) 212 | return as, err 213 | } 214 | 215 | // Shortcut for `var b bool; err := EvalAs(&b, format, args...)` 216 | func (ir *Interpreter) EvalAsBool(format string, args ...interface{}) (bool, error) { 217 | var as bool 218 | err := ir.EvalAs(&as, format, args...) 219 | return as, err 220 | } 221 | 222 | // Sets the TCL variable `name` to the `val`. Sometimes it's nice to be able to 223 | // avoid going through TCL's syntax. Especially for things like passing a whole 224 | // buffer of text to TCL. 225 | func (ir *Interpreter) Set(name string, val interface{}) error { 226 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 227 | return ir.ir.filt(ir.ir.set(name, val)) 228 | } 229 | return ir.ir.run_and_wait(func() error { 230 | return ir.ir.filt(ir.ir.set(name, val)) 231 | }) 232 | } 233 | 234 | // Every TCL error goes through the filter passed to this function. If you pass 235 | // nil, then no error filter is set. 236 | func (ir *Interpreter) ErrorFilter(filt func(error) error) { 237 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 238 | ir.ir.errfilt = filt 239 | } 240 | ir.ir.run_and_wait(func() error { 241 | ir.ir.errfilt = filt 242 | return nil 243 | }) 244 | } 245 | 246 | func (ir *Interpreter) UploadImage(name string, img image.Image) error { 247 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 248 | return ir.ir.filt(ir.ir.upload_image(name, img)) 249 | } 250 | return ir.ir.run_and_wait(func() error { 251 | return ir.ir.filt(ir.ir.upload_image(name, img)) 252 | }) 253 | } 254 | 255 | // Register a new TCL command called `name`. 256 | func (ir *Interpreter) RegisterCommand(name string, cbfunc interface{}) error { 257 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 258 | return ir.ir.filt(ir.ir.register_command(name, cbfunc)) 259 | } 260 | return ir.ir.run_and_wait(func() error { 261 | return ir.ir.filt(ir.ir.register_command(name, cbfunc)) 262 | }) 263 | } 264 | 265 | // Register multiple TCL command within the `name` namespace. The method uses 266 | // runtime reflection and registers only those methods of the `val` which have 267 | // one of the following prefixes: "TCL" or "TCL_". The name of the resulting 268 | // command doesn't include the prefix. 269 | func (ir *Interpreter) RegisterCommands(name string, val interface{}) error { 270 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 271 | return ir.ir.filt(ir.ir.register_commands(name, val)) 272 | } 273 | return ir.ir.run_and_wait(func() error { 274 | return ir.ir.filt(ir.ir.register_commands(name, val)) 275 | }) 276 | } 277 | 278 | // Unregisters (deletes) previously registered command `name`. 279 | func (ir *Interpreter) UnregisterCommand(name string) error { 280 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 281 | return ir.ir.filt(ir.ir.unregister_command(name)) 282 | } 283 | return ir.ir.run_and_wait(func() error { 284 | return ir.ir.filt(ir.ir.unregister_command(name)) 285 | }) 286 | } 287 | 288 | // Unregisters (deletes) previously registered command set within the `name` 289 | // namespace. 290 | func (ir *Interpreter) UnregisterCommands(name string) error { 291 | if C.Tcl_GetCurrentThread() == ir.ir.thread { 292 | return ir.ir.filt(ir.ir.unregister_commands(name)) 293 | } 294 | return ir.ir.run_and_wait(func() error { 295 | return ir.ir.filt(ir.ir.unregister_commands(name)) 296 | }) 297 | } 298 | 299 | //------------------------------------------------------------------------------ 300 | // interpreter 301 | //------------------------------------------------------------------------------ 302 | 303 | type global_handles_t struct { 304 | sync.Mutex 305 | handles handles 306 | } 307 | 308 | func (g *global_handles_t) get_handle_for_value(value interface{}) int { 309 | g.Lock() 310 | out := g.handles.get_handle_for_value(value) 311 | g.Unlock() 312 | return out 313 | } 314 | 315 | func (g *global_handles_t) free_handle(id int) { 316 | g.Lock() 317 | g.free_handle(id) 318 | g.Unlock() 319 | } 320 | 321 | func (g *global_handles_t) get(id int) interface{} { 322 | g.Lock() 323 | out := g.handles[id].Value 324 | g.Unlock() 325 | return out 326 | } 327 | 328 | var global_handles global_handles_t 329 | 330 | //------------------------------------------------------------------------------ 331 | // interpreter 332 | //------------------------------------------------------------------------------ 333 | 334 | type interpreter struct { 335 | C *C.Tcl_Interp 336 | 337 | errfilt func(error) error 338 | 339 | // id of this interpreter in global_handles table 340 | id int 341 | 342 | // buffer for C -> Go handles, every Go object passed to C is stored here 343 | handles handles 344 | 345 | // registered commands 346 | commands map[string]interface{} 347 | 348 | // registered method sets 349 | methods map[string]interface{} 350 | method_handles map[string][]int 351 | 352 | // just a buffer to avoid allocs in _gotk_go_command_handler 353 | valuesbuf []reflect.Value 354 | 355 | thread C.Tcl_ThreadId 356 | queue chan async_action 357 | cmdbuf bytes.Buffer 358 | } 359 | 360 | func release_interpreter(ir *interpreter) { 361 | global_handles.free_handle(ir.id) 362 | } 363 | 364 | func new_interpreter() (*interpreter, error) { 365 | ir := &interpreter{ 366 | C: C.Tcl_CreateInterp(), 367 | errfilt: func(err error) error { return err }, 368 | commands: make(map[string]interface{}), 369 | methods: make(map[string]interface{}), 370 | method_handles: make(map[string][]int), 371 | valuesbuf: make([]reflect.Value, 0, 10), 372 | queue: make(chan async_action, 50), 373 | thread: C.Tcl_GetCurrentThread(), 374 | } 375 | 376 | status := C.Tcl_Init(ir.C) 377 | if status != C.TCL_OK { 378 | return nil, errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 379 | } 380 | 381 | status = C.Tk_Init(ir.C) 382 | if status != C.TCL_OK { 383 | return nil, errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 384 | } 385 | 386 | ir.id = global_handles.get_handle_for_value(ir) 387 | runtime.SetFinalizer(ir, release_interpreter) 388 | return ir, nil 389 | } 390 | 391 | func (ir *interpreter) filt(err error) error { 392 | errfilt := ir.errfilt 393 | ir.errfilt = nil 394 | if errfilt != nil { 395 | err = errfilt(err) 396 | } 397 | ir.errfilt = errfilt 398 | return err 399 | } 400 | 401 | func (ir *interpreter) eval(script []byte) error { 402 | if len(script) == 0 { 403 | return nil 404 | } 405 | status := C.Tcl_EvalEx(ir.C, (*C.char)(unsafe.Pointer(&script[0])), 406 | C.int(len(script)), 0) 407 | if status != C.TCL_OK { 408 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 409 | } 410 | return nil 411 | } 412 | 413 | func (ir *interpreter) eval_as(out interface{}, script []byte) error { 414 | pv := reflect.ValueOf(out) 415 | if pv.Kind() != reflect.Ptr || pv.IsNil() { 416 | panic("gothic: EvalAs expected a non-nil pointer argument") 417 | } 418 | v := pv.Elem() 419 | 420 | err := ir.eval(script) 421 | if err != nil { 422 | return err 423 | } 424 | 425 | return ir.tcl_obj_to_go_value(C.Tcl_GetObjResult(ir.C), v) 426 | } 427 | 428 | func go_value_to_tcl_obj(value interface{}) *C.Tcl_Obj { 429 | v := reflect.ValueOf(value) 430 | switch v.Kind() { 431 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 432 | return C.Tcl_NewWideIntObj(C.Tcl_WideInt(v.Int())) 433 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 434 | return C.Tcl_NewWideIntObj(C.Tcl_WideInt(v.Uint())) 435 | case reflect.Float32, reflect.Float64: 436 | return C.Tcl_NewDoubleObj(C.double(v.Float())) 437 | case reflect.Bool: 438 | if v.Bool() { 439 | return C.Tcl_NewBooleanObj(1) 440 | } 441 | return C.Tcl_NewBooleanObj(0) 442 | case reflect.String: 443 | s := v.String() 444 | sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) 445 | return C.Tcl_NewStringObj((*C.char)(unsafe.Pointer(sh.Data)), C.int(len(s))) 446 | } 447 | return nil 448 | } 449 | 450 | func (ir *interpreter) set(name string, value interface{}) error { 451 | obj := go_value_to_tcl_obj(value) 452 | if obj == nil { 453 | return errors.New("gothic: cannot convert Go value to TCL object") 454 | } 455 | 456 | cname := C.CString(name) 457 | defer C.free(unsafe.Pointer(cname)) 458 | 459 | obj = C.Tcl_SetVar2Ex(ir.C, cname, nil, obj, C.TCL_LEAVE_ERR_MSG) 460 | if obj == nil { 461 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 462 | } 463 | return nil 464 | } 465 | 466 | func (ir *interpreter) upload_image(name string, img image.Image) error { 467 | var buf bytes.Buffer 468 | err := sprintf(&buf, "image create photo %{}", name) 469 | if err != nil { 470 | return err 471 | } 472 | 473 | nrgba, ok := img.(*image.NRGBA) 474 | if !ok { 475 | // let's do it slowpoke 476 | bounds := img.Bounds() 477 | nrgba = image.NewNRGBA(bounds) 478 | for x := 0; x < bounds.Max.X; x++ { 479 | for y := 0; y < bounds.Max.Y; y++ { 480 | nrgba.Set(x, y, img.At(x, y)) 481 | } 482 | } 483 | } 484 | 485 | cname := C.CString(name) 486 | defer C.free(unsafe.Pointer(cname)) 487 | 488 | handle := C.Tk_FindPhoto(ir.C, cname) 489 | if handle == nil { 490 | err := ir.eval(buf.Bytes()) 491 | if err != nil { 492 | return err 493 | } 494 | handle = C.Tk_FindPhoto(ir.C, cname) 495 | if handle == nil { 496 | return errors.New("failed to create an image handle") 497 | } 498 | } 499 | 500 | imgdata := C.CBytes(nrgba.Pix) 501 | defer C.free(imgdata) 502 | 503 | block := C.Tk_PhotoImageBlock{ 504 | (*C.uchar)(imgdata), 505 | C.int(nrgba.Rect.Max.X), 506 | C.int(nrgba.Rect.Max.Y), 507 | C.int(nrgba.Stride), 508 | 4, 509 | [...]C.int{0, 1, 2, 3}, 510 | } 511 | 512 | status := C.Tk_PhotoPutBlock(ir.C, handle, &block, 0, 0, 513 | C.int(nrgba.Rect.Max.X), C.int(nrgba.Rect.Max.Y), 514 | C.TK_PHOTO_COMPOSITE_SET) 515 | if status != C.TCL_OK { 516 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 517 | } 518 | return nil 519 | } 520 | 521 | func (ir *interpreter) tcl_obj_to_go_value(obj *C.Tcl_Obj, v reflect.Value) error { 522 | var status C.int 523 | 524 | switch v.Kind() { 525 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 526 | var out C.Tcl_WideInt 527 | status = C.Tcl_GetWideIntFromObj(ir.C, obj, &out) 528 | if status == C.TCL_OK { 529 | v.SetInt(int64(out)) 530 | } 531 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 532 | var out C.Tcl_WideInt 533 | status = C.Tcl_GetWideIntFromObj(ir.C, obj, &out) 534 | if status == C.TCL_OK { 535 | v.SetUint(uint64(out)) 536 | } 537 | case reflect.String: 538 | var n C.int 539 | out := C.Tcl_GetStringFromObj(obj, &n) 540 | v.SetString(C.GoStringN(out, n)) 541 | case reflect.Float32, reflect.Float64: 542 | var out C.double 543 | status = C.Tcl_GetDoubleFromObj(ir.C, obj, &out) 544 | if status == C.TCL_OK { 545 | v.SetFloat(float64(out)) 546 | } 547 | case reflect.Bool: 548 | var out C.int 549 | status = C.Tcl_GetBooleanFromObj(ir.C, obj, &out) 550 | if status == C.TCL_OK { 551 | v.SetBool(out == 1) 552 | } 553 | default: 554 | return fmt.Errorf("gothic: cannot convert TCL object to Go type: %s", v.Type()) 555 | } 556 | 557 | if status != C.TCL_OK { 558 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 559 | } 560 | return nil 561 | } 562 | 563 | //------------------------------------------------------------------------------ 564 | // interpreter.commands 565 | //------------------------------------------------------------------------------ 566 | 567 | //export _gotk_go_command_handler 568 | func _gotk_go_command_handler(clidataup unsafe.Pointer, objc C.int, objv unsafe.Pointer) C.int { 569 | // TODO: There is an idea of optimizing everything by a large margin, 570 | // we can preprocess the type of a command in RegisterCommand function 571 | // and then avoid calling reflect.New for every argument passed to that 572 | // function. And we can even do additional error checks for unsupported 573 | // argument types and handle multiple return values case. 574 | 575 | clidata := (*C.GoTkClientData)(clidataup) 576 | ir := global_handles.get(int(clidata.go_interp)).(*interpreter) 577 | args := (*(*[alot]*C.Tcl_Obj)(objv))[1:objc] 578 | cb := ir.handles[clidata.h0].Value 579 | f := reflect.ValueOf(cb) 580 | ft := f.Type() 581 | 582 | ir.valuesbuf = ir.valuesbuf[:0] 583 | for i, n := 0, ft.NumIn(); i < n; i++ { 584 | in := ft.In(i) 585 | 586 | // use default value, if there is not enough args 587 | if len(args) <= i { 588 | ir.valuesbuf = append(ir.valuesbuf, reflect.New(in).Elem()) 589 | continue 590 | } 591 | 592 | v := reflect.New(in).Elem() 593 | err := ir.tcl_obj_to_go_value(args[i], v) 594 | if err != nil { 595 | C._gotk_c_tcl_set_result(ir.C, C.CString(err.Error())) 596 | return C.TCL_ERROR 597 | } 598 | 599 | ir.valuesbuf = append(ir.valuesbuf, v) 600 | } 601 | 602 | // TODO: handle return value 603 | f.Call(ir.valuesbuf) 604 | 605 | return C.TCL_OK 606 | } 607 | 608 | //export _gotk_go_method_handler 609 | func _gotk_go_method_handler(clidataup unsafe.Pointer, objc C.int, objv unsafe.Pointer) C.int { 610 | // TODO: There is an idea of optimizing everything by a large margin, 611 | // we can preprocess the type of a command in RegisterCommand function 612 | // and then avoid calling reflect.New for every argument passed to that 613 | // function. And we can even do additional error checks for unsupported 614 | // argument types and handle multiple return values case. 615 | 616 | clidata := (*C.GoTkClientData)(clidataup) 617 | ir := global_handles.get(int(clidata.go_interp)).(*interpreter) 618 | args := (*(*[alot]*C.Tcl_Obj)(objv))[1:objc] 619 | recv := ir.handles[clidata.h0].Value 620 | meth := ir.handles[clidata.h1].Value 621 | f := reflect.ValueOf(meth) 622 | ft := f.Type() 623 | 624 | ir.valuesbuf = ir.valuesbuf[:0] 625 | ir.valuesbuf = append(ir.valuesbuf, reflect.ValueOf(recv)) 626 | for i, n := 1, ft.NumIn(); i < n; i++ { 627 | ia := i - 1 628 | in := ft.In(i) 629 | 630 | // use default value, if there is not enough args 631 | if len(args) <= ia { 632 | ir.valuesbuf = append(ir.valuesbuf, reflect.New(in).Elem()) 633 | continue 634 | } 635 | 636 | v := reflect.New(in).Elem() 637 | err := ir.tcl_obj_to_go_value(args[ia], v) 638 | if err != nil { 639 | C._gotk_c_tcl_set_result(ir.C, C.CString(err.Error())) 640 | return C.TCL_ERROR 641 | } 642 | 643 | ir.valuesbuf = append(ir.valuesbuf, v) 644 | } 645 | 646 | // TODO: handle return value 647 | f.Call(ir.valuesbuf) 648 | 649 | return C.TCL_OK 650 | } 651 | 652 | //export _gotk_go_command_deleter 653 | func _gotk_go_command_deleter(data unsafe.Pointer) { 654 | clidata := (*C.GoTkClientData)(data) 655 | ir := global_handles.get(int(clidata.go_interp)).(*interpreter) 656 | ir.handles.free_handle(int(clidata.h0)) 657 | } 658 | 659 | func (ir *interpreter) register_command(name string, cbfunc interface{}) error { 660 | typ := reflect.TypeOf(cbfunc) 661 | if typ.Kind() != reflect.Func { 662 | return errors.New("gothic: RegisterCommand only accepts func type as a second argument") 663 | } 664 | if _, ok := ir.commands[name]; ok { 665 | return errors.New("gothic: command with the same name was already registered") 666 | } 667 | ir.commands[name] = cbfunc 668 | cname := C.CString(name) 669 | defer C.free(unsafe.Pointer(cname)) 670 | C._gotk_c_add_command(ir.C, cname, C.int(ir.id), C.int(ir.handles.get_handle_for_value(cbfunc))) 671 | return nil 672 | } 673 | 674 | func (ir *interpreter) register_commands(name string, val interface{}) error { 675 | if _, ok := ir.methods[name]; ok { 676 | return errors.New("gothic: method set with the same name was already registered") 677 | } 678 | ir.methods[name] = val 679 | t := reflect.TypeOf(val) 680 | valh := ir.handles.get_handle_for_value(val) 681 | ir.method_handles[name] = append(ir.method_handles[name], valh) 682 | for i, n := 0, t.NumMethod(); i < n; i++ { 683 | m := t.Method(i) 684 | if !strings.HasPrefix(m.Name, "TCL") { 685 | continue 686 | } 687 | 688 | subname := m.Name[3:] 689 | if strings.HasPrefix(m.Name, "TCL_") { 690 | subname = m.Name[4:] 691 | } 692 | 693 | cname := C.CString(name + "::" + subname) 694 | defer C.free(unsafe.Pointer(cname)) 695 | 696 | mh := ir.handles.get_handle_for_value(m.Func.Interface()) 697 | ir.method_handles[name] = append(ir.method_handles[name], mh) 698 | C._gotk_c_add_method(ir.C, cname, C.int(ir.id), C.int(valh), C.int(mh)) 699 | } 700 | return nil 701 | } 702 | 703 | func (ir *interpreter) unregister_command(name string) error { 704 | if _, ok := ir.commands[name]; !ok { 705 | return errors.New("gothic: trying to unregister a non-existent command") 706 | } 707 | 708 | cname := C.CString(name) 709 | defer C.free(unsafe.Pointer(cname)) 710 | 711 | status := C.Tcl_DeleteCommand(ir.C, cname) 712 | if status != C.TCL_OK { 713 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 714 | } 715 | delete(ir.commands, name) 716 | return nil 717 | } 718 | 719 | func (ir *interpreter) unregister_commands(name string) error { 720 | if _, ok := ir.methods[name]; !ok { 721 | return errors.New("gothic: trying to unregister a non-existent method set") 722 | } 723 | val := ir.methods[name] 724 | t := reflect.TypeOf(val) 725 | for i, n := 0, t.NumMethod(); i < n; i++ { 726 | m := t.Method(i) 727 | if !strings.HasPrefix(m.Name, "TCL") { 728 | continue 729 | } 730 | 731 | subname := m.Name[3:] 732 | if strings.HasPrefix(m.Name, "TCL_") { 733 | subname = m.Name[4:] 734 | } 735 | 736 | cname := C.CString(name + "::" + subname) 737 | defer C.free(unsafe.Pointer(cname)) 738 | 739 | status := C.Tcl_DeleteCommand(ir.C, cname) 740 | if status != C.TCL_OK { 741 | return errors.New(C.GoString(C.Tcl_GetStringResult(ir.C))) 742 | } 743 | } 744 | delete(ir.methods, name) 745 | for _, id := range ir.method_handles[name] { 746 | ir.handles.free_handle(id) 747 | } 748 | delete(ir.method_handles, name) 749 | return nil 750 | } 751 | 752 | //------------------------------------------------------------------------------ 753 | // interpreter.async 754 | //------------------------------------------------------------------------------ 755 | 756 | type async_action struct { 757 | result *error 758 | action func() error 759 | cond *sync.Cond 760 | } 761 | 762 | func (ir *interpreter) run_and_wait(action func() error) (err error) { 763 | cond := sync.NewCond(&sync.Mutex{}) 764 | cond.L.Lock() 765 | 766 | // send event 767 | ir.queue <- async_action{result: &err, action: action, cond: cond} 768 | ev := C._gotk_c_new_async_event(C.int(ir.id)) 769 | C.Tcl_ThreadQueueEvent(ir.thread, ev, C.TCL_QUEUE_TAIL) 770 | C.Tcl_ThreadAlert(ir.thread) 771 | 772 | // wait for result 773 | cond.Wait() 774 | cond.L.Unlock() 775 | 776 | return 777 | } 778 | 779 | //export _gotk_go_async_handler 780 | func _gotk_go_async_handler(ev unsafe.Pointer, flags C.int) C.int { 781 | if flags != C.TK_ALL_EVENTS { 782 | return 0 783 | } 784 | event := (*C.GoTkAsyncEvent)(ev) 785 | ir := global_handles.get(int(event.go_interp)).(*interpreter) 786 | action := <-ir.queue 787 | if action.result == nil { 788 | action.action() 789 | } else { 790 | *action.result = action.action() 791 | } 792 | action.cond.L.Lock() 793 | action.cond.Signal() 794 | action.cond.L.Unlock() 795 | return 1 796 | } 797 | --------------------------------------------------------------------------------