从Common Lisp控制终端(第2部分)

2020-06-20 02:18:23

这是关于从零开始为终端构建McCLIM后端的教程的第二部分。读完第一期后,我们应该很好地掌握如何控制和读取来自终端的输入。为了提高效率和易用性,现在是时候改进一下了。如果你没有读完最后一部分,这里是包含源代码的归档文件,它将作为这篇文章的起点。

目前,我们的I/O与终端同步。当我们调用outor ctl时,字符立即被发送到它,并且我们使用read-input读取输入,直到流为空。上一篇文章中介绍的模型当然很简单,但是简单的模型很难有效地使用。我们将把重点放在“简单”上。在这篇文章中,我将把重点放在输出上。

计算机科学中的所有问题都可以通过另一个层次的方向来解决。--大卫·惠勒。

我们将为编写控制台应用程序构建一个方便的抽象。然而,如果我们放弃直接操纵航站楼的手段,那将是一种耻辱。该库将提供不同的API,因此可以迎合程序员的需要。原则上,同时使用两个不同的抽象是不可行的,因为较高的抽象建立在较低的抽象之上,事情可能会出错。

现在,我们将定义两个包:eu.turtleware.charming-clim/l0和eu.turtleware.charming-Climate.Terminal/L1,它们具有不同的抽象级别来访问终端。它们只是作为导出符号的手段,所有的实现都在一个包中完成,这种做法极大地提高了使用Common Lisp包的人的生活质量。现在创建文件Packages.lisp。

