cmdr
Changes On Branch say-more
Not logged in
Bounty program for improvements to Tcl and certain Tcl packages.

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Changes In Branch say-more Excluding Merge-Ins

This is equivalent to a diff from 860ef7cfb3 to 4c80a42e4d

2015-05-08
20:27
Fix officer tests, new -- option. check-in: 00a6db24bb user: aku tags: trunk
00:17
Begin support of negative/inverted aliases for boolean options. check-in: a09daa498b user: andreask tags: neg-aliases
2015-05-01
23:30
Notes on current state and future directions to work on. Leaf check-in: 4c80a42e4d user: andreask tags: say-more
22:55
New command for output to stderr, plus two higher-level helpers for headers and banners. check-in: 64bb90174f user: andreask tags: say-more
2015-04-21
18:11
Draft work on an output package for basic terminal control, and animations (like progress-bars, barber-poles, counters, etc.) check-in: faf4b58f8c user: andreask tags: say-more
2015-04-17
23:24
history - Added missing docs. check-in: 860ef7cfb3 user: andreask tags: trunk
22:18
Docs - Separate package version information from the project version number. Regenerated docs. check-in: c8ea7fb2f9 user: andreask tags: trunk

Changes to ask.tcl.

11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
# Meta description Commands to interact with the user in various
# Meta description simple ways, for a terminal.
# Meta subject {command line} tty interaction terminal
# Meta require {Tcl 8.5-}
# Meta require debug
# Meta require debug::caller
# Meta require cmdr::color

# Meta require try
# Meta require linenoise
# Meta require struct::matrix
# Meta require textutil::adjust
# @@ Meta End

# # ## ### ##### ######## ############# #####################
## Requisites

package require Tcl 8.5
package require cmdr::color

package require debug
package require debug::caller
package require linenoise
package require try
package require struct::matrix
package require textutil::adjust

namespace eval ::cmdr {
    namespace export ask
}
namespace eval ::cmdr::ask {
    namespace export string string/extended string* yn choose menu
    namespace ensemble create

    namespace import ::cmdr::color

}

# # ## ### ##### ######## ############# #####################

debug define cmdr/ask
debug level  cmdr/ask
debug prefix cmdr/ask {[debug caller] | }







>











>















>







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Meta description Commands to interact with the user in various
# Meta description simple ways, for a terminal.
# Meta subject {command line} tty interaction terminal
# Meta require {Tcl 8.5-}
# Meta require debug
# Meta require debug::caller
# Meta require cmdr::color
# Meta require cmdr::say
# Meta require try
# Meta require linenoise
# Meta require struct::matrix
# Meta require textutil::adjust
# @@ Meta End

# # ## ### ##### ######## ############# #####################
## Requisites

package require Tcl 8.5
package require cmdr::color
package require cmdr::say
package require debug
package require debug::caller
package require linenoise
package require try
package require struct::matrix
package require textutil::adjust

namespace eval ::cmdr {
    namespace export ask
}
namespace eval ::cmdr::ask {
    namespace export string string/extended string* yn choose menu
    namespace ensemble create

    namespace import ::cmdr::color
    namespace import ::cmdr::say
}

# # ## ### ##### ######## ############# #####################

debug define cmdr/ask
debug level  cmdr/ask
debug prefix cmdr/ask {[debug caller] | }
156
157
158
159
160
161
162



163







164
165
166
167
168
169
170
	    if {$e eq "aborted"} {
		error Interrupted error SIGTERM
	    }
	    return {*}${o} $e
	}
	if {$response eq {}} { set response $default }
	if {[::string is bool $response]} break



	puts stdout [Wrap "You must choose \"yes\" or \"no\""]







    }

    return $response
}

proc ::cmdr::ask::choose {query choices {default {}}} {
    debug.cmdr/ask {}







>
>
>
|
>
>
>
>
>
>
>







159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
	    if {$e eq "aborted"} {
		error Interrupted error SIGTERM
	    }
	    return {*}${o} $e
	}
	if {$response eq {}} { set response $default }
	if {[::string is bool $response]} break

	# Show error for a second, then move back to the interaction
	# line and retry (which overwrites the old string).
	say add [color bad [Wrap "You must choose \"yes\" or \"no\""]]
	after 1000
	say rewind
	say up

	# Clear header to prevent redisplay, this part is fixed above
	# the actual input line.
	set header {}
    }

    return $response
}

proc ::cmdr::ask::choose {query choices {default {}}} {
    debug.cmdr/ask {}
195
196
197
198
199
200
201



202







203
204
205
206
207
208
209
	    }
	    return {*}${o} $e
	}
	if {($response eq {}) && $hasdefault} {
	    set response $default
	}
	if {$response in $choices} break



	puts stdout [Wrap "You must choose one of $lc"]







    }

    return $response
}

proc ::cmdr::ask::menu {header prompt choices {default {}}} {
    debug.cmdr/ask {}







>
>
>
|
>
>
>
>
>
>
>







208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
	    }
	    return {*}${o} $e
	}
	if {($response eq {}) && $hasdefault} {
	    set response $default
	}
	if {$response in $choices} break

	# Show error for a second, then move back to the interaction
	# line and retry (which overwrites the old string).
	say add [color bad [Wrap "You must choose one of $lc"]]
	after 1000
	say rewind
	say up

	# Clear header to prevent redisplay, this part is fixed above
	# the actual input line.
	set header {}
    }

    return $response
}

