chuk01
chuk01

Reputation: 31

How to get out of an infinite while loop in TCL by keyboard events?

I've the following TCL infinite while loop. I want to exit by pressing a key or timed out by itself. I can't get the key press to exit. What is missing in the code for it to work?

fconfigure stdin -blocking 0
set time 20000
set start_time [clock clicks -milliseconds]
while  { 0 < 1 } {
    fileevent stdin readable {
        exit
    }
    set end_time [clock clicks -milliseconds]
    set dt [expr $end_time-$start_time]
    if {$dt >= $time} {
        exit
    }
}

Upvotes: 3

Views: 4459

Answers (2)

Peter Lewerin
Peter Lewerin

Reputation: 13252

Specific solution to show a number of screens in sequence, using the space bar to switch to the next screen, and automatically switching screens after five seconds.

First, some dummy definitions to simulate screens. I recommend using command procedures like this to setup each individual screen, but in the actual script they will of course be more complex than these.

grid [label .a] -sticky news
proc screen1 {} {.a configure -text 1}
proc screen2 {} {.a configure -text 2}
proc screen3 {} {.a configure -text 3}
proc screen4 {} {.a configure -text 4}
proc screen5 {} {.a configure -text 5}

Now, the screen switching mechanism (a basic, simple one). The nextScreen command cancels the running timer, if any. Then a global counter, n, is incremented (it doesn’t matter if it doesn’t exist the first time, incr will create it and give it the value 1). Then we check if there is a command called screen$n: if not, the command returns, ending the sequence. If it exists, it is called, and then the timer is set to call nextScreen after five seconds.

proc nextScreen {} {
    global n afterId
    catch {after cancel $afterId}
    incr n
    if {[llength [info commands screen$n]] < 1} {
        return
    }
    screen$n
    set afterId [after 5000 nextScreen]
}

Set the space bar to activate nextScreen.

bind . <space> nextScreen

Call nextScreen to start the sequence (or don’t, in which case space needs to be pressed to start it).

nextScreen

Note: no infinite loop needed (the event loop is the infinite loop here). We also don’t need to take any action to start the event loop: it starts automatically in the windowing shell.

Older, generic solutions to the original problem:

GUI application

You can start the event loop by calling

vwait forever

(forever is just a cute variable name) but in a GUI application you don't need to do that, since it will be started automatically.

If you want to control your script by single-key input, it’s hard to get that to work by using fileevent stdin. OTOH, it’s really easy to bind a keystroke event:

bind . <Key> {
    exit
}

If you want to break with just key “k”, you bind to <Key-k> instead.

Having the loop end automatically after 20 seconds is easy too:

after 20000 exit

Putting it together:

bind . <Key> {
    exit
}
after 20000 exit
while true {
    puts -nonewline .
    update
}

The puts is there to show that the loop is alive, the update is there to ensure that events are still processed.

CLI application

For a CLI application, I really believe you should use Expect if possible. Your platform matters a lot here.

You will need to set stdin to not use blocking or buffering:

chan configure stdin -blocking 0 -buffering none

Then you need to convince the operating system to not use buffering as well (aka “raw input”). On Unix/Linux I’m told one uses stty for this. Not having used Linux since Slackware came on 60+ floppy disks, I really have no idea.

For a Windows solution, you can use twapi to set raw input mode. Note that you need to flush stdin or it will be readable from the start.

(This is tested and works on Win7 and Win10 with twapi 3.0.32. I’m not certain that this is the best solution. Note that twapi has its own commands to replace fileevent.)

package require twapi

twapi::modify_console_input_mode stdin -lineinput false -echoinput false
twapi::flush_console_input stdin

Run this code before the generic script below.

Otherwise, the code is similar (pressing Enter key stops the loop immediately, any other key needs to be followed by Enter unless you’ve managed to set up raw input):

after 0 {
    chan configure stdin -blocking 0 -buffering none
    fileevent stdin readable {
        exit
    }
    after 20000 exit
    while true {
        puts -nonewline .
        update
    }
}
vwait forever

Documentation: after, bind, catch, chan, exit, fileevent, global, grid, if, incr, info, label (widget), llength, package, proc, puts, return, set, twapi (package), update, vwait, while

Upvotes: 1

Dinesh
Dinesh

Reputation: 16428

This is just one another way of getting the expected result, but only with the combination of Control keys.

With Expect's trap command, we can handle the signals such as SIGINT (Ctrl+C).

set loop_var 1
trap {set ::loop_var 0;puts "Resetting the loop_var to zero"} SIGINT
while {$loop_var} {
        puts "loop_var : $loop_var"
        after 1000
}
puts "Out of infinite!!!"

Once you press the Ctrl+C, the loop will break.

Reference : Exploring Expect

Upvotes: 1

Related Questions