POSIX 终端屏保
最近在读一本书《PHP Beyond the Web》,内容难度不大,不过感觉挺有意思的。可能是做惯了web应用,或者说潜意识觉得PHP就是做web应用的,没想到在其它领域上,PHP也用得挺顺手,也许这也是一种偏见吧。看了这本书,对PHP有一些许改观,也算是扩展了视野。
在这里面看到一个挺好玩的小脚本,不过直接运行起来好像是有点小问题,于是在它的基础上改动了一下。
首先定义三个常量,\033在以前修改颜色时也用过,不过一直都是死记,其实它就是ESC的ASCII码。POSIX终端的转义指令可以实现颜色、光标移动等效果。其中ESC是转义字符,用于表示转义指令的开始,后面紧跟着指令代码。以下代码中的CLEAR常量是清空屏幕的指令,HOME常量用于移动光标的位置(屏幕左上角,第一个1表示行,第二个1表示列),RESET常量用于重置所有属性,包括关闭所有颜色和格式;另外[30m到[37m表示字体颜色,[40m到[47m表示背景颜色。此外,还有其它比较不常见的属性。
<?php
const ESC = "\033";
const CLEAR = ESC . "[2J";
const HOME = ESC. "[0;0f";
const RESET = ESC. "[0m";
接着,提示用户输入Enter键,程序读取输入。
// 注意:不用 echo 而用 fwrite,是因为 fwrite(STDOUT...) 写入到 php://stdout 流,而 echo(和 print)写入到 php://output 流。
// 虽然通常情况下它们都是一样的,但也不一定。
// 另外,php://output 是受输出控制以及缓冲的功能,这个功能可能是或者不是我们所希望的
fwrite(STDOUT, "Press Enter To Begin, And Enter Again To End");
// 等待用户按 Enter。STDIN 默认是阻塞流,这意味着我们尝试读取时,脚本会停止,并等待输入。
// 当用户按下 Enter 时,键盘输入到 shell 的内容会被传到我们的脚本(通过 fread)。
fread(STDIN, 1);
我们希望程序运行到用户再次输入Enter。这意味着我们想要通过fread不断检测输入,但如果没有输入时又不暂停/阻塞程序。因此,我们将STDIN设置为非阻塞。
stream_set_blocking(STDIN, 0);
在准备输出时,清空终端并在它的周围画一个方框。在PHP中没有内置的方法能获取终端大小,所以我们要调用外部的shell命令tput来获取终端的相关信息。
$rows = intval(`tput lines`);
$cols = intval(`tput cols`);
fwrite(STDOUT, CLEAR. HOME);
for ($rowcount = 1; $rowcount < $rows; $rowcount++) {
fwrite(STDOUT, sprintf("%s[%s;%sf•", ESC, $rowcount, 1));
fwrite(STDOUT, sprintf("%s[%s;%sf•", ESC, $rowcount, $cols));
}
for ($colcount = 1; $colcount <= $cols; $colcount++) {
fwrite(STDOUT, sprintf("%s[%s;%sf•", ESC, 1, $colcount));
fwrite(STDOUT, sprintf("%s[%s;%sf•", ESC, $rows-1, $colcount));
// 这里有个疑惑,如果不 usleep,输出就会不全,应该是输出缓冲的问题,但是目前试过很多方法都行不通。
// 在 windows 上的 docker 没有问题。不确定是不是仅在 macos iterm2 上有问题,有待确认
usleep(1000);
}
接着,就可以在上面画出的方框内进行屏保绘制了。
while (true) {
if (fread(STDIN,1)) { break; }
$row = rand(2, $rows-2);
$col = rand(2, $cols-2);
$fg_color = rand(30,37);
$bg_color = rand(40,47);
fwrite(STDOUT, sprintf("%s[%sm", ESC, $fg_color));
fwrite(STDOUT, sprintf("%s[%sm", ESC, $bg_color));
fwrite(STDOUT, sprintf("%s[%s;%sf•", ESC, $row, $col));
usleep(1000);
}
如果在绘制屏保过程中,用户再次按了Enter键,则重置所有终端属性,并清空屏幕。
fwrite(STDOUT, RESET);
fwrite(STDOUT, CLEAR. HOME);