proc ::cmdr::ask::menu {header prompt choices {default {}}} {
    debug.cmdr/ask {}
233
234
235
236
237
238
239



240
241
242
243
244
245
246
247
248
249
250
251
252
	incr n
    }
    set Mstr [M format 2string]
    M destroy

    # Format the prompt
    lassign [Fit $prompt 5] pheader prompt




    # Interaction loop
    while {1} {
	if {$header ne {}} {puts stdout $header}
	puts stdout $Mstr

	try {
	    set response \
		[Interact $pheader $prompt \
		     -complete [namespace code [list Complete $fullchoices 0]]]
	} on error {e o} {
	    if {$e eq "aborted"} {
		error Interrupted error SIGTERM







>
>
>



<
<
<







256
257
258
259
260
261
262
263
264
265
266
267
268



269
270
271
272
273
274
275
	incr n
    }
    set Mstr [M format 2string]
    M destroy

    # Format the prompt
    lassign [Fit $prompt 5] pheader prompt

    if {$header ne {}} { say line $header }
    say line $Mstr

    # Interaction loop
    while {1} {



	try {
	    set response \
		[Interact $pheader $prompt \
		     -complete [namespace code [list Complete $fullchoices 0]]]
	} on error {e o} {
	    if {$e eq "aborted"} {
		error Interrupted error SIGTERM
261
262
263
264
265
266
267


268







269
270
271
272
273
274
275

	if {[::string is int $response]} {
	    # Inserting a dummy to handle indexing from 1...
	    set response [lindex [linsert $choices 0 {}] $response]
	    if {$response in $choices} break
	}



	puts stdout [Wrap "You must choose one of the above"]







    }

    return $response
}

# # ## ### ##### ######## ############# #####################








>
>
|
>
>
>
>
>
>
>







284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307

	if {[::string is int $response]} {
	    # Inserting a dummy to handle indexing from 1...
	    set response [lindex [linsert $choices 0 {}] $response]
	    if {$response in $choices} break
	}

	# Show error for a second, then move back to the interaction
	# line and retry (which overwrites the old string).
	say add [color bad [Wrap "You must choose one of the above"]]
	after 1000
	say rewind
	say up

	# Clear header to prevent redisplay, this part is fixed above
	# the actual input line.
	set pheader {}
    }

    return $response
}

# # ## ### ##### ######## ############# #####################

290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
	lappend candidates $c
    }
    return $candidates
}

proc ::cmdr::ask::Interact {header prompt args} {
    debug.cmdr/ask {}
    if {$header ne {}} { puts $header }
    return [linenoise prompt {*}$args -prompt $prompt]
}

proc ::cmdr::ask::Wrap {text {down 0}} {
    debug.cmdr/ask {}
    global env
    if {[info exists env(CMDR_NO_WRAP)]} {







|







322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
	lappend candidates $c
    }
    return $candidates
}

proc ::cmdr::ask::Interact {header prompt args} {
    debug.cmdr/ask {}
    if {$header ne {}} { say line $header }
    return [linenoise prompt {*}$args -prompt $prompt]
}

proc ::cmdr::ask::Wrap {text {down 0}} {
    debug.cmdr/ask {}
    global env
    if {[info exists env(CMDR_NO_WRAP)]} {

Added examples/local.



















>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
#!/usr/bin/env tclsh
# -*- tcl -*-

#lappend auto_path [file dirname [file dirname [file normalize [info script]]]]
#puts "= [join $auto_path "\n= "]"
source  [file dirname [file dirname [file normalize [info script]]]]/say.tcl

source [lindex $argv 0]
exit

Added examples/say-barber-phases.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say barber-phases 16 {**  }] |\n|]|

exit

Added examples/say-barberpole-1.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# infinite barberpole
set B [cmdr say barberpole -width 30]
while {1} {
    after 100
    cmdr say rewind
    $B step
}

exit

Added examples/say-barberpole-2.













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# infinite barberpole with a prefix
set B [cmdr say barberpole -width 30]
cmdr say add "download ... "
cmdr say lock-prefix
while {1} {
    #
    # fake download, unknown size, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 100
    #
    cmdr say rewind
    $B step
}

exit

Added examples/say-counter.

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# progress counter.
set B [cmdr say progress-counter 100]
set i 0
cmdr say add "upload ... "
cmdr say lock-prefix
while {$i < 100} {
    #
    # fake upload, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 100
    #
    cmdr say rewind
    incr i
    $B step $i
}

exit

Added examples/say-general-1.





































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# scanner
set B [cmdr say animate -phases {
    {[***      ]}
    {[** *     ]}
    {[ ** *    ]}
    {[  ** *   ]}
    {[   ** *  ]}
    {[    ** * ]}
    {[     ** *]}
    {[      ***]}
    {[     * **]}
    {[    * ** ]}
    {[   * **  ]}
    {[  * **   ]}
    {[ * **    ]}
    {[* **     ]}
}]
while {1} {
    after 100
    cmdr say rewind
    $B step

    # NOTE: putting the rewind after the step means that we will
    # see the animation output only for a split second and the
    # erased/empty line for the 100 milli delay => the terminal
    # will look empty, with nothing happening.
}

