tcl
#define tcl: \
I-------------------------------------------------\
I-------------------------------------------------\
I-------------------------------------------------\
I /$$$$$$$$ /$$$$$$ /$$ \
I |__ $$__//$$__ $$| $$ \
I | $$ | $$ \__/| $$ \
I | $$ | $$ | $$ \
I | $$ | $$ | $$ \
I | $$ | $$ $$| $$ \
I | $$ | $$$$$$/| $$$$$$$$ \
I |__/ \______/ |________/ \
I-------------------------------------------------\
I-------------------------------------------------\
I-------------------------------------------------I
Tcl/Tk 8.5 Programming Cookbook by Bert Wheeler
• interpreted
• integrates very (VERY) closely with C
• primarily used for rapid prototyping and/or creating cross-platform GUIs
• the Tcl interactive shell uses "%" as ${PS1}; except to see it in examples
Programs: Programs:
tclsh [FILE]+ : runs the provided scripts or launches an interactive shell
tcc4tcl : tcl shell fork running a patched version of tcc;
allows nesting native C in tcl scripts which will be JIT compiled
Syntax: Syntax:
• system shell / Lisp like
{} : encapsulate spaces in arguments
[] : eval a command in-place
Variables:
set <name> <value> : define variable
$ : access variable value
Functions:
proc <name> {[arguments]+} { [...] }
• proc builtin function and not a keyword
Return_codes: Return_codes:
enum {
TCL_OK
TCL_ERROR
TCL_RETURN
TCL_BREAK
TCL_CONTINUE
};
Builtins: Builtins:
Variables: Variables:
argc
argv
argv0
env
TCL_platform
TCL_interactive
retval
Functions: Functions:
IO: IO:
puts
— nonewline
error
open
fconfigure
read
close
Math: Math:
abs arg
acos
ceil
cos
double
floor
fmod
hypot
int
isqrt
expr
log
log10
min
pow
sin
sinh
sqrt
tan
tanh
wide
Variables: Variables:
global
set
unset
incr
Control: Control:
if
for
foreach
while
continue
break
eval
catch
Strings: Strings:
string [verb]
is
repeat
range
last
match
trim
append
format
scan
subst
regexp
regsub
Lists: Lists:
list
concat
join
lappend
lassign
lindex
linsert
llength
lrange
lrepeat
lreverse
lset
lsort
split
Dictionary: Dictionary:
dict
create
append
get
merge
TK: TK:
package require Tk
• "graphical Tool Kit"
• used by python's tkinter
• realistically the only thing keeping Tcl alive
• cross-platform
• from the 90s, looks like the 90s (READ: peak soul)
Widgets: Widgets:
wm
title <window> <string>
destroy
<widget> <wpath> <options>
button
label
toplevel
frame
entry
listbox
<wpath>
• '.' marks the inplicitly created root window
• each widget must name itself with the basename
<(common) options>
— text
— command
pack <widget>
— side [top|left|right|bottom]
+----------------+ +----------------+
| Top | | | Top | |
|----------------| | |--------| |
| L | | R | | L | | R |
| e | | i | | e | | i |
| f | | g | | f | | g |
| t | | h | | t | | h |
| | | t | | | | t |
|----------------| | |--------| |
| Bottom | | | Bottom | |
+----------------+ +----------------+
grid
Popups: Popups:
tk_messageBox
tk_dialog
tk_chooseColor
tk_chooseDirectory
tk_getOpenFile : existing file only
tk_getSaveFile : allows for new
C_integration:
— as mentioned the C integration is nice:
• communication is painless
• Tcl is small enough to ship with an embedded interpreter
• Tcl integrates poorly with C++ directly,
the recommended way is to have your code interacting with Tcl
be compiled as C and linking it to C++
// @BAKE gcc -o $*.out $@ -std=c23 $(pkg-config --cflags --libs tcl tk)
/* This is an implementation of Banana Clicker in C using a Tcl/Tk GUI.
* This program is for demonstration purposes and is in the Public Domain.
*
* The afore mentioned game was chosen because its extremely simple:
* You click the banana on screen and your score goes up by 1.
*
* Tcl will be storing no state, it only handles the displaying.
* This is accomplished by running an interpreter on its own thread.
* Whenever the state must be updated according to user input,
* a callback writes global state.
* We mark our global state compile-unit local because
* that would be the smart thing to do if we had more
* than 1 source file (given a more complex task).
* Alternatively, to handle multiple display instances,
* we could employ a map of interpreters and state objects.
*/
#include <stdlib.h>
#include <pthread.h>
#include <tcl.h>
#include <tk.h>
// Our entire inner state. Maps directly to the user score.
static size_t click_counter = 0;
/* To interact with C, we have to define Tcl commands.
* These behave exactly like regular procedures,
* we can call them seamlessly from our front-end script.
*/
// Macros to hide ugly Tcl API implementation details
#define TCL_ARGS ClientData clientData, Tcl_Interp *interp, int argc, const char **argv
#define TCL_EASY_CREATE_COMMAND(c) \
Tcl_CreateCommand(interp, #c, Tcl_ ## c, (ClientData)NULL, (void (*)(void*))NULL);
static
int Tcl_cIncrementCounter(TCL_ARGS) {
++click_counter;
return TCL_OK;
}
static
int Tcl_cGetCounter(TCL_ARGS) {
char r[12]; // For simplicity we only account for 11 digits + '\0'
sprintf(r, "%ld", click_counter);
/* We have to tell Tcl how to manage the memory.
* Below I marked `r` as a value Tcl should copy and manage herself.
* Alternative options would be:
* specifying it as static,
* or memory returned by `Tcl_Alloc`.
*/
Tcl_SetResult(interp, r, TCL_VOLATILE);
return TCL_OK;
}
/* Since I would like this program to fit in a single file,
* we define our script as a string.
* During prototyping one would instead have "gui.tcl" and
* call `Tcl_EvalFile` on it.
* For production, one would embed the script and statically link to Tcl/Tk.
*/
const char * const tcl_script =
"# @BAKE tclsh $@\n"
"package require Tk\n"
"\n"
"# If not invoked from C tnen run in \"testing\" mode\n"
"# Very useful trick for prototyping\n"
"if {![info exists ::WRAPPED]} {\n"
" proc cIncrementCounter {} {}\n"
" proc cGetCounter {} { return 0 }\n"
"}\n"
"\n"
"wm title . \"Tcl Banana Clicker\"\n"
"\n"
"# Create a terribly illustrated banana\n"
"canvas .banana -width 600 -height 600\n"
".banana create rectangle 100 200 500 300 -fill yellow\n"
".banana create rectangle 500 210 540 240 -fill black\n"
"bind .banana <Button-1> {cIncrementCounter}\n"
"pack .banana\n"
"\n"
"# Create a score message\n"
"label .score\n"
".score configure -font \"TkDefaultFont 20\"\n"
"pack .score\n"
"\n"
"# Loop in an event-driven fashion to query state from C\n"
"proc updateState {} {\n"
" .score configure -text \"Your current score is: [cGetCounter]\"\n"
" after 100 updateState\n"
"}\n"
"after 100 updateState\n"
"\n"
"# Quit if the window is closed\n"
"bind . <Destroy> {exit}\n"
;
/* As mentioned, the interpreter has to run on its own thread.
* `Tk_MainLoop` blocks.
*/
static void tcl_run(void);
void tcl_loop(void) {
pthread_t tcl_thread;
pthread_create(&tcl_thread, NULL, (void* (*)(void*))tcl_run, (void*)NULL);
}
static
void tcl_run(void) {
Tcl_Interp * interp = Tcl_CreateInterp();
if (interp == NULL) {
fprintf(stderr, "Can't create Tcl interpreter\n");
exit(1);
}
Tcl_Init(interp);
Tk_Init(interp);
// Disable our testing trick
Tcl_SetVar(interp, "WRAPPED", "true", 0);
TCL_EASY_CREATE_COMMAND(cIncrementCounter);
TCL_EASY_CREATE_COMMAND(cGetCounter);
int result = Tcl_Eval(interp, tcl_script);
if (result == TCL_ERROR) {
fprintf(stderr, "Tcl script execution failed: %s\n", Tcl_GetStringResult(interp));
exit(1);
}
Tk_MainLoop();
}
signed main() {
// Run the front-end first
tcl_loop();
/* Wait indefinitely for the front-end.
* If we had a real game, this would be our main loop.
* Obviously we could perform arbitrary computations,
* and Tcl would read it asynchronously.
* Since we share our process with the interpreter,
* and it binds the window close to the exit system-call,
* our program will quit as expected.
* Alternatively something like `bool do_run` could be updated
* for a more graceful shutdown.
*/
while (true) { ; }
return 0;
}