(defpackage#:eu.turtleware.charming-clim/l0(:export#:init-Terminal#:关闭终端#:*Terminal*#:put#:esc#:csi#:sgr#:读取输入#:keyp#:复位终端#:清除终端#:清除行#:设置前景颜色#:设置背景颜色#:带有光标位置#:设置光标位置#:保存光标。-Position#:Restore-Cursor-Position#:Request-Cursor-Position#:Cursor-Up#:Cursor-Down#:Cursor-Right#:Cursor-Left#:Set-Cursor-Visibility#:Set-Mice-Tracking)(defpackage#:eu.turtleware.charming-clim/L1(:export#:with-console#:out#:ctl))(defpackage#:eu.turtleware.charming-clim(:use#:common。-LISP#:eu.turtleware.charming-clim/l0#:eu.turtleware.charming-clim/L1)。

我们将利用这个机会使函数命名更加一致,并介绍光标操作实用程序。重命名函数。

并添加用于操作光标的转义序列。不要忘记在代码的其他部分(在宏ctl和函数initialize-instance、(SETF PTR)和(SETF CVP)中)更改对重命名函数的引用。

(Maclet((moveit(Endch)`(if(=n 1)(csi,endch)(csi,endch)(csi n,endch)(deful sor-up(&;可选(N 1))(moveit";A";))(deun sor-down(&;可选(N 1))(moveit";B";))(deful sor-right(&;可选(N 1))(moveit";C";))(def Cursor-Left(&;Optional(N 1))(moveit";D";))(deun设置光标可见性(Visiblep)(if visiblep(csi";?";2 5";h";)(csi";?";2 5";l";)(csi";?";2 5";l";;;(CSI?跟踪;编码h/l);跟踪:1000-正常,1002-按钮,1003-所有动作;1004-焦点输入/输出;编码:1006-SGR编码方案(deFun设置-鼠标跟踪(Enabledp)(如果启用)(如果启用)p(csi";?";1003";;";1006";h";)(csi";?";1003";)(CSI";?";?";1003";)(CSI";?";?";1003";)(deFun set-alt-is-meta(Bool)(if bool(setf+alt-mod++meta-mod+)(setf+alt-mod++alt-mod*+)。

从现在开始,当我们谈论低级抽象时,我们将把目标对象称为终端,而当我们谈论高级抽象时,我们将把它的目标对象称为控制台。重命名以下符号。

并替换源代码中的所有引用以使用新符号。将变量*TERMINAL*和函数INIT-TERMINAL和CLOSE-TERMINAL移到顶部(外来函数定义下方)。

我们将略微重构集合-*-颜色函数。函数将使用表示颜色RGBA值的数字,而不是单独接受每种颜色。例如#ff000000表示红色。现在将忽略字母表,但拥有此组件将省去数据表示格式的另一次更改。

(DeFun Set-前景色(COLOR)(让((r(LDB';(8.。24)颜色))(g(LDB';(8.。16)颜色))(b(LDB';(8.。8)颜色)(a(LDB';(8.。0)color)(DECLARE(忽略a))(sgr";38;2;";r";;";g";;";b)(deFun set-back-color(Color)(let((r(LDB';(8.。24)颜色))(g(LDB';(8.。16)颜色))(b(LDB';(8.。8)颜色)(a(LDB';(8.。0)color)(声明(忽略a))(sgr";48;2;";r";;";g";;";b))。

(def宏ctl(&;REST操作)`(#|.|#(:fgc`(setf(fgc*console*),@args))(:bgc`(setf(bgc*console*),@args)(defclass console()#|.|#(:default-initargs:fgc#xffa0a000:bgc#x222200))(defmethod initialize-instance:After(实例控制台)&。key FGC BGC pos CVP PTR)#|.|#(设置前景色FGC)(设置背景色BGC)(defmethod(SETF FGC):After(RGBA(实例控制台))(设置前景色RGBA))(defmethod(SETF BGC):After(RGBA(实例控制台))(设置背景色RGBA))(deFun show-Screen()#|.|#(out。:bgc#x00000000:fgc#xbb000000))(out(:bgc#x00000000:fgc(亚历山大:Random-ELT';(#x00444400#x00444400#x00664400)。

现在,我们将按以下顺序将与控制台相关的部件移动到单独的文件console.lisp:

最后,示例代码将放在文件example.lisp中。移动功能在那里显示屏幕和开始显示。

(defsystem";eu.turtleware.charming-clim";:defsystem-Dependent-on(#:CFFI):Depends-on(#:Alexandria#:CFFI#:Swank):Components((:cfile";raw-mode";)(:file";Packages";)(:file";Terminal";:Depends-on(";Packages";)(:文件";控制台";:依赖于(";包";";终端";))(:文件";示例";:依赖于(";包";";控制台";))。

Console对象有很多职责,因此重构它以继承仅实现与输出相关的部分的类是有意义的。当我们决定添加另一层间接层时,这也会很有用。在实现缓冲区时,我还会将一个不合适的位置表示固定为cons,并指定剪辑区域。创建文件output.lisp并将其添加到ASD文件。

(defsystem";eu.turtleware.charming-clim";:defsystem-Dependent-on(#:CFFI):Depends-on(#:Alexandria#:CFFI#:Swank):Components((:cfile";raw-mode";)(:file";Packages";)(:file";Terminal";:Depends-on(";Packages";)(:文件";输出";:依赖于(";包";))(:文件";包";控制台";依赖于(";包";";输出";";终端";))(:文件";示例";:依赖(";包";";控制台";()。

宏out和ctl将在当前虚拟缓冲区上操作。为了做到这一点,我们将定义一个必须由所有虚拟缓冲区实现的协议。With-Clip现在变成了一种方便的宏扩展为通用函数Invoke-with-Clip。引入一个带-Buffer的宏来绑定当前缓冲区,该缓冲区绑定到变量*Buffer*。

(defGeneric PUT-CELL(缓冲区行列fg bg))(defGeneric FGC(Buffer))(defGeneric(SETF FGC)(FGC Buffer)(:参数优先顺序缓冲区FGC))(defGeneric BGC(Buffer))(defGeneric(SETF BGC)(BGC Buffer)(:参数优先顺序缓冲区BGC))(defGeneric行(缓冲区))(defGeneric(SETF行)(行缓冲区)(:参数-。优先顺序缓冲行))(defGeneric列(缓冲区))(defGeneric(集合列)(列缓冲区)(:自变量优先顺序缓冲区列))(defGeneric行(缓冲区))(defGeneric Inside-p(缓冲区行))(defGeneric Inside-p(缓冲区行))(defGeneric Inside-With-Clip(缓冲区延续&;REST选项&;key r1 c1 R2 c2 fn)(带-CLIPPING的def宏((缓冲区&;REST选项)&;Body Body)(let((fn(Gensym)`(Flet((,fn(),@body)(DECLARE(Dynamic-Extension(function,fn)(Invoke-with-clipping,buffer(function,fn),@opts)(defvar*buffer*)(def宏with-buffer((Object)&;Body Body)`(let((*buffer*,object)),@body)。

下面用这些术语实现CTL和OUT宏。我们将暂时从ctl宏中省略:cvp和:ptr选项。LETF和Clear-Rectangle保持不变。从console.lisp文件中删除旧宏。

(def宏letf(binings&;Body)(LOOP FOR(Place Value)in Bindings for old-val=(Gensym)Collect`(,old-val,place)into save Collect`(setf,place,value)into store Collect`(setf,place,old-val)into Restore Final)(return`(let(,@save)(Unwind-Protect(pron,@store,@body),@restore)(def宏out((&;键行列FGC BGC)对象)`(let((buf*buffer*)(str(princ-to-string,object)(assert(NULL(find#\newline str)(letf(Row Buf)(or,row(Row Buf)((Colbuf)(or,col(Colbuf)((FGC Buf)(or,FGC(FGC Buf)((BBGC。bgc(Bgc Buf)(LOOP with row=(Row Buf)for colfrom(Colbuf)for ch over str with bgc=(Bgc Buf)with fgc=(Fgc Buf)do(put-cell buf row colch fgc bgc)(def宏ctl(&;REST操作)`(let((buf*buffer*)),@(COLLECT操作中的OP循环(析构-绑定(name&;REST ARGS)OP(ECASE NAME(:clr`(clear-rectangle,@args))(:fgc`(setf(Fgc Buf),@args))(:bgc`(setf(Bgc Buf),@args))(:row`(setf(Row Buf),@args))(:col`(setf(Colbuf),@args)(defun清除-矩形(R1c1r2c2)(带str=(make-string(1+(-c2c1)):首字母元素#\space)表示r从r1到r2do(OUT(:ROW r:COL c1)str))。

如果没有实现,协议会是什么呢?裁剪将通过类Clip实现。此选择是透明的,因为所有函数都是在缓冲区上专用的。每个缓冲区都有其自己的裁剪区域。虚拟缓冲区不知道如何在屏幕上绘制,所以put-cell会打印警告。

(defclass bbox()((r1:initarg:r1:存取器r1)(c1:initarg:c1:存取器c1)(r2:initarg:r2:存取器R2))(c2:initarg:c2:存取器c2))(declass Clip(Bbox)((fn:initarg:fn:存取器fn))(:default-initargs:r1 1:c1:r2 24:C2 80:fn。(常量t)(defclass buffer()((fgc:initarg:fgc:存取器fgc:Documentation";前面的颜色";)(bgc:initarg:bgc:存取器bgc:Documentation";背景颜色";)(row:initarg:row:存取器行:Documentation";当前行";)(ol:initarg:COL:存取器列:Documentation";当前列";)(Clip:initarg:Clip:Accessessor Clip:Documentation";裁剪对象";)(Clip:initarg:Clip:Accessor Clip:Documentation";Clip Object";))(ROWS:initarg:ROWS:存取器行:Documentation";缓冲区行数";)(COLS:initarg:COLS:存取器COLS:Documentation";缓冲区";))(:default-initargs:Clip(make-instance';Clip))(Defmethod put-cell((Buffer)row colch FG BG)(warn";put-cell:default method不做任何事情!";PUT-CELL:DEFAULT方法不做任何事情!";PUT-CELL:DEFAULT方法不做任何事情!";)(defmethod inside-p((缓存)行缓存)(let((剪辑(剪辑缓存)(和(<;=(r1剪辑)行(r2剪辑))(<;=(c1剪辑)列(c2剪辑))(funcall(fn剪辑)行列)(defmethod call-with-clipping((Buffer)cont&;密钥r1c1R2 c2fn)(let((剪辑(剪辑缓冲区)(let((old-r1(r1剪辑))(old-c1(c1剪辑))(old-R2(r2剪辑))(old-c2(c2剪辑))(old-fn(fn剪辑)(SETF(r1剪辑)(max(或r1old-r1)old-r1)(c1剪辑)(max(或c1剪辑)。old-c1)(r2剪辑)(min(或r2old-r2)old-r2)(c2剪辑)(min(或c2 old-c2)old-c2)(fn剪辑)(if(NULL Fn)old-fn(λ(行列)(AND(funcall fn行列)(funcall old-fn行列)(展开保护(Funcall Cont)(SETF(r1剪辑)old-r1。(c1剪辑)old-c1(R2剪辑)old-R2(c2剪辑)old-c2(fn剪辑)old-fn)。

最后,我们可以修改Console类本身。带有-console的宏单独绑定了一个缓冲区,因此我们可以同时访问输出缓冲区和控制台。

(def宏with-console((&;rest args&;key IOS FGC BGC CVP fps&;Allow-Other-Key)&;Body Body)(DECLARE(忽略FGC BGC CVP fps))`(let*((*Terminal*,iOS)(*console*(make-instance';console,@args)(展开保护(with-buffer(*console*),@body)(关闭终端(hnd*console*)。

(defunUPDATE-CONSOLE-DIMEMS()(WITH-CURSOR-POSITION((EXPT 216)(EXPT 216))(Multiple-Value-Bind(ROWS COLS)(GET-CURSOR-POSITION)(SETF(ROWS*CONSOLE*)ROWS)(SETF(COLS*CONSOLE*)COLS)(SETF(R2(CLIP*CONSOLE*))ROWS)(SETF(c2(CLIP*CONSOLE*))COLS))。

并且类控制台本身被重塑为从类缓冲区继承。请注意,我们去掉了位置和应用程序插槽。

(defclass控制台(缓冲区)((IOS:initarg:IOS:存取器IOS:Documentation";控制台I/O流。";)(cvp:initarg:cvp:存取器cvp:Documentation";光标可见性。)(ptr:initarg:ptr:存取器ptr:Documentation";指针跟踪。";)(fps:initarg:fps:存取器fps:Documentation";)。)(hnd:访问器hnd:Documentation";终端处理程序。";))(:default-initargs:IOS(错误";必须指定I/O流。";):fgc#xffa0a000:bgc#x222200:行1:列1:cvp nil:ptr t:fps 10)(defmethod initialize-instance:After(实例控制台)&;Key FGC BGC行列CVP PTR)(SETF(HND实例)(初始化终端))(设置前景色FGC)(设置背景色BGC)(设置光标位置行列)(设置光标可见性CVP)(设置鼠标跟踪PTR)(let((*控制台*实例))(更新控制台维度))(defmethod(SETF FGC):After(RGBA(实例控制台))(设置前景。-color RGBA))(defmethod(SETF BGC):After(RGBA(实例控制台))(set-back-color RGBA))(defmethod(SETF Row):After(row(实例控制台))(设置游标位置行为空)(defmethod(SETF Column):After(ol(实例控制台))(Set-Cursor-Position nil ol))(defmethod(SETF PTR):After(PTR(实例控制台))。))(set-鼠标跟踪(not(Null Ptr)(defmethod(Setf Cvp):After(cvp(实例控制台))(set-Cursor-Visibility(not(Null Cvp)。

将单元格放到屏幕上需要先设置光标位置和单元格颜色,然后调用函数put。函数put-cell负责验证该单元是否在裁剪区域内。

(defmethod put-cell((缓冲控制台)行列fg bg)(WHEN(内部-p缓冲行列)(设置光标位置行列)(设置前景色fg)(设置背景色bg)(Put Ch)

最后,我们需要考虑with-clipping宏中的更改,将缓冲区作为第一个参数传递,并删除对app访问器的引用。修改功能显示画面:

(deFun show-screen()(loop for ch=(read-input)Until(Null Ch)do(cond((keyp ch#\q:c)(cl-user::Quit)((keyp ch#\u:c)(Ignore-Errors(user-action)(Flet((ll(行列)(or(or(and(<;(ABS(-(+列)26))2)(<;=列20))(<;(abs(-(+(-40列)行)26))2)(带-剪裁(*缓冲*:fn#';L:r1 2:r2 11)(OUT(:ROW(1+(随机12)):COL(1+(随机40)):bgc#x00000000:fgc#xbb000000)(亚历山大:Random-ELT';(";X";";O";)(With-Clip(*Buffer*:fn(lambda(行列)(或(=行1)(=行12)(funcall(补码#';11)行列)(OUT(:行(1+(随机12)):COL(1+(随机40)):bgc#x00000000:fgc(亚历山大:随机ELT';(#x00444400#x00444400#x00664400)(亚历山大:Random-ELT';(";+";-";)。

所有这些更改都是非常有侵入性的,因此请确保重新启动映像并再次尝试运行应用程序,以确保一切仍然正常。

是时候编写一个新的示例应用程序了。坐着别动,我们在写窗口管理器!为了与CLIM术语兼容,我们称它为帧管理器。每个应用程序将由其边界框和呈现函数定义的帧表示。

(defclass frame-manager()((Frames:initarg:Frames:Accessor Frame:Documentation";All Frame";)(active:initarg:active:Active:Active Frame:Documentation";Active Frame";))(:default-initargs:frame nil:active nil));haha,完全不是剪辑。(Defclass Frame(Bbox)((fn:initarg:fn:存取器fn))(:default-initargs:r1 1:c1 1:r2 24:C2 80:fn(常量t)。

显示帧涉及在启用剪裁的情况下调用呈现函数,并显示装饰。通常单元格的宽度小于其高度,因此如果我们想要节省一些空间,在应用程序一侧将装饰绘制为竖条更有意义。这就是我们要做的。活动框架将用不同的侧栏颜色表示。

(去趣化渲染应用程序(fm帧)(with-clipping(*buffer*:r1(r1帧):c1(c1帧):R2(R2帧):C2(c2帧))(funcall(fn帧)(去趣化渲染装饰(fm帧)(声明(忽略fm))(循环,用于第(1+(c2帧))至(1+(c2帧))的行)(去趣味渲染-应用程序(fm帧)(with-clipping(*buffer*:r1帧):c1(c1帧)):R2(R2帧):(funcall(fn帧)。1-(R2帧))DO(OUT(:ROW:COL COL)";";)最后(OUT(:COLCOL:ROW(R1帧):FGC#xff224400)";x";)(OUT(:COLCOL:ROW(R2帧))";/";)(deFun Display-Screen(FM)(Dolist(Frame(Frame FM))(if(eq Frame(Active FM))(ctl(:bgc#x22224400)(:fgc#xffffff00))(ctl(:bgc#x1111100)(:fgc#xbbbbb00)(渲染应用程序FM帧)(渲染装饰FM帧)。

(去趣句柄-事件(FM事件)(Flet((Reset()(ctl(:bgc#x22222200))(update-console-Dimension)(清除终端)(cond((keyp event#\q:C)(cl-user::Quit))((keyp event#\R:C)(Reset))((keyp event#\N:C)(Alexandria:IF-let。((CUR(活动FM)(let*((fms(帧FM))(POS(位置FMS))(NEW(1+POS)(IF(=NEW(长度FMS))(SETF(活动FM)NIL)(SETF(活动FM)(ELT FMS NEW)(SETF(活动FM)(FIRST(FRAMES FM)((KEYIP EVENT#\U。:c)(Ignore-Errors(用户操作)((Keyp Event#\E:C)(Error";bam";)(deun start-display()(LOOP(with-Simple-Restart(再一次";再次启动显示。";))(忽略错误(swank:create-server))(Handler-case(with-console(:IOS*Terminal-io*))

..