exit

Added examples/say-general-2.

























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# infinite slider - semi-barberpole
set B [cmdr say animate -phases {
    {[         ]}
    {[*        ]}
    {[**       ]}
    {[***      ]}
    {[* **     ]}
    {[ * **    ]}
    {[  * **   ]}
    {[   * **  ]}
    {[    * ** ]}
    {[     * **]}
    {[      * *]}
    {[       * ]}
    {[        *]}
}]
while {1} {
    after 100
    cmdr say rewind
    $B step
}

exit

Added examples/say-larson.







































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# larson scanner
set B [cmdr say animate -phases [cmdr say larson-phases 9 ***]]
while {1} {
    after 100
    cmdr say rewind
    $B step

    # NOTE: putting the rewind after the step means that we will
    # see the animation output only for a split second and the
    # erased/empty line for the 100 milli delay => the terminal
    # will look empty, with nothing happening.
}

exit

Added examples/say-larson-phases.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say larson-phases 23 ***] |\n|]|

exit

Added examples/say-operation-barber.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

cmdr say operation {Upload ... } {
    after 5000 {set ::done ok}
    vwait ::done
} -play {cmdr say barberpole -width -11}

exit

Added examples/say-operation-ping.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

cmdr say operation {Upload } {
    after 5000 {set ::done ok}
    vwait ::done
} -play {cmdr say ping -width 31 -wrap 1}

exit

Added examples/say-operation-pulse.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

cmdr say operation {Upload } {
    after 5000 {set ::done ok}
    vwait ::done
} -every 50 -play {cmdr say pulse -width 7}

exit

Added examples/say-operation-turn.























>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

cmdr say operation {Upload } {
    after 5000 {set ::done ok}
    vwait ::done
} -every 50 -play {cmdr say turn}

exit

Added examples/say-percent.

















































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# percent progress counter
set B [cmdr say percent -max 10000 -digits 2]
set i 0
cmdr say add "upload ... "
cmdr say lock-prefix
while {$i < 10000} {
    #
    # fake upload, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 10
    #
    cmdr say rewind
    incr i
    $B step $i
}

exit

Added examples/say-ping.



































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# ping bar
#set B [cmdr say ping -width 17]
#set B [cmdr say ping -width 17 -wrap 1]
set B [cmdr say ping -width 41 -wrap 1 -head {} -map {	    100000 @
	    10000  \#
	    1000   %
	    100    |
	    10     *
    1      {}
}]
#set B [cmdr say ping -width -8 -wrap 1]
#set B [cmdr say ping -width -8]
#set B [cmdr say ping -width Inf]
cmdr say add "upload "
cmdr say lock-prefix
while {1} {
    #
    # fake upload, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 10
    #
    cmdr say rewind
    $B step
}

exit

Added examples/say-progress-1.



































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# percent progress bar
set B [cmdr say progress -max 1000 -width 50]
set C [cmdr say percent  -max 1000]
set i 0
cmdr say add "upload ... "
cmdr say lock-prefix
while {$i < 1000} {
    #
    # fake upload, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 10
    #
    cmdr say rewind
    incr i
    cmdr say add \[
    $B step $i
    cmdr say add \]

    cmdr say add { }
    $C step $i
}
#after 10
#cmdr say rewind
cmdr say line { OK}

exit

Added examples/say-progress-2.











































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# percent progress bar, full width
set C [cmdr say percent -max 1000]

set  n [string length "upload ... \[\] "]
incr n [$C width]

set B [cmdr say progress -max 1000 -width -$n]
set i 0
cmdr say add "upload ... "
cmdr say lock-prefix
while {$i < 1000} {
    #
    # fake upload, sync ... actual use:
    # - fcopy callback,
    # - http progress-callback
    # - tcllib transfer callback
    after 10
    #
    cmdr say rewind
    incr i
    $C step $i
    cmdr say add { }

    cmdr say add \[
    $B step $i
    cmdr say add \]
}
after 1000
cmdr say rewind
cmdr say line { OK}

exit

Added examples/say-progress-phases.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say progress-phases 6 * ^] |\n|]|

exit

Added examples/say-pulse.































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# infinite pulse - semi-barberpole
set B [cmdr say animate \
	   -phases [cmdr say pulse-phases 3 [cmdr color {bg-blue white} *]]]
while {1} {
    after 100
    cmdr say rewind
    $B step
}

exit

Added examples/say-pulse-phases-1.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say pulse-phases 6 *] |\n|]|

exit

Added examples/say-pulse-phases-2.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say pulse-phases 3 [cmdr color red .]] |\n|]|

exit

Added examples/say-slider.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# infinite slider - semi-barberpole
set B [cmdr say animate -phases [cmdr say slider-phases 9 ***]]
while {1} {
    after 100
    cmdr say rewind
    $B step
}

exit

Added examples/say-slider-phases.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts |[join [cmdr say slider-phases 9 ***] |\n|]|

exit

Added examples/say-turn.











































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

# turn 
set B [cmdr say animate -phases [cmdr say turn-phases]]
while {1} {
    after 100
    cmdr say rewind
    cmdr say add (
    $B step
    cmdr say add )

    # NOTE: putting the rewind after the step means that we will
    # see the animation output only for a split second and the
    # erased/empty line for the 100 milli delay => the terminal
    # will look empty, with nothing happening.
}

exit

Added examples/say-turn-phases.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
#!/usr/bin/env tclsh
# -*- tcl -*-
package require Tcl 8.5
package require cmdr::say

puts ([join [cmdr say turn-phases] )\n(])

exit

Added say.tcl.























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
## -*- tcl -*-
# # ## ### ##### ######## ############# #####################
## CMDR - Convenience commands for terminal manipulation and output

# @@ Meta Begin
# Package cmdr::say 0
# Meta author   {Andreas Kupries}
# Meta location https://core.tcl.tk/akupries/cmdr
# Meta platform tcl
# Meta summary     Terminal manipulation and output.
# Meta description Commands to generate terminal output, to control
# Meta description the terminal, and print text
# Meta subject {command line} tty interaction terminal
# Meta require {Tcl 8.5-}
# Meta require debug
# Meta require debug::caller
# Meta require cmdr::color
# Meta require linenoise
# Meta require TclOO
# @@ Meta End

# # ## ### ##### ######## ############# #####################
##

# Notes on things to work on ...

# - "operation" is possibly mis-named. Its main topic looks to be
#   about animation which is green-threaded around its code-block.
#
# - Animation, or in general, "animated feedback" needs flags and
#   acessors to disable it, for example when stdout is not a
#   tty. Similar to how the color commands become no-ops in that case.
#
#   Because usually we do not wish to clutter a log file with the
#   mixture of strings and terminal control sequences used to implement
#   the animations.
#
# - Strongly consider moving "operation" and the various animation
#   classes into a separate package operating on top of this one,
#   i.e. "cmdr::say". This would have the flag control as well.
#
# - Regarding base animation classes have to implement compositing
#   classes (wrapper for fixed header/trailer strings around an
#   animation, sequence of animations (must have matching 'step'
#   signature) - Interesting case for sequences: How much can we
#   automate (derive) the individual -width specifications from an
#   overall setting and knowledge of fixed sizes ?
#
# - Animation implementation base class => configure|cget API
#
# - Two common use-cases for the animations:
#
#   (a) Timer-driven animation, usually for an operation not
#       generating (usable) feedback.
#
#   (b) Progress-driven animation, usually for an operation generating
#       proper feedback we can use to drive it.

# # ## ### ##### ######## ############# #####################
## Requisites

package require Tcl 8.5
package require cmdr::color
package require debug
package require debug::caller
package require linenoise
package require TclOO
package require try

namespace eval ::cmdr {
    namespace export say
}
namespace eval ::cmdr::say {
    namespace export \
	up down forw back \
	erase-screen erase-up erase-down \
	erase-line erase-back erase-forw \
	goto home line add trouble rewind lock-prefix clear-prefix \
	next next* animate barberpole percent progress-counter progress \
	ping pulse turn larson slider operation header banner \
	\
	auto-width barber-phases progress-phases larson-phases \
	slider-phases pulse-phases turn-phases \
	\
	to

    namespace ensemble create
    namespace import ::cmdr::color

    # State for "add", "lock-prefix", "clear-prefix", and "rewind"
    variable prefix {}
    variable pre    {}

    # Channel to write to. Default is stdout. Mainly to allow
    # redirection during testing.
    variable to
}

# # ## ### ##### ######## ############# #####################

debug define cmdr/say
debug level  cmdr/say
debug prefix cmdr/say {[debug caller] | }

# # ## ### ##### ######## ############# #####################
## output redirection

proc ::cmdr::say::to {{chan {}}} {
    debug.cmdr/say {}
    variable to
    if {[llength [info level 0]] == 1} {
	return $chan
    }
    set to $chan
    return
}

# # ## ### ##### ######## ############# #####################
## cursor movement

proc ::cmdr::say::up {{n 1}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[${n}A
    flush           stdout
    return
}

proc ::cmdr::say::down {{n 1}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[${n}B
    flush           stdout
    return
}

proc ::cmdr::say::forw {{n 1}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[${n}C
    flush           stdout
    return
}

proc ::cmdr::say::back {{n 1}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[${n}D
    flush           stdout
    return
}

# # ## ### ##### ######## ############# #####################
## (partial) screen erasure

proc ::cmdr::say::erase-screen {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[2J$suffix
    flush           stdout
    return
}

proc ::cmdr::say::erase-up {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[1J$suffix
    flush           stdout
    return
}

proc ::cmdr::say::erase-down {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[0J$suffix
    flush           stdout
    return
}

# # ## ### ##### ######## ############# #####################
## (partial) line erasure

proc ::cmdr::say::erase-line {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[2K$suffix
    flush           stdout
    return
}

proc ::cmdr::say::erase-back {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[1K$suffix
    flush           stdout
    return
}

proc ::cmdr::say::erase-forw {{suffix {}}} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[0K$suffix
    flush           stdout
    return
}

# # ## ### ##### ######## ############# #####################
## absolute cursor movement

proc ::cmdr::say::goto {x y} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[${y};${x}f
    flush           stdout
    return
}

proc ::cmdr::say::home {} {
    debug.cmdr/say {}
    puts -nonewline stdout \033\[H
    flush           stdout
    return
}

# # ## ### ##### ######## ############# #####################
## bottom level line output and animation control

proc ::cmdr::say::add {text} {
    debug.cmdr/say {}
    variable pre
    append   pre $text
    puts -nonewline stdout $text
    flush           stdout
    return
}

proc ::cmdr::say::line {text} {
    debug.cmdr/say {}
    variable pre    {}
    variable prefix {}
    puts  stdout $text
    flush stdout
    return
}

proc ::cmdr::say::trouble {text} {
    debug.cmdr/say {}
    puts  stderr $text
    flush stderr
    return
}

proc ::cmdr::say::rewind {} {
    debug.cmdr/say {}
    variable prefix
    erase-line \r$prefix
    return
}

proc ::cmdr::say::lock-prefix {} {
    debug.cmdr/say {}
    variable pre
    variable prefix $pre
    return
}

proc ::cmdr::say::clear-prefix {} {
    debug.cmdr/say {}
    variable prefix {}
    return
}

proc ::cmdr::say::next {} {
    debug.cmdr/say {}
    clear-prefix
    puts  stdout ""
    flush stdout
    return
}

proc ::cmdr::say::next* {} {
    debug.cmdr/say {}
    puts  stdout ""
    flush stdout
    return
}

# # ## ### ##### ######## ############# #####################

proc ::cmdr::say::header {text {filler -}} {
    debug.cmdr/say {}
    line \n${text}\n[string repeat $filler [string length $text]]
    return
}

proc ::cmdr::say::banner {text} {
    debug.cmdr/say {}
    line \n$text
    return
}

proc ::cmdr::say::operation {lead script args} {
    debug.cmdr/say {}

    set cmd     {}  ;# -play
    set delay   100 ;# -every
    set trailer OK  ;# -trailer, -no-trailer

    while {1} {
	set o [lindex $args 0]
	switch -glob -- $o {
	    -every { ;# int number argument
		set delay [lindex $args 1]
		set args  [lrange $args 2 end]
	    }
	    -play { ;# cmd prefix argument
		set cmd  [lindex $args 1]
		set args [lrange $args 2 end]
	    }
	    -no-trailer { ;# no argument
		set trailer {}
		set args    [lrange $args 1 end]
	    }
	    -trailer { ;# string argument
		set trailer [lindex $args 1]
		set args    [lrange $args 2 end]
	    }
	    -* {
		return -code error \
		    -errorcode {CMD SAY BAD-OPTION} \
		    "Unknown option $o, expected -every, -play, or -(no-)trailer"
	    }
	    default break
	}
    }

    add $lead
    lock-prefix

    if {[llength $cmd]} {
	set animation [uplevel 1 $cmd]
	$animation run $delay
    }

    try {
	uplevel 1 $script
    } {*}$args on error {e o} {
	# General error => close line, and pass.
	line ""
	return {*}$o $e
    } on ok {e o} {
	rewind ; # remove animation output, leave only the lead
	if {$trailer ne {}} {
	    line [color good $trailer]
	}
    } finally {
	# Stop ongoing animation, if any
	if {[llength $cmd]} {
	    $animation done
	}
    }
    return $e
}

# # ## ### ##### ######## ############# #####################
## Animation helper class (timer driven auto-step).

oo::class create ::cmdr::say::Auto {
    variable mytimer

    method run {delay} {
	debug.cmdr/say {}
	my stop ;# Kill a running timer, this allows us to call sequences of runs without worrying about the system state.

	cmdr say rewind
	my step

	set mytimer [after $delay [info level 0]]
	return
    }

    method stop {} {
	debug.cmdr/say {}
	catch {
	    after cancel $mytimer
	}
	set mytimer {}
	return
    }

    destructor {
	debug.cmdr/say {}
	my stop
    }
}

# # ## ### ##### ######## ############# #####################
## general animation command and class

proc ::cmdr::say::animate {args} {
    debug.cmdr/say {}
    array set config $args
    Require config -phases
    return [animate::Im new -phases [Fill $config(-phases)]]
}

oo::class create ::cmdr::say::animate::Im {
    superclass ::cmdr::say::Auto
    variable myphases myphase mywidth

    constructor {args} {
	debug.cmdr/say {}
	my configure {*}$args
	return
    }

    method configure {args} {
	array set config $args
	::cmdr::say::Require config -phases

	set myphases $config(-phases)
	set myphase  0
	set mywidth  [string length [lindex $myphases 0]]
	return
    }

    method width {} {
	debug.cmdr/say {}
	return $mywidth
    }

    method step {} {
	debug.cmdr/say {}
	cmdr say add [lindex $myphases $myphase]
	incr myphase
	if {$myphase == [llength $myphases]} { set myphase 0 }
	return
    }

    method goto {phase} {
	debug.cmdr/say {}
	set myphase $phase
	cmdr say add [lindex $myphases $myphase]
	return
    }

    method done {} {
	debug.cmdr/say {}
	my destroy
    }
}

# # ## ### ##### ######## ############# #####################
## barberpole animation

proc ::cmdr::say::barberpole {args} {
    debug.cmdr/say {}

    array set config {
	-width   {}
	-pattern {**  }
    }
    array set config $args

    set phases [barber-phases \
		    [auto-width $config(-width)] \
		    $config(-pattern)]

    # Run the animation via the general class.
    return [animate::Im new -phases $phases]
}

# # ## ### ##### ######## ############# #####################
## pulse animation

proc ::cmdr::say::pulse {args} {
    debug.cmdr/say {}

    array set config {
	-width   {}
	-stem    .
    }
    array set config $args

    set phases [pulse-phases \
		    [auto-width $config(-width)] \
		    $config(-stem)]

    # Run the animation via the general class.
    return [animate::Im new -phases $phases]
}

# # ## ### ##### ######## ############# #####################
## turn animation

proc ::cmdr::say::turn {} {
    debug.cmdr/say {}
    set phases [turn-phases]
    # Run the animation via the general class.
    return [animate::Im new -phases $phases]
}

# # ## ### ##### ######## ############# #####################
## slider animation

proc ::cmdr::say::slider {args} {
    debug.cmdr/say {}

    array set config {
	-width   {}
	-pattern ***
    }
    array set config $args

    set phases [slider-phases \
		    [auto-width $config(-width)] \
		    $config(-pattern)]

    # Run the animation via the general class.
    return [animate::Im new -phases $phases]
}

# # ## ### ##### ######## ############# #####################
## larson scanner animation

proc ::cmdr::say::larson {args} {
    debug.cmdr/say {}

    array set config {
	-width   {}
	-pattern ***
    }
    array set config $args

    set phases [larson-phases \
		    [auto-width $config(-width)] \
		    $config(-pattern)]

    # Run the animation via the general class.
    return [animate::Im new -phases $phases]
}

# # ## ### ##### ######## ############# #####################
## progress counter, n of max.

proc ::cmdr::say::progress-counter {max} {
    debug.cmdr/say {}
    return [progress-counter::Im new $max]
}

oo::class create ::cmdr::say::progress-counter::Im {
    superclass ::cmdr::say::Auto
    variable mywidth myformat mymax

    constructor {max} {
	debug.cmdr/say {}
	set n [string length $max]
	set mymax    $max
	set mywidth  [expr {1+2*$n}]
	set myformat %${n}d/%d
	return
    }

    method width {} {
	debug.cmdr/say {}
	return $mywidth
    }

    method step {at} {
	debug.cmdr/say {}
	cmdr say add [format $myformat $at $mymax]
	return
    }

    method done {} {
	debug.cmdr/say {}
	my destroy
    }
}

# # ## ### ##### ######## ############# #####################
## progress counter, in percent.

proc ::cmdr::say::percent {args} {
    debug.cmdr/say {}

    array set config {
	-max    100
	-digits 2
    }
    array set config $args

    return [percent::Im new $config(-max) $config(-digits)]
}

oo::class create ::cmdr::say::percent::Im {
    superclass ::cmdr::say::Auto
    variable mywidth myformat mymax

    constructor {max digits} {
	debug.cmdr/say {}
	set mymax $max

	set mywidth 3
	if {$digits} {
	    incr mywidth ;# .
	    incr mywidth $digits
	}
	set myformat %${mywidth}.${digits}f
	return
    }

    method width {} {
	debug.cmdr/say {}
	return $mywidth
    }

    method step {at} {
	debug.cmdr/say {}
	set percent [expr {100*double($at)/double($mymax)}]
	cmdr say add [format $myformat $percent]%
	return
    }

    method done {} {
	debug.cmdr/say {}
	my destroy
    }
}

# # ## ### ##### ######## ############# #####################
## progress-bar

proc ::cmdr::say::progress {args} {
    debug.cmdr/say {}

    array set config {
	-max    100
	-width  {}
	-stem   =
	-head   >
    }
    array set config $args

    set phases [progress-phases \
		    [auto-width $config(-width)] \
		    $config(-stem) \
		    $config(-head)]

    return [progress::Im new $config(-max) $phases]
}

oo::class create ::cmdr::say::progress::Im {
    variable mymax myphases

    constructor {max phases} {
	debug.cmdr/say {}
	# Inner object, general animation, container for our phases.
	cmdr::say::animate::Im create A -phases $phases
	set mymax    $max
	set myphases [llength $phases]
	return
    }

    forward width \
	A width

    method step {at} {
	debug.cmdr/say {}
	set phase [expr {int($myphases * (double($at)/double($mymax)))}]
	if {$phase >= $myphases} { set phase end }
	A goto $phase
	return
    }

    method done {} {
	debug.cmdr/say {}
	my destroy
    }
}


# # ## ### ##### ######## ############# #####################
## ping

proc ::cmdr::say::ping {args} {
    debug.cmdr/say {}

    # Options
    # -head  string - Show in front of the ping characters.
    # -map   dict   - Map moduli to characters, when reached.
    #                 Order relevant, largest moduli first.
    #                 End with modulus 1, always matching!
    #                 (System will internally add such to close the map).
    # -width int    - Amount of space to use for the moving ping+head
    # -wrap  bool   - Wrapping keeps the moving part within the current line.
    #                 Without wrap a wrap goes to the next line using next*.

    array set config {
	-wrap 0
	-head &
	-map {
	    100000 @
	    10000  \#
	    1000   %
	    100    |
	    10     *
	    1      .
	}
	-width {}
    }
    array set config $args

    return [ping::Im new \
		$config(-wrap) \
		$config(-head) \
		$config(-map) \
		[auto-width $config(-width)]]
}

oo::class create ::cmdr::say::ping::Im {
    superclass ::cmdr::say::Auto
    variable mywrap myhead mymap mywidth myphase mybuffer myewidth mytrail

    constructor {wrap head map width} {
	debug.cmdr/say {}

	# close map, if user forgot. No-op if user closed it.
	lappend map 1 _ ;# default char configurable ?

	set mywrap   $wrap
	set myhead   $head
	set mymap    $map
	set mywidth  $width
	set myewidth [expr {$mywidth - [string length $head]}] ;# effective width.
	set myphase  0
	set mybuffer ""
	if {$mywrap} {
	    set mytrail [string repeat { } $myewidth]
	} else {
	    set mytrail {}
	}
	return
    }

    method width {} { return $mywidth }

    method step {} {
	debug.cmdr/say {}

	set ch [my Next]

	# Create a buffer to print, from the previous state.
	# First attempt. Without a head leading the pings.

	append  mybuffer $ch
	set str $mybuffer

	if {$mywrap} {
	    set mytrail [string range $mytrail 1 end]
	}

	if {[string length $str] < $myewidth} {
	    # Still fits. Simply print.
	    cmdr say add $str$myhead$mytrail
	} else {
	    # Too long. Split into a fitting piece and overshot.
	    set fit  [string range $str 0          ${myewidth}-1]
	    set over [string range $str ${myewidth} end]

	    # Print the fitting piece first to complete the line,
	    # then move to the next line and print the overshot.
	    cmdr say add $fit

	    if {!$mywrap} {
		cmdr say next*      ;# no-wrap: next line, keep current prefix.
	    } else {
		cmdr say rewind     ;# wrap:    rewind/clear line and continue
	    }
	    cmdr say add $over$myhead$mytrail

	    # Set state for next call.
	    set mybuffer $over
	    if {$mywrap} {
		set mytrail $fit
	    }
	}
	return
    }

    method done {} {
	debug.cmdr/say {}
	my destroy
    }

    method Next {} {
	incr myphase
	foreach {mod char} $mymap {
	    if {($myphase % $mod) != 0} continue
	    return $char
	}
	return -code error -errorcode ... ""
    }
}

# # ## ### ##### ######## ############# #####################

proc ::cmdr::say::auto-width {width} {
    debug.cmdr/say {}

    if {$width eq {}} {
	# Nothing defined, adapt to terminal width
	set width [linenoise columns]
    } elseif {$width < 0} {
	# Adapt to terminal width, with some space reserved.
	set width [expr {[linenoise columns] + $width}]
    }
    return $width
}

# # ## ### ##### ######## ############# #####################

proc ::cmdr::say::barber-phases {width pattern} {
    debug.cmdr/say {}

    # Repeat the base pattern as necessary to fill the requested
    # width. We use a count which gives us something which might be a
    # bit over, so after replication the resulting string is cut down
    # to the exact size.
    set n    [expr {int(0.5+ceil($width / double([string length $pattern])))}]
    set base [string range [string repeat $pattern $n] 0 ${width}-1]

    # Example for standard pattern '**  ', widths 16, and 17.
    # |**  **  **  **  |
    # | **  **  **  ** |
    # |  **  **  **  **|
    # |*  **  **  **  *|

    # |**  **  **  **  *| Note how the odd length yields one larger
    # |***  **  **  **  | band (***) in the pattern, which ripples
    # | ***  **  **  ** | across, and multiplies the number of phases
    # |  ***  **  **  **| before reaching the origin again.
    # |*  ***  **  **  *|
    # |**  ***  **  **  | Similar effects will be had for for any width
    # | **  ***  **  ** | which W != 0 mod (length pattern).
    # |  **  ***  **  **|
    # |*  **  ***  **  *|
    # |**  **  ***  **  |
    # | **  **  ***  ** |
    # |  **  **  ***  **|
    # |*  **  **  ***  *|
    # |**  **  **  ***  |
    # | **  **  **  *** |
    # |  **  **  **  ***|
    # |*  **  **  **  **|

    lappend phases $base
    # rotate the base through all configurations until we reach it again.
    set next $base
    while {1} {
	set next [string range $next 1 end][string index $next 0]
	if {$next eq $base} break
	lappend phases $next
    }

    return $phases
}

proc ::cmdr::say::progress-phases {width stem head} {
    debug.cmdr/say {}

    # compute phases.
    set h [string length $head]
    set phases {}
    for {set i 0} {$i < ($width - $h)} {incr i} {
	lappend phases [string repeat $stem $i]$head
	# >, =>, ==>, ...
    }

    return [Fill $phases]
}

proc ::cmdr::say::larson-phases {width pattern} {
    debug.cmdr/say {}

    set n [string length $pattern]
    if {$n > $width} {
	return [list $pattern]
    }

    set spaces [expr {$width - $n}]

    # round 1: slide right
    for {
	set pre  0
	set post $spaces
    } { $post > 0 } {
	incr pre
	incr post -1
    } {
	lappend phases [string repeat { } $pre]${pattern}[string repeat { } $post]
    }

    # round 2: slide left
    for {
	set pre  $spaces
	set post 0
    } { $pre > 0 } {
	incr pre  -1
	incr post
    } {
	lappend phases [string repeat { } $pre]${pattern}[string repeat { } $post]
    }

    return $phases
}

proc ::cmdr::say::slider-phases {width pattern} {
    debug.cmdr/say {}

    set n [string length $pattern]
    if {$n > $width} {
	return [list $pattern]
    }

    # round 1: slide pattern in.
    lappend phases {}
    for {set k 0} {$k < $n} {incr k} {
	lappend phases [string range $pattern end-$k end]
    }

    # round 2: slide pattern right.
    set spaces [expr {$width - $n}]
    for {
	set pre  0
	set post $spaces
    } { $post > 0 } {
	incr pre
	incr post -1
    } {
	lappend phases [string repeat { } $pre]${pattern}[string repeat { } $post]
    }

    # round 3: slide pattern out.
    for {set k 0} {$k < $n} {incr k} {
	lappend phases [string repeat { } $pre][string range $pattern 0 end-$k]
	incr pre
    }

    return $phases
}

proc ::cmdr::say::pulse-phases {width stem} {
    debug.cmdr/say {}

    # compute phases.
    set phases {}
    for {set i 0} {$i <= $width} {incr i} {
	lappend phases [string repeat $stem $i]
    }
    return [Fill $phases]
}

proc ::cmdr::say::turn-phases {} {
    return [list | / - \\]
}

# # ## ### ##### ######## ############# #####################

proc ::cmdr::say::Require {cv option} {
    debug.cmdr/say {}
    upvar 1 $cv config
    if {[info exists config($option)]} return
    return -code error \
	-errorCode [list CMDR SAY OPTION MISSING $option] \
	"Missing required option \"$option\""
}

proc ::cmdr::say::Fill {phases} {
    # compute max length of phase strings
    set max [string length [lindex $phases 0]]
    foreach p [lrange $phases 1 end] {
	# Look for ANSI color control sequences and remove them. Avoid
	# counting their characters as such sequences as a whole
	# represent a state change, and are logically of zero/no
	# width.
	regsub -all "\033\\\[\[0-9;\]*m" $p {} p
	set n [string length $p]
	if {$n < $max} continue
	set max $n
    }

    # then pad all other strings which do not reach that length with
    # spaces at the end.
    set tmp {}
    foreach p $phases {
	# Look for ANSI color control sequences and discount
	# them. Avoid counting their characters as such sequences as a
	# whole represent a state change, and are logically of zero/no
	# width.
	regsub -all "\033\\\[\[0-9;\]*m" $p {} px
	set n [string length $px]
	if {$n < $max} {
	    append p [string repeat { } [expr {$max - $n}]]
	}
	lappend tmp $p
    }

    return $tmp
}

# # ## ### ##### ######## ############# #####################
## Ready
package provide cmdr::say 1

Added tests/say.test.





















































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# -*- tcl -*- tcl.tk//DSL tcltest//EN//2.0
# # ## ### ##### ######## ############# #####################
## Testing the cmdr::say package.
#
# We are explicitly specifying the -width in all commands taking this
# option and do not use widths < 0. This is done to avoid all code
# paths actually requiring linenoise and interaction with the
# terminal.
#

kt check Tcl     8.5
kt check tcltest 2

kt require support TclOO
kt require support linenoise
kt require support debug
kt require support debug::caller

kt local   support cmdr::tty
kt local   support cmdr::color
kt local   testing cmdr::say

# # ## ### ##### ######## ############# #####################

proc setup {} {
    cmdr say to [open TEST w]
    return
}

proc see {} {
    close [cmdr say to]
    cmdr say to {}
    set chan [open TEST rb]
    set d    [read $chan]
    close $chan
    return $d
}

proc cleanup {} {
    cmdr say to {}
    return
}

# # ## ### ##### ######## ############# #####################

test cmdr-say-to-1.0 {to, wrong num args, too many} -body {
    cmdr say to C X
} -returnCodes error \
    -result {}

# # ## ### ##### ######## ############# #####################
## cursor movement

foreach {n move response1 response2} {
    0 up   \033\[1A \033\[2A
    1 down \033\[1B \033\[2B
    2 forw \033\[1C \033\[2C
    3 back \033\[1D \033\[2D
} {
    test cmdr-say-${move}-1.0 "$move, wrong num args, too many" -body {
	cmdr say $move N X
    } -returnCodes error \
	-result {}

    test cmdr-say-${move}-1.1 "$move, default" -body {
	cmdr say $move
	see
    } -setup setup -cleanup cleanup -result $response1

    test cmdr-say-${move}-1.2 "$move, explicit" -body {
	cmdr say $move 2
	see
    } -setup setup -cleanup cleanup -result $response2
}

# # ## ### ##### ######## ############# #####################
## erase screen

# # ## ### ##### ######## ############# #####################
## erase line

# # ## ### ##### ######## ############# #####################
## absolute moves

# # ## ### ##### ######## ############# #####################
## animations

# # ## ### ##### ######## ############# #####################
cleanupTests
return