trial.terminal 108/1312(8%) line coverage

      
10
20
30
40
50
60
70
80
90
100
110
120
130
140
150
160
170
180
190
200
210
220
230
240
250
260
270
280
290
300
310
320
330
340
350
360
370
380
390
400
410
420
430
440
450
460
470
480
490
500
510
520
530
540
550
560
570
580
590
600
610
620
630
640
650
660
670
680
690
700
710
720
730
740
750
760
770
780
790
800
810
820
830
840
850
860
870
880
890
900
910
920
930
940
950
960
970
980
990
1000
1010
1020
1030
1040
1050
1060
1070
1080
1090
1100
1110
1120
1130
1140
1150
1160
1170
1180
1190
1200
1210
1220
1230
1240
1250
1260
1270
1280
1290
1300
1310
1320
1330
1340
1350
1360
1370
1380
1390
1400
1410
1420
1430
1440
1450
1460
1470
1480
1490
1500
1510
1520
1530
1540
1550
1560
1570
1580
1590
1600
1610
1620
1630
1640
1650
1660
1670
1680
1690
1700
1710
1720
1730
1740
1750
1760
1770
1780
1790
1800
1810
1820
1830
1840
1850
1860
1870
1880
1890
1900
1910
1920
1930
1940
1950
1960
1970
1980
1990
2000
2010
2020
2030
2040
2050
2060
2070
2080
2090
2100
2110
2120
2130
2140
2150
2160
2170
2180
2190
2200
2210
2220
2230
2240
2250
2260
2270
2280
2290
2300
2310
2320
2330
2340
2350
2360
2370
2380
2390
2400
2410
2420
2430
2440
2450
2460
2470
2480
2490
2500
2510
2520
2530
2540
2550
2560
2570
2580
2590
2600
2610
2620
2630
2640
2650
2660
2670
2680
2690
2700
2710
2720
2730
2740
2750
2760
2770
2780
2790
2800
2810
2820
2830
2840
2850
2860
2870
2880
2890
2900
2910
2920
2930
2940
2950
2960
2970
2980
2990
3000
3010
3020
3030
3040
3050
3060
3070
3080
3090
3100
3110
3120
3130
3140
3150
3160
3170
3180
3190
3200
3210
3220
3230
3240
3250
3260
3270
3280
3290
3300
3310
3320
3330
3340
3350
3360
3370
3380
3390
3400
3410
3420
3430
3440
3450
3460
3470
3480
3490
3500
3510
3520
3530
3540
3550
3560
3570
3580
3590
3600
3610
3620
3630
3640
3650
3660
3670
3680
3690
3700
3710
3720
3730
3740
3750
3760
3770
3780
3790
3800
3810
3820
3830
3840
3850
3860
3870
3881
38912
3903
3910
3920
3931
3940
3950
3960
3970
3980
3990
4000
4010
4020
4030
4040
4050
4060
4070
4080
4090
4100
4110
4121
4131
4140
4151
4160
4170
41833
41915
4200
42118
42218
42318
42417
4250
426204
42750
4280
42918
4300
4310
4320
433143
43415
43515
4360
437128
43810
439118
440118
441100
442118
4430
444118
44518
4460
4470
4481
449432
450143
4510
4520
4530
4540
4550
4560
4570
4580
4590
4600
4610
4621
4631
4640
4651
4661
4671
4680
4690
4700
4710
4720
4730
4740
4750
4760
4770
4780
4790
4800
4810
4820
4830
4840
4850
4860
4870
4881
4891
4901
4910
4920
4931
4941
4950
4963
4970
4980
4990
5000
5010
5020
5030
5040
5050
5060
5070
5080
5090
5100
5110
5120
5130
5140
5150
5160
5170
5180
5190
5200
5210
5220
5230
5240
5250
5260
5270
5280
5290
5300
5310
5320
5330
5340
5350
5360
5370
5380
5390
5400
5410
5420
5430
5440
5450
5460
5470
5480
5490
5500
5510
5520
5530
5540
5550
5560
5570
5580
5590
5600
5610
5620
5630
5640
5650
5660
5670
5680
5690
5700
5710
5720
5730
5740
5750
5760
5770
5780
5790
5800
5810
5820
5830
5840
5850
5860
5870
5880
5890
5900
5910
5920
5930
5940
5950
5960
5970
5980
5990
6000
6010
6020
6030
6040
6050
6060
6070
6080
6090
6100
6110
6120
6130
6140
6150
6160
6170
6180
6190
6200
6210
6220
6230
6240
6250
6260
6270
6280
6290
6300
6310
6320
6330
6340
6350
6360
6370
6380
6390
6400
6410
6420
6430
6440
6450
6460
6470
6480
6490
6500
6510
6520
6530
6540
6550
6560
6570
6580
6590
6600
6610
6620
6630
6640
6650
6660
6670
6680
6690
6700
6710
6720
6730
6740
6750
6760
6770
6780
6790
6801
6811
6821
6831
6841
6850
6861
6870
6881
6890
6900
6910
6920
6931
6940
6950
6960
6970
6980
6991
7000
7010
7020
7030
7040
7050
7060
7070
7080
7090
7100
7110
7120
7130
7140
7150
7160
7170
7180
7190
7200
7210
7220
7230
7240
7250
7260
7270
7280
7290
7300
7310
7320
7330
7340
7350
7360
7370
7380
7390
7400
7410
7420
7430
7440
7450
7460
7470
7480
7490
7500
7510
7520
7530
7540
7550
7560
7570
7580
7590
7600
7610
7620
7630
7640
7650
7660
7670
7680
7690
7700
7710
7720
7730
7740
7750
7760
7770
7780
7790
7800
7810
7820
7830
7840
7850
7860
7870
7880
7890
7900
7910
7920
7930
7940
7950
7960
7970
7980
7990
8000
8010
8020
8030
8040
8050
8060
8070
8080
8090
8100
8110
8120
8130
8140
8150
8160
8170
8180
8190
8200
8210
8220
8230
8240
8250
8260
8270
8280
8290
8300
8310
8320
8330
8340
8350
8360
8370
8380
8390
8400
8410
8420
8430
8440
8450
8460
8470
8480
8490
8500
8510
8520
8530
8540
8550
8560
8570
8580
8590
8600
8611515
8620
8630
8640
8650
8660
8670
8680
8690
8700
8710
8720
8730
8740
8750
8760
8770
8780
8790
8800
8810
8820
8830
8840
8850
8860
8870
8880
8890
8900
8910
8920
8930
8940
8950
8960
8970
8980
8990
9000
9010
9020
9030
9040
9050
9060
9070
9081515
9091515
9100
9111515
9120
9131515
9141515
9150
9160
9170
9184545
9191515
9203030
9210
9220
9231515
9240
9250
9260
9270
9280
9291515
9301515
9311515
9320
9330
9340
9350
9360
9370
9380
9390
9400
9410
9420
9430
9440
9450
9460
9470
9480
9490
9500
9510
9520
9530
9540
9550
9560
9571515
9580
9591515
9601515
9611515
9621515
9630
9640
9650
9660
9670
9680
9690
9700
9710
9720
9730
9741
9750
9760
9770
9780
9790
9800
9810
9820
9830
9840
9850
9860
9870
9880
9890
9900
9910
9920
9930
9940
9950
9960
9970
9980
9990
10000
10010
10020
10030
10040
10050
10060
10070
10080
10090
10100
10110
10120
10130
10140
10150
10160
10170
10180
10190
10200
10210
10220
10230
10240
10250
10260
10270
10280
10290
10300
10310
10320
10330
10340
10350
10360
10370
10380
10390
10400
10410
10420
10430
10440
10450
10460
10470
10480
10490
10500
10510
10520
10530
10540
10550
10560
10570
10580
10590
10600
10610
10620
10630
10640
10650
10660
10670
10680
10690
10700
10710
10720
10730
10740
10750
10760
10770
10780
10790
10803030
10810
10820
10830
10843030
10850
10860
10873030
10880
10896060
10903030
10913030
10920
10933030
10940
10950
10960
10970
10980
10990
11000
11010
11020
11030
11040
11050
11060
11070
11080
11090
11100
11110
11120
11130
11140
11150
11160
11170
11180
11190
11200
11210
11220
112328372
112428372
112528372
112628372
11270
11280
11290
11300
11310
113228372
113328372
113428372
11350
11360
11370
11380
11390
11400
11410
114214186
114314186
114414186
11450
11460
11470
11480
114914186
115014186
115114186
11520
11530
11540
11550
11560
11570
11580
11590
11600
11610
11620
11630
11640
11650
11660
11670
11680
11690
11700
11710
11720
11730
11740
11750
11761515
11771515
11781515
11790
11800
11811515
11820
11830
11840
11850
11860
11870
11880
11890
11900
11910
11920
11930
11940
11950
11960
11970
11980
11990
12000
12010
12020
12030
12040
12050
12060
12070
12080
12090
12100
12110
12120
12130
12140
12150
121647103
121714186
1218343
1219343
1220343
1221343
12220
12230
12240
12250
12260
12270
12280
122913843
123013843
123113178
12320
12330
123428372
123513178
123613178
12370
12380
123914186
12400
12410
12420
12430
12440
12450
12460
12470
12480
12490
12501515
12510
12520
12530
12540
12550
12560
12570
12580
12590
12600
12610
12620
12634545
12640
12650
12660
12670
12680
12690
12700
12710
12720
12730
12740
12750
12760
12770
12780
12790
12800
12810
12820
12830
12840
12850
12860
12870
12880
12890
12900
12910
12920
12930
12940
12950
12960
12970
12980
12990
13000
13010
13020
13030
13040
13050
13060
13070
13080
13090
13100
13110
13120
13130
13140
13150
13160
13170
13180
13190
13200
13210
13220
13230
13240
13250
13260
13270
13280
13290
13300
13310
13320
13330
13340
13350
13360
13370
13380
13390
13400
13410
13420
13430
13440
13450
13460
13470
13480
13490
13500
13510
13520
13530
13540
13550
13560
13570
13580
13590
13600
13610
13620
13630
13640
13650
13660
13670
13680
13690
13700
13710
13720
13730
13740
13750
13760
13770
13780
13790
13800
13810
13820
13830
13840
13850
13860
13870
13880
13890
13900
13910
13920
13930
13940
13950
13960
13970
13980
13990
14000
14010
14020
14030
14040
14050
14060
14070
14080
14090
14100
14110
14120
14130
14140
14150
14160
14170
14180
14190
14200
14210
14220
14230
14240
14250
14260
14270
14280
14290
14300
14310
14320
14330
14340
14350
14360
14370
14380
14390
14400
14410
14420
14430
14440
14450
14460
14470
14480
14490
14500
14510
14520
14530
14540
14550
14560
14570
14580
14590
14600
14610
14620
14630
14640
14650
14660
14670
14680
14690
14700
14710
14720
14730
14740
14750
14760
14770
14780
14790
14800
14810
14820
14830
14840
14850
14860
14870
14880
14890
14900
14910
14920
14930
14940
14950
14960
14970
14980
14990
15000
15010
15020
15030
15040
15050
15060
15070
15080
15090
15100
15110
15120
15130
15140
15150
15160
15170
15180
15190
15200
15210
15220
15230
15240
15250
15260
15270
15280
15290
15300
15310
15320
15330
15340
15350
15360
15370
15380
15390
15400
15410
15420
15430
15440
15450
15460
15470
15480
15490
15500
15510
15520
15530
15540
15550
15560
15570
15580
15590
15600
15610
15620
15630
15640
15650
15660
15670
15680
15690
15700
15710
15720
15730
15740
15750
15760
15770
15780
15790
15800
15810
15820
15830
15840
15850
15860
15870
15880
15890
15900
15910
15920
15930
15940
15950
15960
15970
15980
15990
16000
16010
16020
16030
16040
16050
16060
16070
16080
16090
16100
16110
16120
16130
16140
16150
16160
16170
16180
16190
16200
16210
16220
16230
16240
16250
16260
16270
16280
16290
16300
16310
16320
16330
16340
16350
16360
16370
16380
16390
16400
16410
16420
16430
16440
16450
16460
16470
16480
16490
16500
16510
16520
16530
16540
16550
16560
16570
16580
16590
16600
16610
16620
16630
16640
16650
16660
16670
16680
16690
16700
16710
16720
16730
16740
16750
16760
16770
16780
16790
16800
16810
16820
16830
16840
16850
16860
16870
16880
16890
16900
16910
16920
16930
16940
16950
16960
16970
16980
16990
17000
17010
17020
17030
17040
17050
17060
17070
17080
17090
17100
17110
17120
17130
17140
17150
17160
17170
17180
17190
17200
17210
17220
17230
17240
17250
17260
17270
17280
17290
17300
17310
17320
17330
17340
17350
17360
17370
17380
17390
17400
17410
17420
17430
17440
17450
17460
17470
17480
17490
17500
17510
17520
17530
17540
17550
17560
17570
17580
17590
17600
17610
17620
17630
17640
17650
17660
17670
17680
17690
17700
17710
17720
17730
17740
17750
17760
17770
17780
17790
17800
17810
17820
17830
17840
17850
17860
17870
17880
17890
17900
17910
17920
17930
17940
17950
17960
17970
17980
17990
18000
18010
18020
18030
18040
18050
18060
18070
18080
18090
18100
18110
18120
18130
18140
18150
18160
18170
18180
18190
18200
18210
18220
18230
18240
18250
18260
18270
18280
18290
18300
18310
18320
18330
18340
18350
18360
18370
18380
18390
18400
18410
18420
18430
18440
18450
18460
18470
18480
18490
18500
18510
18520
18530
18540
18550
18560
18570
18580
18590
18600
18610
18620
18630
18640
18650
18660
18670
18680
18690
18700
18710
18720
18730
18740
18750
18760
18770
18780
18790
18800
18810
18820
18830
18840
18850
18860
18870
18880
18890
18900
18910
18920
18930
18940
18950
18960
18970
18980
18990
19000
19010
19020
19030
19040
19050
19060
19070
19080
19090
19100
19110
19120
19130
19140
19150
19160
19170
19180
19190
19200
19210
19220
19230
19240
19250
19260
19270
19280
19290
19300
19310
19320
19330
19340
19350
19360
19370
19380
19390
19400
19410
19420
19430
19440
19450
19460
19470
19480
19490
19500
19510
19520
19530
19540
19550
19560
19570
19580
19590
19600
19610
19620
19630
19640
19650
19660
19670
19680
19690
19700
19710
19720
19730
19740
19750
19760
19770
19780
19790
19800
19810
19820
19830
19840
19850
19860
19870
19880
19890
19900
19910
19920
19930
19940
19950
19960
19970
19980
19990
20000
20010
20020
20030
20040
20050
20060
20070
20080
20090
20100
20110
20120
20130
20140
20150
20160
20170
20180
20190
20200
20210
20220
20230
20240
20250
20260
20270
20280
20290
20300
20310
20320
20330
20340
20350
20360
20370
20380
20390
20400
20410
20420
20430
20440
20450
20460
20470
20480
20490
20500
20510
20520
20530
20540
20550
20560
20570
20580
20590
20600
20610
20620
20630
20640
20650
20660
20670
20680
20690
20700
20710
20720
20730
20740
20750
20760
20770
20780
20790
20800
20810
20820
20830
20840
20850
20860
20870
20880
20890
20900
20910
20920
20930
20940
20950
20960
20970
20980
20990
21000
21010
21020
21030
21040
21050
21060
21070
21080
21090
21100
21110
21120
21130
21140
21150
21160
21170
21180
21190
21200
21210
21220
21230
21240
21250
21260
21270
21280
21290
21300
21310
21320
21330
21340
21350
21360
21370
21380
21390
21400
21410
21420
21430
21440
21450
21460
21470
21480
21490
21500
21510
21520
21530
21540
21550
21560
21570
21580
21590
21600
21610
21620
21630
21640
21650
21660
21670
21680
21690
21700
21710
21720
21730
21740
21750
21760
21770
21780
21790
21800
21810
21820
21830
21840
21850
21860
21870
21880
21890
21900
21910
21920
21930
21940
21950
21960
21970
21980
21990
22000
22010
22020
22030
22040
22050
22060
22070
22080
22090
22100
22110
22120
22130
22140
22150
22160
22170
22180
22190
22200
22210
22220
22230
22240
22250
22260
22270
22280
22290
22300
22310
22320
22330
22340
22350
22360
22370
22380
22390
22400
22410
22420
22430
22440
22450
22460
22470
22480
22490
22500
22510
22520
22530
22540
22550
22560
22570
22580
22590
22600
22610
22620
22630
22640
22650
22660
22670
22680
22690
22700
22710
22720
22730
22740
22750
22760
22770
22780
22790
22800
22810
22820
22830
22840
22850
22860
22870
22880
22890
22900
22910
22920
22930
22940
22950
22960
22970
22980
22990
23000
23010
23020
23030
23040
23050
23060
23070
23080
23090
23100
23110
23120
23130
23140
23150
23160
23170
23180
23190
23200
23210
23220
23230
23240
23250
23260
23270
23280
23290
23300
23310
23320
23330
23340
23350
23360
23370
23380
23390
23400
23410
23420
23430
23440
23450
23460
23470
23480
23490
23500
23510
23520
23530
23540
23550
23560
23570
23580
23590
23600
23610
23620
23630
23640
23650
23660
23670
23680
23690
23700
23710
23720
23730
23740
23750
23760
23770
23780
23790
23800
23810
23820
23830
23840
23850
23860
23870
23880
23890
23900
23910
23920
23930
23940
23950
23960
23970
23980
23990
24000
24010
24020
24030
24040
24050
24060
24070
24080
24090
24100
24110
24120
24130
24140
24150
24160
24170
24180
24190
24200
24210
24220
24230
24240
24250
24260
24270
24280
24290
24300
24310
24320
24330
24340
24350
24360
24370
24380
24390
24400
24410
24420
24430
24440
24450
24460
24470
24480
24490
24500
24510
24520
24530
24540
24550
24560
24570
24580
24590
24600
24610
24620
24630
24640
24650
24660
24670
24680
24690
24700
24710
24720
24730
24740
24750
24760
24770
24780
24790
24800
24810
24820
24830
24840
24850
24860
24870
24880
24890
24900
24910
24920
24930
24940
24950
24960
24970
24980
24990
25000
25010
25020
25030
25040
25050
25060
25070
25080
25090
25100
25110
25120
25130
25140
25150
25160
25170
25180
25190
25200
25210
25220
25230
25240
25250
25260
25270
25280
25290
25300
25310
25320
25330
25340
25350
25360
25370
25380
25390
25400
25410
25420
25430
25440
25450
25460
25470
25480
25490
25500
25510
25520
25530
25540
25550
25560
25570
25580
25590
25600
25610
25620
25630
25640
25650
25660
25670
25680
25690
25700
25710
25720
25730
25740
25750
25760
25770
25780
25790
25800
25810
25820
25830
25840
25850
25860
25870
25880
25890
25900
25910
25920
25930
25940
25950
25960
25970
25980
25990
26000
26010
26020
26030
26040
26050
26060
26070
26080
26090
26100
26110
26120
26130
26140
26150
26160
26170
26180
26190
26200
26210
26220
26230
26240
26250
26260
26270
26280
26290
26300
26310
26320
26330
26340
26350
26360
26370
26380
26390
26400
26410
26420
26430
26440
26450
26460
26470
26480
26490
26500
26510
26520
26530
26540
26550
26560
26570
26580
26590
26600
26610
26620
26630
26640
26650
26660
26670
26680
26690
26700
26710
26720
26730
26740
26750
26760
26770
26780
26790
26800
26810
26820
26830
26840
26850
26860
26870
26880
26890
26900
26910
26920
26930
26940
26950
26960
26970
26980
26990
27000
27010
27020
27030
27040
27050
27060
27070
27080
27090
27100
27110
27120
27130
27140
27150
27160
27170
27180
27190
27200
27210
27220
27230
27240
27250
27260
27270
27280
27290
27300
27310
27320
27330
27340
27350
27360
27370
27380
27390
27400
27410
27420
27430
27440
27450
27460
27470
27480
27490
27500
27510
27520
27530
27540
27550
27560
27570
27580
27590
27600
27610
27620
27630
27640
27650
27660
27670
27680
27690
27700
27710
27720
27730
27740
27750
27760
27770
27780
27790
27800
27810
27820
27830
27840
27850
27860
27870
27880
27890
27900
27910
27920
27930
27940
27950
27960
27970
27980
27990
28000
28010
28020
28030
28040
28050
28060
28070
28080
28090
28100
28110
28120
28130
28140
28150
28160
28170
28180
28190
28200
28210
28220
28230
28240
28250
28260
28270
28280
28290
28300
28310
28320
28330
28340
28350
28360
28370
28380
28390
28400
28410
28420
28430
28440
28450
28460
28470
28480
28490
28500
28510
28520
28530
28540
28550
28560
28570
28580
28590
28600
28610
28620
28630
28640
28650
28660
28670
28680
28690
28700
28710
28720
28730
28740
28750
28760
28770
28780
28790
28800
28810
28820
28830
28840
28850
28860
28870
28880
28890
28900
28910
28920
28930
28940
28950
28960
28970
28980
28990
29000
29010
29020
29030
29040
29050
29060
29070
29080
29090
29100
29110
29120
29130
29140
29150
29160
29170
29180
29190
29200
29210
29220
29230
29240
29250
29260
29270
29280
29290
29300
29310
29320
29330
29340
29350
29360
29370
29380
29390
29400
29410
29420
29430
29440
29450
29460
29470
29480
29490
29500
29510
29520
29530
29540
29550
29560
29570
29580
29590
29600
29610
29620
29630
29640
29650
29660
29670
29680
29690
29700
29710
29720
29730
29740
29750
29760
29770
29780
29790
29800
29810
29820
29830
29840
29850
29860
29870
29880
29890
29900
29910
29920
29930
29940
29950
29960
29970
29980
29990
30000
30010
30020
30030
30040
30050
30060
30070
30080
30090
30100
30110
30120
30130
30140
30150
30160
30170
30180
30190
30200
30210
30220
30230
30240
30250
30260
30270
30280
30290
30300
30310
30320
30330
30340
30350
30360
30370
30380
30390
30400
30410
30420
30430
30440
30450
30460
30470
30480
30490
30500
30510
30520
30530
30540
30550
30560
30570
30580
30590
30600
30610
30620
30630
30640
30650
30660
30670
30680
30690
30700
30710
30720
30730
30740
30750
30760
30770
30780
30790
30800
30810
30820
30830
30840
30850
30860
30870
30880
30890
30900
30910
30920
30930
30940
30950
30960
30970
30980
30990
31000
31010
31020
31030
31040
31050
31060
31070
31080
31090
31100
31110
31120
31130
31140
31150
31160
31170
31180
31190
31200
31210
31220
31230
31240
31250
31260
31270
31280
31290
31300
31310
31320
31330
31340
31350
31360
31370
31380
31390
31400
31410
31420
31430
31440
31450
31460
31470
31480
31490
31500
31510
31520
31530
31540
31550
31560
31570
31580
31590
31600
31610
31620
31630
31640
31650
31660
31670
31680
31690
31700
31710
31720
31730
31740
31750
31760
31770
31780
31790
31800
31810
31820
31830
31840
31850
31860
31870
31880
31890
31900
31910
31920
31930
31940
31950
31960
31970
31980
31990
32000
32010
32020
32030
32040
32050
32060
32070
32080
32090
32100
32110
32120
32130
32140
32150
32160
32170
32180
32190
32200
32210
32220
32230
32240
32250
32260
32270
32280
32290
32300
32310
32320
32330
32340
32350
32360
32370
32380
32390
32400
32410
32420
32430
32440
32450
32460
32470
32480
32490
32500
32510
32520
32530
32540
32550
32560
32570
32580
32590
32600
32610
32620
32630
32640
32650
32660
32670
32680
32690
32700
32710
32720
32730
32740
32750
32760
32770
32780
32790
32800
32810
32820
32830
32840
32850
32860
32870
32880
32890
32900
32910
32920
32930
32940
32950
32960
32970
32980
32990
33000
33010
33020
33030
33040
33050
33060
33070
33080
33090
33100
33110
33120
33130
33140
33150
33160
33170
33180
33190
33200
33210
33220
33230
33240
33250
33260
33270
33280
33290
33300
33310
33320
33330
33340
33350
33360
33370
33380
33390
33400
33410
33420
33430
33440
33450
33460
33470
33480
33490
33500
33510
33520
33530
33540
33550
33560
33570
33580
33590
33600
33610
33620
33630
33640
33650
33660
33670
33680
33690
33700
33710
33720
33730
33740
33750
33760
33770
33780
33790
33800
33810
33820
33830
33840
33850
33860
33870
33880
33890
33900
33910
33920
33930
33940
33950
33960
33970
33980
33990
34000
34010
34020
34030
34040
34050
34060
34070
34080
34090
34100
34110
34120
34130
34140
34150
34160
34170
34180
34190
34200
34210
34220
34230
34240
34250
34260
34270
34280
34290
34300
34310
34320
34330
34340
34350
34360
34370
34380
34390
34400
34410
34420
34430
34440
34450
34460
34470
34480
34490
34500
34510
34520
34530
34540
34550
34560
34570
34580
34590
34600
34610
34620
34630
34640
34650
34660
34670
34680
34690
34700
34710
34720
34730
34740
34750
34760
34770
34780
34790
34800
34810
34820
34830
34840
34850
34860
34870
34880
34890
34900
34910
34920
34930
34940
34950
34960
34970
34980
34990
35000
35010
35020
35030
35040
35050
35060
35070
35080
35090
35100
35110
35120
35130
35140
35150
35160
35170
35180
35190
35200
35210
35220
35230
35240
35250
35260
35270
35280
35290
35300
35310
35320
35330
35340
35350
35360
35370
35380
35390
35400
35410
35420
35430
35440
35450
35460
35470
35480
35490
35500
35510
35520
35530
35540
35550
35560
35570
35580
35590
35600
35610
35620
35630
35640
35650
35660
35670
35680
35690
35700
35710
35720
35730
35740
35750
35760
35770
35780
35790
35800
35810
35820
35830
35840
35850
35860
35870
35880
35890
35900
35910
35920
35930
35940
35950
35960
35970
35980
35990
36000
36010
36020
36030
36040
36050
36060
36070
36080
36090
36100
36110
36120
36130
36140
36150
36160
36170
36180
36190
36200
36210
36220
36230
36240
36250
36260
36270
36280
36290
36300
36310
36320
36330
36340
36350
36360
36370
36380
36390
36400
36410
36420
36430
36440
36450
36460
36470
36480
36490
36500
36510
36520
36530
36540
36550
36560
36570
36580
36590
36600
36610
36620
36630
36640
36650
36660
36670
36680
36690
36700
36710
36720
36730
36740
36750
36760
36770
36780
36790
36800
36810
36820
36830
36840
36850
36860
36870
36880
36890
36900
36910
36920
36930
36940
36950
36960
36970
36980
36990
37000
37010
37020
37030
37040
37050
37060
37070
37080
37090
37100
37110
37120
37130
37140
37150
37160
37170
37180
37190
37200
37210
37220
37230
37240
37250
37260
37270
37280
37290
37300
37310
37320
37330
37340
37350
37360
37370
37380
37390
37400
37410
37420
37430
37440
37450
37460
37470
37480
37490
37500
37510
37520
37530
37540
37550
37560
37570
37580
37590
37600
37610
37620
37630
37640
37650
37660
37670
37680
37690
37700
37710
37720
37730
37740
37750
37760
37770
37780
37790
37800
37810
37820
37830
37840
37850
37860
37870
37880
37890
37900
37910
37920
37930
37940
37950
37960
37970
37980
37990
38000
38010
38020
38030
38040
38050
38060
38070
38080
38090
38100
38110
38120
38130
38140
38150
38160
38170
38180
38190
38200
38210
38220
38230
38240
38250
38260
38270
38280
38290
38300
38310
38320
38330
38340
38350
38360
38370
38380
38390
38400
38410
38420
38430
38440
38450
38460
38470
38480
38490
38500
38510
38520
38530
38540
38550
38560
38570
38580
38590
38600
38610
38620
38630
38640
38650
38660
38670
38680
38690
38700
38710
38720
38730
38740
38750
38760
38770
38780
38790
38800
38810
38820
38830
38840
38850
38860
38870
38880
38890
38900
38910
38920
38930
38940
38950
38960
38970
38980
38990
39000
39010
39020
39030
39040
39050
39060
39070
39080
39090
39100
39110
39120
39130
39140
39150
39160
39170
39180
39190
39200
39210
39220
39230
39240
39250
39260
39270
39280
39290
39300
39310
39320
39330
39340
39350
39360
39370
39380
39390
39400
39410
39420
39430
39440
39450
39460
39470
39480
39490
39500
39510
39520
39530
39540
39550
39560
39570
39580
39590
39600
39610
39620
39630
39640
39650
39660
39670
39680
39690
39700
39710
39720
/++ Ignore coverage Module for interacting with the user's terminal, including color output, cursor manipulation, and full-featured real-time mouse and keyboard input. Also includes high-level convenience methods, like [Terminal.getline], which gives the user a line editor with history, completion, etc. See the [#examples]. The main interface for this module is the Terminal struct, which encapsulates the output functions and line-buffered input of the terminal, and RealTimeConsoleInput, which gives real time input. Creating an instance of these structs will perform console initialization. When the struct goes out of scope, any changes in console settings will be automatically reverted. Note: on Posix, it traps SIGINT and translates it into an input event. You should keep your event loop moving and keep an eye open for this to exit cleanly; simply break your event loop upon receiving a UserInterruptionEvent. (Without the signal handler, ctrl+c can leave your terminal in a bizarre state.) As a user, if you have to forcibly kill your program and the event doesn't work, there's still ctrl+\ On Mac Terminal btw, a lot of hacks are needed and mouse support doesn't work. Most functions basically work now though. Future_Roadmap: $(LIST * The CharacterEvent and NonCharacterKeyEvent types will be removed. Instead, use KeyboardEvent on new programs. * The ScrollbackBuffer will be expanded to be easier to use to partition your screen. It might even handle input events of some sort. Its API may change. * getline I want to be really easy to use both for code and end users. It will need multi-line support eventually. * I might add an expandable event loop and base level widget classes. This may be Linux-specific in places and may overlap with similar functionality in simpledisplay.d. If I can pull it off without a third module, I want them to be compatible with each other too so the two modules can be combined easily. (Currently, they are both compatible with my eventloop.d and can be easily combined through it, but that is a third module.) * More advanced terminal features as functions, where available, like cursor changing and full-color functions. * The module will eventually be renamed to `arsd.terminal`. * More documentation. ) WHAT I WON'T DO: $(LIST * support everything under the sun. If it isn't default-installed on an OS I or significant number of other people might actually use, and isn't written by me, I don't really care about it. This means the only supported terminals are: $(LIST * xterm (and decently xterm compatible emulators like Konsole) * Windows console * rxvt (to a lesser extent) * Linux console * My terminal emulator family of applications https://github.com/adamdruppe/terminal-emulator ) Anything else is cool if it does work, but I don't want to go out of my way for it. * Use other libraries, unless strictly optional. terminal.d is a stand-alone module by default and always will be. * Do a full TUI widget set. I might do some basics and lay a little groundwork, but a full TUI is outside the scope of this module (unless I can do it really small.) ) +/ module trial.terminal; /* Widgets: tab widget scrollback buffer partitioned canvas */ // FIXME: ctrl+d eof on stdin // FIXME: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686016%28v=vs.85%29.aspx version(Posix) { enum SIGWINCH = 28; __gshared bool windowSizeChanged = false; __gshared bool interrupted = false; /// you might periodically check this in a long operation and abort if it is set. Remember it is volatile. It is also sent through the input event loop via RealTimeConsoleInput __gshared bool hangedUp = false; /// similar to interrupted. version(with_eventloop) struct SignalFired {} extern(C) void sizeSignalHandler(int sigNumber) nothrow { windowSizeChanged = true; version(with_eventloop) { import arsd.eventloop; try send(SignalFired()); catch(Exception) {} } } extern(C) void interruptSignalHandler(int sigNumber) nothrow { interrupted = true; version(with_eventloop) { import arsd.eventloop; try send(SignalFired()); catch(Exception) {} } } extern(C) void hangupSignalHandler(int sigNumber) nothrow { hangedUp = true; version(with_eventloop) { import arsd.eventloop; try send(SignalFired()); catch(Exception) {} } } } // parts of this were taken from Robik's ConsoleD // https://github.com/robik/ConsoleD/blob/master/consoled.d // Uncomment this line to get a main() to demonstrate this module's // capabilities. //version = Demo version(Windows) { import core.sys.windows.windows; import std.string : toStringz; private { enum RED_BIT = 4; enum GREEN_BIT = 2; enum BLUE_BIT = 1; } } version(Posix) { import core.sys.posix.termios; import core.sys.posix.unistd; import unix = core.sys.posix.unistd; import core.sys.posix.sys.types; import core.sys.posix.sys.time; import core.stdc.stdio; private { enum RED_BIT = 1; enum GREEN_BIT = 2; enum BLUE_BIT = 4; } version(linux) { extern(C) int ioctl(int, int, ...); enum int TIOCGWINSZ = 0x5413; } else version(OSX) { import core.stdc.config; extern(C) int ioctl(int, c_ulong, ...); enum TIOCGWINSZ = 1074295912; } else static assert(0, "confirm the value of tiocgwinsz"); struct winsize { ushort ws_row; ushort ws_col; ushort ws_xpixel; ushort ws_ypixel; } // I'm taking this from the minimal termcap from my Slackware box (which I use as my /etc/termcap) and just taking the most commonly used ones (for me anyway). // this way we'll have some definitions for 99% of typical PC cases even without any help from the local operating system enum string builtinTermcap = ` # Generic VT entry. vg|vt-generic|Generic VT entries:\ :bs:mi:ms:pt:xn:xo:it#8:\ :RA=\E[?7l:SA=\E?7h:\ :bl=^G:cr=^M:ta=^I:\ :cm=\E[%i%d;%dH:\ :le=^H:up=\E[A:do=\E[B:nd=\E[C:\ :LE=\E[%dD:RI=\E[%dC:UP=\E[%dA:DO=\E[%dB:\ :ho=\E[H:cl=\E[H\E[2J:ce=\E[K:cb=\E[1K:cd=\E[J:sf=\ED:sr=\EM:\ :ct=\E[3g:st=\EH:\ :cs=\E[%i%d;%dr:sc=\E7:rc=\E8:\ :ei=\E[4l:ic=\E[@:IC=\E[%d@:al=\E[L:AL=\E[%dL:\ :dc=\E[P:DC=\E[%dP:dl=\E[M:DL=\E[%dM:\ :so=\E[7m:se=\E[m:us=\E[4m:ue=\E[m:\ :mb=\E[5m:mh=\E[2m:md=\E[1m:mr=\E[7m:me=\E[m:\ :sc=\E7:rc=\E8:kb=\177:\ :ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D: # Slackware 3.1 linux termcap entry (Sat Apr 27 23:03:58 CDT 1996): lx|linux|console|con80x25|LINUX System Console:\ :do=^J:co#80:li#25:cl=\E[H\E[J:sf=\ED:sb=\EM:\ :le=^H:bs:am:cm=\E[%i%d;%dH:nd=\E[C:up=\E[A:\ :ce=\E[K:cd=\E[J:so=\E[7m:se=\E[27m:us=\E[36m:ue=\E[m:\ :md=\E[1m:mr=\E[7m:mb=\E[5m:me=\E[m:is=\E[1;25r\E[25;1H:\ :ll=\E[1;25r\E[25;1H:al=\E[L:dc=\E[P:dl=\E[M:\ :it#8:ku=\E[A:kd=\E[B:kr=\E[C:kl=\E[D:kb=^H:ti=\E[r\E[H:\ :ho=\E[H:kP=\E[5~:kN=\E[6~:kH=\E[4~:kh=\E[1~:kD=\E[3~:kI=\E[2~:\ :k1=\E[[A:k2=\E[[B:k3=\E[[C:k4=\E[[D:k5=\E[[E:k6=\E[17~:\ :F1=\E[23~:F2=\E[24~:\ :k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:K1=\E[1~:K2=\E[5~:\ :K4=\E[4~:K5=\E[6~:\ :pt:sr=\EM:vt#3:xn:km:bl=^G:vi=\E[?25l:ve=\E[?25h:vs=\E[?25h:\ :sc=\E7:rc=\E8:cs=\E[%i%d;%dr:\ :r1=\Ec:r2=\Ec:r3=\Ec: # Some other, commonly used linux console entries. lx|con80x28:co#80:li#28:tc=linux: lx|con80x43:co#80:li#43:tc=linux: lx|con80x50:co#80:li#50:tc=linux: lx|con100x37:co#100:li#37:tc=linux: lx|con100x40:co#100:li#40:tc=linux: lx|con132x43:co#132:li#43:tc=linux: # vt102 - vt100 + insert line etc. VT102 does not have insert character. v2|vt102|DEC vt102 compatible:\ :co#80:li#24:\ :ic@:IC@:\ :is=\E[m\E[?1l\E>:\ :rs=\E[m\E[?1l\E>:\ :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ :ks=:ke=:\ :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:\ :tc=vt-generic: # vt100 - really vt102 without insert line, insert char etc. vt|vt100|DEC vt100 compatible:\ :im@:mi@:al@:dl@:ic@:dc@:AL@:DL@:IC@:DC@:\ :tc=vt102: # Entry for an xterm. Insert mode has been disabled. vs|xterm|xterm-color|xterm-256color|vs100|xterm terminal emulator (X Window System):\ :am:bs:mi@:km:co#80:li#55:\ :im@:ei@:\ :cl=\E[H\E[J:\ :ct=\E[3k:ue=\E[m:\ :is=\E[m\E[?1l\E>:\ :rs=\E[m\E[?1l\E>:\ :vi=\E[?25l:ve=\E[?25h:\ :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\E[15~:\ :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ :F1=\E[23~:F2=\E[24~:\ :kh=\E[H:kH=\E[F:\ :ks=:ke=:\ :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ :tc=vt-generic: #rxvt, added by me rxvt|rxvt-unicode:\ :am:bs:mi@:km:co#80:li#55:\ :im@:ei@:\ :ct=\E[3k:ue=\E[m:\ :is=\E[m\E[?1l\E>:\ :rs=\E[m\E[?1l\E>:\ :vi=\E[?25l:\ :ve=\E[?25h:\ :eA=\E)0:as=^N:ae=^O:ac=aaffggjjkkllmmnnooqqssttuuvvwwxx:\ :kI=\E[2~:kD=\E[3~:kP=\E[5~:kN=\E[6~:\ :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:k0=\E[21~:\ :F1=\E[23~:F2=\E[24~:\ :kh=\E[7~:kH=\E[8~:\ :ks=:ke=:\ :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:\ :tc=vt-generic: # Some other entries for the same xterm. v2|xterms|vs100s|xterm small window:\ :co#80:li#24:tc=xterm: vb|xterm-bold|xterm with bold instead of underline:\ :us=\E[1m:tc=xterm: vi|xterm-ins|xterm with insert mode:\ :mi:im=\E[4h:ei=\E[4l:tc=xterm: Eterm|Eterm Terminal Emulator (X11 Window System):\ :am:bw:eo:km:mi:ms:xn:xo:\ :co#80:it#8:li#24:lm#0:pa#64:Co#8:AF=\E[3%dm:AB=\E[4%dm:op=\E[39m\E[49m:\ :AL=\E[%dL:DC=\E[%dP:DL=\E[%dM:DO=\E[%dB:IC=\E[%d@:\ :K1=\E[7~:K2=\EOu:K3=\E[5~:K4=\E[8~:K5=\E[6~:LE=\E[%dD:\ :RI=\E[%dC:UP=\E[%dA:ae=^O:al=\E[L:as=^N:bl=^G:cd=\E[J:\ :ce=\E[K:cl=\E[H\E[2J:cm=\E[%i%d;%dH:cr=^M:\ :cs=\E[%i%d;%dr:ct=\E[3g:dc=\E[P:dl=\E[M:do=\E[B:\ :ec=\E[%dX:ei=\E[4l:ho=\E[H:i1=\E[?47l\E>\E[?1l:ic=\E[@:\ :im=\E[4h:is=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l:\ :k1=\E[11~:k2=\E[12~:k3=\E[13~:k4=\E[14~:k5=\E[15~:\ :k6=\E[17~:k7=\E[18~:k8=\E[19~:k9=\E[20~:kD=\E[3~:\ :kI=\E[2~:kN=\E[6~:kP=\E[5~:kb=^H:kd=\E[B:ke=:kh=\E[7~:\ :kl=\E[D:kr=\E[C:ks=:ku=\E[A:le=^H:mb=\E[5m:md=\E[1m:\ :me=\E[m\017:mr=\E[7m:nd=\E[C:rc=\E8:\ :sc=\E7:se=\E[27m:sf=^J:so=\E[7m:sr=\EM:st=\EH:ta=^I:\ :te=\E[2J\E[?47l\E8:ti=\E7\E[?47h:ue=\E[24m:up=\E[A:\ :us=\E[4m:vb=\E[?5h\E[?5l:ve=\E[?25h:vi=\E[?25l:\ :ac=aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~: # DOS terminal emulator such as Telix or TeleMate. # This probably also works for the SCO console, though it's incomplete. an|ansi|ansi-bbs|ANSI terminals (emulators):\ :co#80:li#24:am:\ :is=:rs=\Ec:kb=^H:\ :as=\E[m:ae=:eA=:\ :ac=0\333+\257,\256.\031-\030a\261f\370g\361j\331k\277l\332m\300n\305q\304t\264u\303v\301w\302x\263~\025:\ :kD=\177:kH=\E[Y:kN=\E[U:kP=\E[V:kh=\E[H:\ :k1=\EOP:k2=\EOQ:k3=\EOR:k4=\EOS:k5=\EOT:\ :k6=\EOU:k7=\EOV:k8=\EOW:k9=\EOX:k0=\EOY:\ :tc=vt-generic: `; } enum Bright = 0x08; /// Defines the list of standard colors understood by Terminal. enum Color : ushort { black = 0, /// . red = RED_BIT, /// . green = GREEN_BIT, /// . yellow = red | green, /// . blue = BLUE_BIT, /// . magenta = red | blue, /// . cyan = blue | green, /// . white = red | green | blue, /// . DEFAULT = 256, } /// When capturing input, what events are you interested in? /// /// Note: these flags can be OR'd together to select more than one option at a time. /// /// Ctrl+C and other keyboard input is always captured, though it may be line buffered if you don't use raw. /// The rationale for that is to ensure the Terminal destructor has a chance to run, since the terminal is a shared resource and should be put back before the program terminates. enum ConsoleInputFlags { raw = 0, /// raw input returns keystrokes immediately, without line buffering echo = 1, /// do you want to automatically echo input back to the user? mouse = 2, /// capture mouse events paste = 4, /// capture paste events (note: without this, paste can come through as keystrokes) size = 8, /// window resize events releasedKeys = 64, /// key release events. Not reliable on Posix. allInputEvents = 8|4|2, /// subscribe to all input events. Note: in previous versions, this also returned release events. It no longer does, use allInputEventsWithRelease if you want them. allInputEventsWithRelease = allInputEvents|releasedKeys, /// subscribe to all input events, including (unreliable on Posix) key release events. } /// Defines how terminal output should be handled. enum ConsoleOutputType { linear = 0, /// do you want output to work one line at a time? cellular = 1, /// or do you want access to the terminal screen as a grid of characters? //truncatedCellular = 3, /// cellular, but instead of wrapping output to the next line automatically, it will truncate at the edges minimalProcessing = 255, /// do the least possible work, skips most construction and desturction tasks. Only use if you know what you're doing here } /// Some methods will try not to send unnecessary commands to the screen. You can override their judgement using a ForceOption parameter, if present enum ForceOption { automatic = 0, /// automatically decide what to do (best, unless you know for sure it isn't right) neverSend = -1, /// never send the data. This will only update Terminal's internal state. Use with caution. alwaysSend = 1, /// always send the data, even if it doesn't seem necessary } // we could do it with termcap too, getenv("TERMCAP") then split on : and replace \E with \033 and get the pieces /// Encapsulates the I/O capabilities of a terminal. /// /// Warning: do not write out escape sequences to the terminal. This won't work /// on Windows and will confuse Terminal's internal state on Posix. struct Terminal { /// @disable this(); @disable this(this); private ConsoleOutputType type; version(Posix) { private int fdOut; private int fdIn; private int[] delegate() getSizeOverride; void delegate(in void[]) _writeDelegate; // used to override the unix write() system call, set it magically } version(Posix) { bool terminalInFamily(string[] terms...) { import std.process; import std.string; auto term = environment.get("TERM"); foreach(t; terms) if(indexOf(term, t) != -1) return true; return false; } // This is a filthy hack because Terminal.app and OS X are garbage who don't // work the way they're advertised. I just have to best-guess hack and hope it // doesn't break anything else. (If you know a better way, let me know!) bool isMacTerminal() { import std.process; import std.string; auto term = environment.get("TERM"); return term == "xterm-256color"; } static string[string] termcapDatabase; static void readTermcapFile(bool useBuiltinTermcap = false) { import std.file; import std.stdio; import std.string; if(!exists("/etc/termcap")) useBuiltinTermcap = true; string current; void commitCurrentEntry() { if(current is null) return; string names = current; auto idx = indexOf(names, ":"); if(idx != -1) names = names[0 .. idx]; foreach(name; split(names, "|")) termcapDatabase[name] = current; current = null; } void handleTermcapLine(in char[] line) { if(line.length == 0) { // blank commitCurrentEntry(); return; // continue } if(line[0] == '#') // comment return; // continue size_t termination = line.length; if(line[$-1] == '\\') termination--; // cut off the \\ current ~= strip(line[0 .. termination]); // termcap entries must be on one logical line, so if it isn't continued, we know we're done if(line[$-1] != '\\') commitCurrentEntry(); } if(useBuiltinTermcap) { foreach(line; splitLines(builtinTermcap)) { handleTermcapLine(line); } } else { foreach(line; File("/etc/termcap").byLine()) { handleTermcapLine(line); } } } static string getTermcapDatabase(string terminal) { import std.string; if(termcapDatabase is null) readTermcapFile(); auto data = terminal in termcapDatabase; if(data is null) return null; auto tc = *data; auto more = indexOf(tc, ":tc="); if(more != -1) { auto tcKey = tc[more + ":tc=".length .. $]; auto end = indexOf(tcKey, ":"); if(end != -1) tcKey = tcKey[0 .. end]; tc = getTermcapDatabase(tcKey) ~ tc; } return tc; } string[string] termcap; void readTermcap() { import std.process; import std.string; import std.array; string termcapData = environment.get("TERMCAP"); if(termcapData.length == 0) { termcapData = getTermcapDatabase(environment.get("TERM")); } auto e = replace(termcapData, "\\\n", "\n"); termcap = null; foreach(part; split(e, ":")) { // FIXME: handle numeric things too auto things = split(part, "="); if(things.length) termcap[things[0]] = things.length > 1 ? things[1] : null; } } string findSequenceInTermcap(in char[] sequenceIn) { char[10] sequenceBuffer; char[] sequence; if(sequenceIn.length > 0 && sequenceIn[0] == '\033') { if(!(sequenceIn.length < sequenceBuffer.length - 1)) return null; sequenceBuffer[1 .. sequenceIn.length + 1] = sequenceIn[]; sequenceBuffer[0] = '\\'; sequenceBuffer[1] = 'E'; sequence = sequenceBuffer[0 .. sequenceIn.length + 1]; } else { sequence = sequenceBuffer[1 .. sequenceIn.length + 1]; } import std.array; foreach(k, v; termcap) if(v == sequence) return k; return null; } string getTermcap(string key) { auto k = key in termcap; if(k !is null) return *k; return null; } // Looks up a termcap item and tries to execute it. Returns false on failure bool doTermcap(T...)(string key, T t) { import std.conv; auto fs = getTermcap(key); if(fs is null) return false; int swapNextTwo = 0; R getArg(R)(int idx) { if(swapNextTwo == 2) { idx ++; swapNextTwo--; } else if(swapNextTwo == 1) { idx --; swapNextTwo--; } foreach(i, arg; t) { if(i == idx) return to!R(arg); } assert(0, to!string(idx) ~ " is out of bounds working " ~ fs); } char[256] buffer; int bufferPos = 0; void addChar(char c) { import std.exception; enforce(bufferPos < buffer.length); buffer[bufferPos++] = c; } void addString(in char[] c) { import std.exception; enforce(bufferPos + c.length < buffer.length); buffer[bufferPos .. bufferPos + c.length] = c[]; bufferPos += c.length; } void addInt(int c, int minSize) { import std.string; auto str = format("%0"~(minSize ? to!string(minSize) : "")~"d", c); addString(str); } bool inPercent; int argPosition = 0; int incrementParams = 0; bool skipNext; bool nextIsChar; bool inBackslash; foreach(char c; fs) { if(inBackslash) { if(c == 'E') addChar('\033'); else addChar(c); inBackslash = false; } else if(nextIsChar) { if(skipNext) skipNext = false; else addChar(cast(char) (c + getArg!int(argPosition) + (incrementParams ? 1 : 0))); if(incrementParams) incrementParams--; argPosition++; inPercent = false; } else if(inPercent) { switch(c) { case '%': addChar('%'); inPercent = false; break; case '2': case '3': case 'd': if(skipNext) skipNext = false; else addInt(getArg!int(argPosition) + (incrementParams ? 1 : 0), c == 'd' ? 0 : (c - '0') ); if(incrementParams) incrementParams--; argPosition++; inPercent = false; break; case '.': if(skipNext) skipNext = false; else addChar(cast(char) (getArg!int(argPosition) + (incrementParams ? 1 : 0))); if(incrementParams) incrementParams--; argPosition++; break; case '+': nextIsChar = true; inPercent = false; break; case 'i': incrementParams = 2; inPercent = false; break; case 's': skipNext = true; inPercent = false; break; case 'b': argPosition--; inPercent = false; break; case 'r': swapNextTwo = 2; inPercent = false; break; // FIXME: there's more // http://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html default: assert(0, "not supported " ~ c); } } else { if(c == '%') inPercent = true; else if(c == '\\') inBackslash = true; else addChar(c); } } writeStringRaw(buffer[0 .. bufferPos]); return true; } } version(Posix) /** * Constructs an instance of Terminal representing the capabilities of * the current terminal. * * While it is possible to override the stdin+stdout file descriptors, remember * that is not portable across platforms and be sure you know what you're doing. * * ditto on getSizeOverride. That's there so you can do something instead of ioctl. */ this(ConsoleOutputType type, int fdIn = 0, int fdOut = 1, int[] delegate() getSizeOverride = null) { this.fdIn = fdIn; this.fdOut = fdOut; this.getSizeOverride = getSizeOverride; this.type = type; readTermcap(); if(type == ConsoleOutputType.minimalProcessing) { _suppressDestruction = true; return; } if(type == ConsoleOutputType.cellular) { doTermcap("ti"); clear(); moveTo(0, 0, ForceOption.alwaysSend); // we need to know where the cursor is for some features to work, and moving it is easier than querying it } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[22;0t"); // save window title on a stack (support seems spotty, but it doesn't hurt to have it) } } version(Windows) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO originalSbi; } version(Windows) /// ditto this(ConsoleOutputType type) { if(type == ConsoleOutputType.cellular) { hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, null, CONSOLE_TEXTMODE_BUFFER, null); if(hConsole == INVALID_HANDLE_VALUE) { import std.conv; throw new Exception(to!string(GetLastError())); } SetConsoleActiveScreenBuffer(hConsole); /* http://msdn.microsoft.com/en-us/library/windows/desktop/ms686125%28v=vs.85%29.aspx http://msdn.microsoft.com/en-us/library/windows/desktop/ms683193%28v=vs.85%29.aspx */ COORD size; /* CONSOLE_SCREEN_BUFFER_INFO sbi; GetConsoleScreenBufferInfo(hConsole, &sbi); size.X = cast(short) GetSystemMetrics(SM_CXMIN); size.Y = cast(short) GetSystemMetrics(SM_CYMIN); */ // FIXME: this sucks, maybe i should just revert it. but there shouldn't be scrollbars in cellular mode //size.X = 80; //size.Y = 24; //SetConsoleScreenBufferSize(hConsole, size); clear(); } else { hConsole = GetStdHandle(STD_OUTPUT_HANDLE); } GetConsoleScreenBufferInfo(hConsole, &originalSbi); } // only use this if you are sure you know what you want, since the terminal is a shared resource you generally really want to reset it to normal when you leave... bool _suppressDestruction; version(Posix) ~this() { if(_suppressDestruction) { flush(); return; } if(type == ConsoleOutputType.cellular) { doTermcap("te"); } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[23;0t"); // restore window title from the stack } showCursor(); reset(); flush(); if(lineGetter !is null) lineGetter.dispose(); } version(Windows) ~this() { flush(); // make sure user data is all flushed before resetting reset(); showCursor(); if(lineGetter !is null) lineGetter.dispose(); auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleActiveScreenBuffer(stdo); if(hConsole !is stdo) CloseHandle(hConsole); } // lazily initialized and preserved between calls to getline for a bit of efficiency (only a bit) // and some history storage. LineGetter lineGetter; int _currentForeground = Color.DEFAULT; int _currentBackground = Color.DEFAULT; RGB _currentForegroundRGB; RGB _currentBackgroundRGB; bool reverseVideo = false; /++ Attempts to set color according to a 24 bit value (r, g, b, each >= 0 and < 256). This is not supported on all terminals. It will attempt to fall back to a 256-color or 8-color palette in those cases automatically. Returns: true if it believes it was successful (note that it cannot be completely sure), false if it had to use a fallback. +/ bool setTrueColor(RGB foreground, RGB background, ForceOption force = ForceOption.automatic) { if(force == ForceOption.neverSend) { _currentForeground = -1; _currentBackground = -1; _currentForegroundRGB = foreground; _currentBackgroundRGB = background; return true; } if(force == ForceOption.automatic && _currentForeground == -1 && _currentBackground == -1 && (_currentForegroundRGB == foreground && _currentBackgroundRGB == background)) return true; _currentForeground = -1; _currentBackground = -1; _currentForegroundRGB = foreground; _currentBackgroundRGB = background; version(Windows) { flush(); ushort setTob = cast(ushort) approximate16Color(background); ushort setTof = cast(ushort) approximate16Color(foreground); SetConsoleTextAttribute( hConsole, cast(ushort)((setTob << 4) | setTof)); return false; } else { // FIXME: if the terminal reliably does support 24 bit color, use it // instead of the round off. But idk how to detect that yet... // fallback to 16 color for term that i know don't take it well import std.process; import std.string; if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { // not likely supported, use 16 color fallback auto setTof = approximate16Color(foreground); auto setTob = approximate16Color(background); writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm", (setTof & Bright) ? 1 : 0, cast(int) (setTof & ~Bright), cast(int) (setTob & ~Bright) )); return false; } // otherwise, assume it is probably supported and give it a try writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", colorToXTermPaletteIndex(foreground), colorToXTermPaletteIndex(background) )); return true; } } /// Changes the current color. See enum Color for the values. void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { if(force != ForceOption.neverSend) { version(Windows) { // assuming a dark background on windows, so LowContrast == dark which means the bit is NOT set on hardware /* foreground ^= LowContrast; background ^= LowContrast; */ ushort setTof = cast(ushort) foreground; ushort setTob = cast(ushort) background; // this isn't necessarily right but meh if(background == Color.DEFAULT) setTob = Color.black; if(foreground == Color.DEFAULT) setTof = Color.white; if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { flush(); // if we don't do this now, the buffering can screw up the colors... if(reverseVideo) { if(background == Color.DEFAULT) setTof = Color.black; else setTof = cast(ushort) background | (foreground & Bright); if(background == Color.DEFAULT) setTob = Color.white; else setTob = cast(ushort) (foreground & ~Bright); } SetConsoleTextAttribute( hConsole, cast(ushort)((setTob << 4) | setTof)); } } else { import std.process; // I started using this envvar for my text editor, but now use it elsewhere too // if we aren't set to dark, assume light /* if(getenv("ELVISBG") == "dark") { // LowContrast on dark bg menas } else { foreground ^= LowContrast; background ^= LowContrast; } */ ushort setTof = cast(ushort) foreground & ~Bright; ushort setTob = cast(ushort) background & ~Bright; if(foreground & Color.DEFAULT) setTof = 9; // ansi sequence for reset if(background == Color.DEFAULT) setTob = 9; import std.string; if(force == ForceOption.alwaysSend || reverseVideo != this.reverseVideo || foreground != _currentForeground || background != _currentBackground) { writeStringRaw(format("\033[%dm\033[3%dm\033[4%dm\033[%dm", (foreground != Color.DEFAULT && (foreground & Bright)) ? 1 : 0, cast(int) setTof, cast(int) setTob, reverseVideo ? 7 : 27 )); } } } _currentForeground = foreground; _currentBackground = background; this.reverseVideo = reverseVideo; } private bool _underlined = false; /// Note: the Windows console does not support underlining void underline(bool set, ForceOption force = ForceOption.automatic) { if(set == _underlined && force != ForceOption.alwaysSend) return; version(Posix) { if(set) writeStringRaw("\033[4m"); else writeStringRaw("\033[24m"); } _underlined = set; } // FIXME: do I want to do bold and italic? /// Returns the terminal to normal output colors void reset() { version(Windows) SetConsoleTextAttribute( hConsole, originalSbi.wAttributes); else writeStringRaw("\033[0m"); _underlined = false; _currentForeground = Color.DEFAULT; _currentBackground = Color.DEFAULT; reverseVideo = false; } // FIXME: add moveRelative /// The current x position of the output cursor. 0 == leftmost column @property int cursorX() { return _cursorX; } /// The current y position of the output cursor. 0 == topmost row @property int cursorY() { return _cursorY; } private int _cursorX; private int _cursorY; /// Moves the output cursor to the given position. (0, 0) is the upper left corner of the screen. The force parameter can be used to force an update, even if Terminal doesn't think it is necessary void moveTo(int x, int y, ForceOption force = ForceOption.automatic) { if(force != ForceOption.neverSend && (force == ForceOption.alwaysSend || x != _cursorX || y != _cursorY)) { executeAutoHideCursor(); version(Posix) { doTermcap("cm", y, x); } else version(Windows) { flush(); // if we don't do this now, the buffering can screw up the position COORD coord = {cast(short) x, cast(short) y}; SetConsoleCursorPosition(hConsole, coord); } else static assert(0); } _cursorX = x; _cursorY = y; } /// shows the cursor void showCursor() { version(Posix) doTermcap("ve"); else { CONSOLE_CURSOR_INFO info; GetConsoleCursorInfo(hConsole, &info); info.bVisible = true; SetConsoleCursorInfo(hConsole, &info); } } /// hides the cursor void hideCursor() { version(Posix) { doTermcap("vi"); } else { CONSOLE_CURSOR_INFO info; GetConsoleCursorInfo(hConsole, &info); info.bVisible = false; SetConsoleCursorInfo(hConsole, &info); } } private bool autoHidingCursor; private bool autoHiddenCursor; // explicitly not publicly documented // Sets the cursor to automatically insert a hide command at the front of the output buffer iff it is moved. // Call autoShowCursor when you are done with the batch update. void autoHideCursor() { autoHidingCursor = true; } private void executeAutoHideCursor() { if(autoHidingCursor) { version(Windows) hideCursor(); else version(Posix) { // prepend the hide cursor command so it is the first thing flushed writeBuffer = "\033[?25l" ~ writeBuffer; } autoHiddenCursor = true; autoHidingCursor = false; // already been done, don't insert the command again } } // explicitly not publicly documented // Shows the cursor if it was automatically hidden by autoHideCursor and resets the internal auto hide state. void autoShowCursor() { if(autoHiddenCursor) showCursor(); autoHidingCursor = false; autoHiddenCursor = false; } /* // alas this doesn't work due to a bunch of delegate context pointer and postblit problems // instead of using: auto input = terminal.captureInput(flags) // use: auto input = RealTimeConsoleInput(&terminal, flags); /// Gets real time input, disabling line buffering RealTimeConsoleInput captureInput(ConsoleInputFlags flags) { return RealTimeConsoleInput(&this, flags); } */ /// Changes the terminal's title void setTitle(string t) { version(Windows) { SetConsoleTitleA(toStringz(t)); } else { import std.string; if(terminalInFamily("xterm", "rxvt", "screen")) writeStringRaw(format("\033]0;%s\007", t)); } } /// Flushes your updates to the terminal. /// It is important to call this when you are finished writing for now if you are using the version=with_eventloop void flush() { if(writeBuffer.length == 0) return; version(Posix) { if(_writeDelegate !is null) { _writeDelegate(writeBuffer); } else { ssize_t written; while(writeBuffer.length) { written = unix.write(this.fdOut, writeBuffer.ptr, writeBuffer.length); if(written < 0) throw new Exception("write failed for some reason"); writeBuffer = writeBuffer[written .. $]; } } } else version(Windows) { import std.conv; // FIXME: I'm not sure I'm actually happy with this allocation but // it probably isn't a big deal. At least it has unicode support now. wstring writeBufferw = to!wstring(writeBuffer); while(writeBufferw.length) { DWORD written; WriteConsoleW(hConsole, writeBufferw.ptr, cast(DWORD)writeBufferw.length, &written, null); writeBufferw = writeBufferw[written .. $]; } writeBuffer = null; } } int[] getSize() { version(Windows) { CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo( hConsole, &info ); int cols, rows; cols = (info.srWindow.Right - info.srWindow.Left + 1); rows = (info.srWindow.Bottom - info.srWindow.Top + 1); return [cols, rows]; } else { if(getSizeOverride is null) { winsize w; ioctl(0, TIOCGWINSZ, &w); return [w.ws_col, w.ws_row]; } else return getSizeOverride(); } } void updateSize() { auto size = getSize(); _width = size[0]; _height = size[1]; } private int _width; private int _height; /// The current width of the terminal (the number of columns) @property int width() { if(_width == 0 || _height == 0) updateSize(); return _width; } /// The current height of the terminal (the number of rows) @property int height() { if(_width == 0 || _height == 0) updateSize(); return _height; } /* void write(T...)(T t) { foreach(arg; t) { writeStringRaw(to!string(arg)); } } */ /// Writes to the terminal at the current cursor position. void writef(T...)(string f, T t) { import std.string; writePrintableString(format(f, t)); } /// ditto void writefln(T...)(string f, T t) { writef(f ~ "\n", t); } /// ditto void write(T...)(T t) { import std.conv; string data; foreach(arg; t) { data ~= to!string(arg); } writePrintableString(data); } /// ditto void writeln(T...)(T t) { write(t, "\n"); } /+ /// A combined moveTo and writef that puts the cursor back where it was before when it finishes the write. /// Only works in cellular mode. /// Might give better performance than moveTo/writef because if the data to write matches the internal buffer, it skips sending anything (to override the buffer check, you can use moveTo and writePrintableString with ForceOption.alwaysSend) void writefAt(T...)(int x, int y, string f, T t) { import std.string; auto toWrite = format(f, t); auto oldX = _cursorX; auto oldY = _cursorY; writeAtWithoutReturn(x, y, toWrite); moveTo(oldX, oldY); } void writeAtWithoutReturn(int x, int y, in char[] data) { moveTo(x, y); writeStringRaw(toWrite, ForceOption.alwaysSend); } +/ void writePrintableString(in char[] s, ForceOption force = ForceOption.automatic) { // an escape character is going to mess things up. Actually any non-printable character could, but meh // assert(s.indexOf("\033") == -1); // tracking cursor position foreach(ch; s) { switch(ch) { case '\n': _cursorX = 0; _cursorY++; break; case '\r': _cursorX = 0; break; case '\t': _cursorX ++; _cursorX += _cursorX % 8; // FIXME: get the actual tabstop, if possible break; default: if(ch <= 127) // way of only advancing once per dchar instead of per code unit _cursorX++; } if(_wrapAround && _cursorX > width) { _cursorX = 0; _cursorY++; } if(_cursorY == height) _cursorY--; /+ auto index = getIndex(_cursorX, _cursorY); if(data[index] != ch) { data[index] = ch; } +/ } writeStringRaw(s); } /* private */ bool _wrapAround = true; deprecated alias writePrintableString writeString; /// use write() or writePrintableString instead private string writeBuffer; // you really, really shouldn't use this unless you know what you are doing /*private*/ void writeStringRaw(in char[] s) { // FIXME: make sure all the data is sent, check for errors version(Posix) { writeBuffer ~= s; // buffer it to do everything at once in flush() calls } else version(Windows) { writeBuffer ~= s; } else static assert(0); } /// Clears the screen. void clear() { version(Posix) { doTermcap("cl"); } else version(Windows) { // http://support.microsoft.com/kb/99261 flush(); DWORD c; CONSOLE_SCREEN_BUFFER_INFO csbi; DWORD conSize; GetConsoleScreenBufferInfo(hConsole, &csbi); conSize = csbi.dwSize.X * csbi.dwSize.Y; COORD coordScreen; FillConsoleOutputCharacterA(hConsole, ' ', conSize, coordScreen, &c); FillConsoleOutputAttribute(hConsole, csbi.wAttributes, conSize, coordScreen, &c); moveTo(0, 0, ForceOption.alwaysSend); } _cursorX = 0; _cursorY = 0; } /// gets a line, including user editing. Convenience method around the LineGetter class and RealTimeConsoleInput facilities - use them if you need more control. /// You really shouldn't call this if stdin isn't actually a user-interactive terminal! So if you expect people to pipe data to your app, check for that or use something else. // FIXME: add a method to make it easy to check if stdin is actually a tty and use other methods there. string getline(string prompt = null) { if(lineGetter is null) lineGetter = new LineGetter(&this); // since the struct might move (it shouldn't, this should be unmovable!) but since // it technically might, I'm updating the pointer before using it just in case. lineGetter.terminal = &this; if(prompt !is null) lineGetter.prompt = prompt; auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw); auto line = lineGetter.getline(&input); // lineGetter leaves us exactly where it was when the user hit enter, giving best // flexibility to real-time input and cellular programs. The convenience function, // however, wants to do what is right in most the simple cases, which is to actually // print the line (echo would be enabled without RealTimeConsoleInput anyway and they // did hit enter), so we'll do that here too. writePrintableString("\n"); return line; } } /+ struct ConsoleBuffer { int cursorX; int cursorY; int width; int height; dchar[] data; void actualize(Terminal* t) { auto writer = t.getBufferedWriter(); this.copyTo(&(t.onScreen)); } void copyTo(ConsoleBuffer* buffer) { buffer.cursorX = this.cursorX; buffer.cursorY = this.cursorY; buffer.width = this.width; buffer.height = this.height; buffer.data[] = this.data[]; } } +/ /** * Encapsulates the stream of input events received from the terminal input. */ struct RealTimeConsoleInput { @disable this(); @disable this(this); version(Posix) { private int fdOut; private int fdIn; private sigaction_t oldSigWinch; private sigaction_t oldSigIntr; private sigaction_t oldHupIntr; private termios old; ubyte[128] hack; // apparently termios isn't the size druntime thinks it is (at least on 32 bit, sometimes).... // tcgetattr smashed other variables in here too that could create random problems // so this hack is just to give some room for that to happen without destroying the rest of the world } version(Windows) { private DWORD oldInput; private DWORD oldOutput; HANDLE inputHandle; } private ConsoleInputFlags flags; private Terminal* terminal; private void delegate()[] destructor; /// To capture input, you need to provide a terminal and some flags. public this(Terminal* terminal, ConsoleInputFlags flags) { this.flags = flags; this.terminal = terminal; version(Windows) { inputHandle = GetStdHandle(STD_INPUT_HANDLE); GetConsoleMode(inputHandle, &oldInput); DWORD mode = 0; mode |= ENABLE_PROCESSED_INPUT /* 0x01 */; // this gives Ctrl+C which we probably want to be similar to linux //if(flags & ConsoleInputFlags.size) mode |= ENABLE_WINDOW_INPUT /* 0208 */; // gives size etc if(flags & ConsoleInputFlags.echo) mode |= ENABLE_ECHO_INPUT; // 0x4 if(flags & ConsoleInputFlags.mouse) mode |= ENABLE_MOUSE_INPUT; // 0x10 // if(flags & ConsoleInputFlags.raw) // FIXME: maybe that should be a separate flag for ENABLE_LINE_INPUT SetConsoleMode(inputHandle, mode); destructor ~= { SetConsoleMode(inputHandle, oldInput); }; GetConsoleMode(terminal.hConsole, &oldOutput); mode = 0; // we want this to match linux too mode |= ENABLE_PROCESSED_OUTPUT; /* 0x01 */ mode |= ENABLE_WRAP_AT_EOL_OUTPUT; /* 0x02 */ SetConsoleMode(terminal.hConsole, mode); destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; // FIXME: change to UTF8 as well } version(Posix) { this.fdIn = terminal.fdIn; this.fdOut = terminal.fdOut; if(fdIn != -1) { tcgetattr(fdIn, &old); auto n = old; auto f = ICANON; if(!(flags & ConsoleInputFlags.echo)) f |= ECHO; n.c_lflag &= ~f; tcsetattr(fdIn, TCSANOW, &n); } // some weird bug breaks this, https://github.com/robik/ConsoleD/issues/3 //destructor ~= { tcsetattr(fdIn, TCSANOW, &old); }; if(flags & ConsoleInputFlags.size) { import core.sys.posix.signal; sigaction_t n; n.sa_handler = &sizeSignalHandler; n.sa_mask = cast(sigset_t) 0; n.sa_flags = 0; sigaction(SIGWINCH, &n, &oldSigWinch); } { import core.sys.posix.signal; sigaction_t n; n.sa_handler = &interruptSignalHandler; n.sa_mask = cast(sigset_t) 0; n.sa_flags = 0; sigaction(SIGINT, &n, &oldSigIntr); } { import core.sys.posix.signal; sigaction_t n; n.sa_handler = &hangupSignalHandler; n.sa_mask = cast(sigset_t) 0; n.sa_flags = 0; sigaction(SIGHUP, &n, &oldHupIntr); } if(flags & ConsoleInputFlags.mouse) { // basic button press+release notification // FIXME: try to get maximum capabilities from all terminals // right now this works well on xterm but rxvt isn't sending movements... terminal.writeStringRaw("\033[?1000h"); destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; // the MOUSE_HACK env var is for the case where I run screen // but set TERM=xterm (which I do from putty). The 1003 mouse mode // doesn't work there, breaking mouse support entirely. So by setting // MOUSE_HACK=1002 it tells us to use the other mode for a fallback. import std.process : environment; if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { // this is vt200 mouse with full motion tracking, supported by xterm terminal.writeStringRaw("\033[?1003h"); destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; } else if(terminal.terminalInFamily("rxvt", "screen") || environment.get("MOUSE_HACK") == "1002") { terminal.writeStringRaw("\033[?1002h"); // this is vt200 mouse with press/release and motion notification iff buttons are pressed destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; } } if(flags & ConsoleInputFlags.paste) { if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { terminal.writeStringRaw("\033[?2004h"); // bracketed paste mode destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; } } // try to ensure the terminal is in UTF-8 mode if(terminal.terminalInFamily("xterm", "screen", "linux") && !terminal.isMacTerminal()) { terminal.writeStringRaw("\033%G"); } terminal.flush(); } version(with_eventloop) { import arsd.eventloop; version(Windows) auto listenTo = inputHandle; else version(Posix) auto listenTo = this.fdIn; else static assert(0, "idk about this OS"); version(Posix) addListener(&signalFired); if(listenTo != -1) { addFileEventListeners(listenTo, &eventListener, null, null); destructor ~= { removeFileEventListeners(listenTo); }; } addOnIdle(&terminal.flush); destructor ~= { removeOnIdle(&terminal.flush); }; } } version(with_eventloop) { version(Posix) void signalFired(SignalFired) { if(interrupted) { interrupted = false; send(InputEvent(UserInterruptionEvent(), terminal)); } if(windowSizeChanged) send(checkWindowSizeChanged()); if(hangedUp) { hangedUp = false; send(InputEvent(HangupEvent(), terminal)); } } import arsd.eventloop; void eventListener(OsFileHandle fd) { auto queue = readNextEvents(); foreach(event; queue) send(event); } } ~this() { // the delegate thing doesn't actually work for this... for some reason version(Posix) if(fdIn != -1) tcsetattr(fdIn, TCSANOW, &old); version(Posix) { if(flags & ConsoleInputFlags.size) { // restoration sigaction(SIGWINCH, &oldSigWinch, null); } sigaction(SIGINT, &oldSigIntr, null); sigaction(SIGHUP, &oldHupIntr, null); } // we're just undoing everything the constructor did, in reverse order, same criteria foreach_reverse(d; destructor) d(); } /** Returns true if there iff getch() would not block. WARNING: kbhit might consume input that would be ignored by getch. This function is really only meant to be used in conjunction with getch. Typically, you should use a full-fledged event loop if you want all kinds of input. kbhit+getch are just for simple keyboard driven applications. */ bool kbhit() { auto got = getch(true); if(got == dchar.init) return false; getchBuffer = got; return true; } /// Check for input, waiting no longer than the number of milliseconds bool timedCheckForInput(int milliseconds) { version(Windows) { auto response = WaitForSingleObject(terminal.hConsole, milliseconds); if(response == 0) return true; // the object is ready return false; } else version(Posix) { if(fdIn == -1) return false; timeval tv; tv.tv_sec = 0; tv.tv_usec = milliseconds * 1000; fd_set fs; FD_ZERO(&fs); FD_SET(fdIn, &fs); if(select(fdIn + 1, &fs, null, null, &tv) == -1) { return false; } return FD_ISSET(fdIn, &fs); } } /* private */ bool anyInput_internal() { if(inputQueue.length || timedCheckForInput(0)) return true; version(Posix) if(interrupted || windowSizeChanged || hangedUp) return true; return false; } private dchar getchBuffer; /// Get one key press from the terminal, discarding other /// events in the process. Returns dchar.init upon receiving end-of-file. /// /// Be aware that this may return non-character key events, like F1, F2, arrow keys, etc., as private use Unicode characters. Check them against KeyboardEvent.Key if you like. dchar getch(bool nonblocking = false) { if(getchBuffer != dchar.init) { auto a = getchBuffer; getchBuffer = dchar.init; return a; } if(nonblocking && !anyInput_internal()) return dchar.init; auto event = nextEvent(); while(event.type != InputEvent.Type.KeyboardEvent || event.keyboardEvent.pressed == false) { if(event.type == InputEvent.Type.UserInterruptionEvent) throw new UserInterruptionException(); if(event.type == InputEvent.Type.HangupEvent) throw new HangupException(); if(event.type == InputEvent.Type.EndOfFileEvent) return dchar.init; if(nonblocking && !anyInput_internal()) return dchar.init; event = nextEvent(); } return event.keyboardEvent.which; } //char[128] inputBuffer; //int inputBufferPosition; version(Posix) int nextRaw(bool interruptable = false) { if(fdIn == -1) return 0; char[1] buf; try_again: auto ret = read(fdIn, buf.ptr, buf.length); if(ret == 0) return 0; // input closed if(ret == -1) { import core.stdc.errno; if(errno == EINTR) // interrupted by signal call, quite possibly resize or ctrl+c which we want to check for in the event loop if(interruptable) return -1; else goto try_again; else throw new Exception("read failed"); } //terminal.writef("RAW READ: %d\n", buf[0]); if(ret == 1) return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; else assert(0); // read too much, should be impossible } version(Posix) int delegate(char) inputPrefilter; version(Posix) dchar nextChar(int starting) { if(starting <= 127) return cast(dchar) starting; char[6] buffer; int pos = 0; buffer[pos++] = cast(char) starting; // see the utf-8 encoding for details int remaining = 0; ubyte magic = starting & 0xff; while(magic & 0b1000_000) { remaining++; magic <<= 1; } while(remaining && pos < buffer.length) { buffer[pos++] = cast(char) nextRaw(); remaining--; } import std.utf; size_t throwAway; // it insists on the index but we don't care return decode(buffer[], throwAway); } InputEvent checkWindowSizeChanged() { auto oldWidth = terminal.width; auto oldHeight = terminal.height; terminal.updateSize(); version(Posix) windowSizeChanged = false; return InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); } // character event // non-character key event // paste event // mouse event // size event maybe, and if appropriate focus events /// Returns the next event. /// /// Experimental: It is also possible to integrate this into /// a generic event loop, currently under -version=with_eventloop and it will /// require the module arsd.eventloop (Linux only at this point) InputEvent nextEvent() { terminal.flush(); if(inputQueue.length) { auto e = inputQueue[0]; inputQueue = inputQueue[1 .. $]; return e; } wait_for_more: version(Posix) if(interrupted) { interrupted = false; return InputEvent(UserInterruptionEvent(), terminal); } version(Posix) if(hangedUp) { hangedUp = false; return InputEvent(HangupEvent(), terminal); } version(Posix) if(windowSizeChanged) { return checkWindowSizeChanged(); } auto more = readNextEvents(); if(!more.length) goto wait_for_more; // i used to do a loop (readNextEvents can read something, but it might be discarded by the input filter) but now it goto's above because readNextEvents might be interrupted by a SIGWINCH aka size event so we want to check that at least assert(more.length); auto e = more[0]; inputQueue = more[1 .. $]; return e; } InputEvent* peekNextEvent() { if(inputQueue.length) return &(inputQueue[0]); return null; } enum InjectionPosition { head, tail } void injectEvent(InputEvent ev, InjectionPosition where) { final switch(where) { case InjectionPosition.head: inputQueue = ev ~ inputQueue; break; case InjectionPosition.tail: inputQueue ~= ev; break; } } InputEvent[] inputQueue; version(Windows) InputEvent[] readNextEvents() { terminal.flush(); // make sure all output is sent out before waiting for anything INPUT_RECORD[32] buffer; DWORD actuallyRead; // FIXME: ReadConsoleInputW auto success = ReadConsoleInputA(inputHandle, buffer.ptr, buffer.length, &actuallyRead); if(success == 0) throw new Exception("ReadConsoleInput"); InputEvent[] newEvents; input_loop: foreach(record; buffer[0 .. actuallyRead]) { switch(record.EventType) { case KEY_EVENT: auto ev = record.KeyEvent; KeyboardEvent ke; CharacterEvent e; NonCharacterKeyEvent ne; e.eventType = ev.bKeyDown ? CharacterEvent.Type.Pressed : CharacterEvent.Type.Released; ne.eventType = ev.bKeyDown ? NonCharacterKeyEvent.Type.Pressed : NonCharacterKeyEvent.Type.Released; ke.pressed = ev.bKeyDown ? true : false; // only send released events when specifically requested if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) break; e.modifierState = ev.dwControlKeyState; ne.modifierState = ev.dwControlKeyState; ke.modifierState = ev.dwControlKeyState; if(ev.UnicodeChar) { // new style event goes first ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(ke, terminal); // old style event then follows as the fallback e.character = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(e, terminal); } else { // old style event ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; // new style event. See comment on KeyboardEvent.Key ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); // FIXME: make this better. the goal is to make sure the key code is a valid enum member // Windows sends more keys than Unix and we're doing lowest common denominator here foreach(member; __traits(allMembers, NonCharacterKeyEvent.Key)) if(__traits(getMember, NonCharacterKeyEvent.Key, member) == ne.key) { newEvents ~= InputEvent(ke, terminal); newEvents ~= InputEvent(ne, terminal); break; } } break; case MOUSE_EVENT: auto ev = record.MouseEvent; MouseEvent e; e.modifierState = ev.dwControlKeyState; e.x = ev.dwMousePosition.X; e.y = ev.dwMousePosition.Y; switch(ev.dwEventFlags) { case 0: //press or release e.eventType = MouseEvent.Type.Pressed; static DWORD lastButtonState; auto lastButtonState2 = lastButtonState; e.buttons = ev.dwButtonState; lastButtonState = e.buttons; // this is sent on state change. if fewer buttons are pressed, it must mean released if(cast(DWORD) e.buttons < lastButtonState2) { e.eventType = MouseEvent.Type.Released; // if last was 101 and now it is 100, then button far right was released // so we flip the bits, ~100 == 011, then and them: 101 & 011 == 001, the // button that was released e.buttons = lastButtonState2 & ~e.buttons; } break; case MOUSE_MOVED: e.eventType = MouseEvent.Type.Moved; e.buttons = ev.dwButtonState; break; case 0x0004/*MOUSE_WHEELED*/: e.eventType = MouseEvent.Type.Pressed; if(ev.dwButtonState > 0) e.buttons = MouseEvent.Button.ScrollDown; else e.buttons = MouseEvent.Button.ScrollUp; break; default: continue input_loop; } newEvents ~= InputEvent(e, terminal); break; case WINDOW_BUFFER_SIZE_EVENT: auto ev = record.WindowBufferSizeEvent; auto oldWidth = terminal.width; auto oldHeight = terminal.height; terminal._width = ev.dwSize.X; terminal._height = ev.dwSize.Y; newEvents ~= InputEvent(SizeChangedEvent(oldWidth, oldHeight, terminal.width, terminal.height), terminal); break; // FIXME: can we catch ctrl+c here too? default: // ignore } } return newEvents; } version(Posix) InputEvent[] readNextEvents() { terminal.flush(); // make sure all output is sent out before we try to get input // we want to starve the read, especially if we're called from an edge-triggered // epoll (which might happen in version=with_eventloop.. impl detail there subject // to change). auto initial = readNextEventsHelper(); // lol this calls select() inside a function prolly called from epoll but meh, // it is the simplest thing that can possibly work. The alternative would be // doing non-blocking reads and buffering in the nextRaw function (not a bad idea // btw, just a bit more of a hassle). while(timedCheckForInput(0)) { auto ne = readNextEventsHelper(); initial ~= ne; foreach(n; ne) if(n.type == InputEvent.Type.EndOfFileEvent) return initial; // hit end of file, get out of here lest we infinite loop // (select still returns info available even after we read end of file) } return initial; } // The helper reads just one actual event from the pipe... version(Posix) InputEvent[] readNextEventsHelper() { InputEvent[] charPressAndRelease(dchar character) { if((flags & ConsoleInputFlags.releasedKeys)) return [ // new style event InputEvent(KeyboardEvent(true, character, 0), terminal), InputEvent(KeyboardEvent(false, character, 0), terminal), // old style event InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal), ]; else return [ // new style event InputEvent(KeyboardEvent(true, character, 0), terminal), // old style event InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal) ]; } InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { if((flags & ConsoleInputFlags.releasedKeys)) return [ // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), // old style event InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), ]; else return [ // new style event FIXME: when the old events are removed, kill the +0xF0000 from here! InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), // old style event InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) ]; } char[30] sequenceBuffer; // this assumes you just read "\033[" char[] readEscapeSequence(char[] sequence) { int sequenceLength = 2; sequence[0] = '\033'; sequence[1] = '['; while(sequenceLength < sequence.length) { auto n = nextRaw(); sequence[sequenceLength++] = cast(char) n; // I think a [ is supposed to termiate a CSI sequence // but the Linux console sends CSI[A for F1, so I'm // hacking it to accept that too if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) break; } return sequence[0 .. sequenceLength]; } InputEvent[] translateTermcapName(string cap) { switch(cap) { //case "k0": //return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); case "k1": return keyPressAndRelease(NonCharacterKeyEvent.Key.F1); case "k2": return keyPressAndRelease(NonCharacterKeyEvent.Key.F2); case "k3": return keyPressAndRelease(NonCharacterKeyEvent.Key.F3); case "k4": return keyPressAndRelease(NonCharacterKeyEvent.Key.F4); case "k5": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5); case "k6": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6); case "k7": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7); case "k8": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8); case "k9": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9); case "k;": case "k0": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10); case "F1": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11); case "F2": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12); case "kb": return charPressAndRelease('\b'); case "kD": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete); case "kd": case "do": return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow); case "ku": case "up": return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow); case "kl": return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow); case "kr": case "nd": return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow); case "kN": case "K5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown); case "kP": case "K2": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp); case "ho": // this might not be a key but my thing sometimes returns it... weird... case "kh": case "K1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); case "kH": return keyPressAndRelease(NonCharacterKeyEvent.Key.End); case "kI": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); default: // don't know it, just ignore //import std.stdio; //writeln(cap); } return null; } InputEvent[] doEscapeSequence(in char[] sequence) { switch(sequence) { case "\033[200~": // bracketed paste begin // we want to keep reading until // "\033[201~": // and build a paste event out of it string data; for(;;) { auto n = nextRaw(); if(n == '\033') { n = nextRaw(); if(n == '[') { auto esc = readEscapeSequence(sequenceBuffer); if(esc == "\033[201~") { // complete! break; } else { // was something else apparently, but it is pasted, so keep it data ~= esc; } } else { data ~= '\033'; data ~= cast(char) n; } } else { data ~= cast(char) n; } } return [InputEvent(PasteEvent(data), terminal)]; case "\033[M": // mouse event auto buttonCode = nextRaw() - 32; // nextChar is commented because i'm not using UTF-8 mouse mode // cuz i don't think it is as widely supported auto x = cast(int) (/*nextChar*/(nextRaw())) - 33; /* they encode value + 32, but make upper left 1,1. I want it to be 0,0 */ auto y = cast(int) (/*nextChar*/(nextRaw())) - 33; /* ditto */ bool isRelease = (buttonCode & 0b11) == 3; int buttonNumber; if(!isRelease) { buttonNumber = (buttonCode & 0b11); if(buttonCode & 64) buttonNumber += 3; // button 4 and 5 are sent as like button 1 and 2, but code | 64 // so button 1 == button 4 here // note: buttonNumber == 0 means button 1 at this point buttonNumber++; // hence this // apparently this considers middle to be button 2. but i want middle to be button 3. if(buttonNumber == 2) buttonNumber = 3; else if(buttonNumber == 3) buttonNumber = 2; } auto modifiers = buttonCode & (0b0001_1100); // 4 == shift // 8 == meta // 16 == control MouseEvent m; if(buttonCode & 32) m.eventType = MouseEvent.Type.Moved; else m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; // ugh, if no buttons are pressed, released and moved are indistinguishable... // so we'll count the buttons down, and if we get a release static int buttonsDown = 0; if(!isRelease && buttonNumber <= 3) // exclude wheel "presses"... buttonsDown++; if(isRelease && m.eventType != MouseEvent.Type.Moved) { if(buttonsDown) buttonsDown--; else // no buttons down, so this should be a motion instead.. m.eventType = MouseEvent.Type.Moved; } if(buttonNumber == 0) m.buttons = 0; // we don't actually know :( else m.buttons = 1 << (buttonNumber - 1); // I prefer flags so that's how we do it m.x = x; m.y = y; m.modifierState = modifiers; return [InputEvent(m, terminal)]; default: // look it up in the termcap key database auto cap = terminal.findSequenceInTermcap(sequence); if(cap !is null) { return translateTermcapName(cap); } else { if(terminal.terminalInFamily("xterm")) { import std.conv, std.string; auto terminator = sequence[$ - 1]; auto parts = sequence[2 .. $ - 1].split(";"); // parts[0] and terminator tells us the key // parts[1] tells us the modifierState uint modifierState; int modGot; if(parts.length > 1) modGot = to!int(parts[1]); mod_switch: switch(modGot) { case 2: modifierState |= ModifierState.shift; break; case 3: modifierState |= ModifierState.alt; break; case 4: modifierState |= ModifierState.shift | ModifierState.alt; break; case 5: modifierState |= ModifierState.control; break; case 6: modifierState |= ModifierState.shift | ModifierState.control; break; case 7: modifierState |= ModifierState.alt | ModifierState.control; break; case 8: modifierState |= ModifierState.shift | ModifierState.alt | ModifierState.control; break; case 9: .. case 16: modifierState |= ModifierState.meta; if(modGot != 9) { modGot -= 8; goto mod_switch; } break; // this is an extension in my own terminal emulator case 20: .. case 36: modifierState |= ModifierState.windows; modGot -= 20; goto mod_switch; default: } switch(terminator) { case 'A': return keyPressAndRelease(NonCharacterKeyEvent.Key.UpArrow, modifierState); case 'B': return keyPressAndRelease(NonCharacterKeyEvent.Key.DownArrow, modifierState); case 'C': return keyPressAndRelease(NonCharacterKeyEvent.Key.RightArrow, modifierState); case 'D': return keyPressAndRelease(NonCharacterKeyEvent.Key.LeftArrow, modifierState); case 'H': return keyPressAndRelease(NonCharacterKeyEvent.Key.Home, modifierState); case 'F': return keyPressAndRelease(NonCharacterKeyEvent.Key.End, modifierState); case 'P': return keyPressAndRelease(NonCharacterKeyEvent.Key.F1, modifierState); case 'Q': return keyPressAndRelease(NonCharacterKeyEvent.Key.F2, modifierState); case 'R': return keyPressAndRelease(NonCharacterKeyEvent.Key.F3, modifierState); case 'S': return keyPressAndRelease(NonCharacterKeyEvent.Key.F4, modifierState); case '~': // others switch(parts[0]) { case "5": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageUp, modifierState); case "6": return keyPressAndRelease(NonCharacterKeyEvent.Key.PageDown, modifierState); case "2": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert, modifierState); case "3": return keyPressAndRelease(NonCharacterKeyEvent.Key.Delete, modifierState); case "15": return keyPressAndRelease(NonCharacterKeyEvent.Key.F5, modifierState); case "17": return keyPressAndRelease(NonCharacterKeyEvent.Key.F6, modifierState); case "18": return keyPressAndRelease(NonCharacterKeyEvent.Key.F7, modifierState); case "19": return keyPressAndRelease(NonCharacterKeyEvent.Key.F8, modifierState); case "20": return keyPressAndRelease(NonCharacterKeyEvent.Key.F9, modifierState); case "21": return keyPressAndRelease(NonCharacterKeyEvent.Key.F10, modifierState); case "23": return keyPressAndRelease(NonCharacterKeyEvent.Key.F11, modifierState); case "24": return keyPressAndRelease(NonCharacterKeyEvent.Key.F12, modifierState); default: } break; default: } } else if(terminal.terminalInFamily("rxvt")) { // FIXME: figure these out. rxvt seems to just change the terminator while keeping the rest the same // though it isn't consistent. ugh. } else { // maybe we could do more terminals, but linux doesn't even send it and screen just seems to pass through, so i don't think so; xterm prolly covers most them anyway // so this space is semi-intentionally left blank } } } return null; } auto c = nextRaw(true); if(c == -1) return null; // interrupted; give back nothing so the other level can recheck signal flags if(c == 0) return [InputEvent(EndOfFileEvent(), terminal)]; if(c == '\033') { if(timedCheckForInput(50)) { // escape sequence c = nextRaw(); if(c == '[') { // CSI, ends on anything >= 'A' return doEscapeSequence(readEscapeSequence(sequenceBuffer)); } else if(c == 'O') { // could be xterm function key auto n = nextRaw(); char[3] thing; thing[0] = '\033'; thing[1] = 'O'; thing[2] = cast(char) n; auto cap = terminal.findSequenceInTermcap(thing); if(cap is null) { return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease('O') ~ charPressAndRelease(thing[2]); } else { return translateTermcapName(cap); } } else { // I don't know, probably unsupported terminal or just quick user input or something return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease(nextChar(c)); } } else { // user hit escape (or super slow escape sequence, but meh) return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); } } else { // FIXME: what if it is neither? we should check the termcap auto next = nextChar(c); if(next == 127) // some terminals send 127 on the backspace. Let's normalize that. next = '\b'; return charPressAndRelease(next); } } } /// The new style of keyboard event struct KeyboardEvent { bool pressed; /// dchar which; /// uint modifierState; /// /// bool isCharacter() { return !(which >= Key.min && which <= Key.max); } // these match Windows virtual key codes numerically for simplicity of translation there // but are plus a unicode private use area offset so i can cram them in the dchar // http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx /// . enum Key : dchar { escape = 0x1b + 0xF0000, /// . F1 = 0x70 + 0xF0000, /// . F2 = 0x71 + 0xF0000, /// . F3 = 0x72 + 0xF0000, /// . F4 = 0x73 + 0xF0000, /// . F5 = 0x74 + 0xF0000, /// . F6 = 0x75 + 0xF0000, /// . F7 = 0x76 + 0xF0000, /// . F8 = 0x77 + 0xF0000, /// . F9 = 0x78 + 0xF0000, /// . F10 = 0x79 + 0xF0000, /// . F11 = 0x7A + 0xF0000, /// . F12 = 0x7B + 0xF0000, /// . LeftArrow = 0x25 + 0xF0000, /// . RightArrow = 0x27 + 0xF0000, /// . UpArrow = 0x26 + 0xF0000, /// . DownArrow = 0x28 + 0xF0000, /// . Insert = 0x2d + 0xF0000, /// . Delete = 0x2e + 0xF0000, /// . Home = 0x24 + 0xF0000, /// . End = 0x23 + 0xF0000, /// . PageUp = 0x21 + 0xF0000, /// . PageDown = 0x22 + 0xF0000, /// . } } /// Deprecated: use KeyboardEvent instead in new programs /// Input event for characters struct CharacterEvent { /// . enum Type { Released, /// . Pressed /// . } Type eventType; /// . dchar character; /// . uint modifierState; /// Don't depend on this to be available for character events } /// Deprecated: use KeyboardEvent instead in new programs struct NonCharacterKeyEvent { /// . enum Type { Released, /// . Pressed /// . } Type eventType; /// . // these match Windows virtual key codes numerically for simplicity of translation there //http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731%28v=vs.85%29.aspx /// . enum Key : int { escape = 0x1b, /// . F1 = 0x70, /// . F2 = 0x71, /// . F3 = 0x72, /// . F4 = 0x73, /// . F5 = 0x74, /// . F6 = 0x75, /// . F7 = 0x76, /// . F8 = 0x77, /// . F9 = 0x78, /// . F10 = 0x79, /// . F11 = 0x7A, /// . F12 = 0x7B, /// . LeftArrow = 0x25, /// . RightArrow = 0x27, /// . UpArrow = 0x26, /// . DownArrow = 0x28, /// . Insert = 0x2d, /// . Delete = 0x2e, /// . Home = 0x24, /// . End = 0x23, /// . PageUp = 0x21, /// . PageDown = 0x22, /// . } Key key; /// . uint modifierState; /// A mask of ModifierState. Always use by checking modifierState & ModifierState.something, the actual value differs across platforms } /// . struct PasteEvent { string pastedText; /// . } /// . struct MouseEvent { // these match simpledisplay.d numerically as well /// . enum Type { Moved = 0, /// . Pressed = 1, /// . Released = 2, /// . Clicked, /// . } Type eventType; /// . // note: these should numerically match simpledisplay.d for maximum beauty in my other code /// . enum Button : uint { None = 0, /// . Left = 1, /// . Middle = 4, /// . Right = 2, /// . ScrollUp = 8, /// . ScrollDown = 16 /// . } uint buttons; /// A mask of Button int x; /// 0 == left side int y; /// 0 == top uint modifierState; /// shift, ctrl, alt, meta, altgr. Not always available. Always check by using modifierState & ModifierState.something } /// When you get this, check terminal.width and terminal.height to see the new size and react accordingly. struct SizeChangedEvent { int oldWidth; int oldHeight; int newWidth; int newHeight; } /// the user hitting ctrl+c will send this /// You should drop what you're doing and perhaps exit when this happens. struct UserInterruptionEvent {} /// If the user hangs up (for example, closes the terminal emulator without exiting the app), this is sent. /// If you receive it, you should generally cleanly exit. struct HangupEvent {} /// Sent upon receiving end-of-file from stdin. struct EndOfFileEvent {} interface CustomEvent {} version(Windows) enum ModifierState : uint { shift = 0x10, control = 0x8 | 0x4, // 8 == left ctrl, 4 == right ctrl // i'm not sure if the next two are available alt = 2 | 1, //2 ==left alt, 1 == right alt // FIXME: I don't think these are actually available windows = 512, meta = 4096, // FIXME sanity // I don't think this is available on Linux.... scrollLock = 0x40, } else enum ModifierState : uint { shift = 4, alt = 2, control = 16, meta = 8, windows = 512 // only available if you are using my terminal emulator; it isn't actually offered on standard linux ones } version(DDoc) /// enum ModifierState : uint { /// shift = 4, /// alt = 2, /// control = 16, } /++ [RealTimeConsoleInput.nextEvent] returns one of these. Check the type, then use the [InputEvent.get|get] method to get the more detailed information about the event. ++/ struct InputEvent { /// . enum Type { KeyboardEvent, /// Keyboard key pressed (or released, where supported) CharacterEvent, /// Do not use this in new programs, use KeyboardEvent instead NonCharacterKeyEvent, /// Do not use this in new programs, use KeyboardEvent instead PasteEvent, /// The user pasted some text. Not always available, the pasted text might come as a series of character events instead. MouseEvent, /// only sent if you subscribed to mouse events SizeChangedEvent, /// only sent if you subscribed to size events UserInterruptionEvent, /// the user hit ctrl+c EndOfFileEvent, /// stdin has received an end of file HangupEvent, /// the terminal hanged up - for example, if the user closed a terminal emulator CustomEvent /// . } /// . @property Type type() { return t; } /// Returns a pointer to the terminal associated with this event. /// (You can usually just ignore this as there's only one terminal typically.) /// /// It may be null in the case of program-generated events; @property Terminal* terminal() { return term; } /++ Gets the specific event instance. First, check the type (such as in a `switch` statement), then extract the correct one from here. Note that the template argument is a $(B value type of the enum above), not a type argument. So to use it, do $(D event.get!(InputEvent.Type.KeyboardEvent)), for example. See_Also: The event types: [KeyboardEvent], [MouseEvent], [SizeChangedEvent], [PasteEvent], [UserInterruptionEvent], [EndOfFileEvent], [HangupEvent], [CustomEvent] And associated functions: [RealTimeConsoleInput], [ConsoleInputFlags] ++/ @property auto get(Type T)() { if(type != T) throw new Exception("Wrong event type"); static if(T == Type.CharacterEvent) return characterEvent; else static if(T == Type.KeyboardEvent) return keyboardEvent; else static if(T == Type.NonCharacterKeyEvent) return nonCharacterKeyEvent; else static if(T == Type.PasteEvent) return pasteEvent; else static if(T == Type.MouseEvent) return mouseEvent; else static if(T == Type.SizeChangedEvent) return sizeChangedEvent; else static if(T == Type.UserInterruptionEvent) return userInterruptionEvent; else static if(T == Type.EndOfFileEvent) return endOfFileEvent; else static if(T == Type.HangupEvent) return hangupEvent; else static if(T == Type.CustomEvent) return customEvent; else static assert(0, "Type " ~ T.stringof ~ " not added to the get function"); } /// custom event is public because otherwise there's no point at all this(CustomEvent c, Terminal* p = null) { t = Type.CustomEvent; customEvent = c; } private { this(CharacterEvent c, Terminal* p) { t = Type.CharacterEvent; characterEvent = c; } this(KeyboardEvent c, Terminal* p) { t = Type.KeyboardEvent; keyboardEvent = c; } this(NonCharacterKeyEvent c, Terminal* p) { t = Type.NonCharacterKeyEvent; nonCharacterKeyEvent = c; } this(PasteEvent c, Terminal* p) { t = Type.PasteEvent; pasteEvent = c; } this(MouseEvent c, Terminal* p) { t = Type.MouseEvent; mouseEvent = c; } this(SizeChangedEvent c, Terminal* p) { t = Type.SizeChangedEvent; sizeChangedEvent = c; } this(UserInterruptionEvent c, Terminal* p) { t = Type.UserInterruptionEvent; userInterruptionEvent = c; } this(HangupEvent c, Terminal* p) { t = Type.HangupEvent; hangupEvent = c; } this(EndOfFileEvent c, Terminal* p) { t = Type.EndOfFileEvent; endOfFileEvent = c; } Type t; Terminal* term; union { KeyboardEvent keyboardEvent; CharacterEvent characterEvent; NonCharacterKeyEvent nonCharacterKeyEvent; PasteEvent pasteEvent; MouseEvent mouseEvent; SizeChangedEvent sizeChangedEvent; UserInterruptionEvent userInterruptionEvent; HangupEvent hangupEvent; EndOfFileEvent endOfFileEvent; CustomEvent customEvent; } } } /** FIXME: support lines that wrap FIXME: better controls maybe FIXME: support multi-line "lines" and some form of line continuation, both from the user (if permitted) and from the application, so like the user hits "class foo { \n" and the app says "that line needs continuation" automatically. FIXME: fix lengths on prompt and suggestion A note on history: To save history, you must call LineGetter.dispose() when you're done with it. History will not be automatically saved without that call! The history saving and loading as a trivially encountered race condition: if you open two programs that use the same one at the same time, the one that closes second will overwrite any history changes the first closer saved. GNU Getline does this too... and it actually kinda drives me nuts. But I don't know what a good fix is except for doing a transactional commit straight to the file every time and that seems like hitting the disk way too often. We could also do like a history server like a database daemon that keeps the order correct but I don't actually like that either because I kinda like different bashes to have different history, I just don't like it all to get lost. Regardless though, this isn't even used in bash anyway, so I don't think I care enough to put that much effort into it. Just using separate files for separate tasks is good enough I think. */ class LineGetter { /* A note on the assumeSafeAppends in here: since these buffers are private, we can be pretty sure that stomping isn't an issue, so I'm using this liberally to keep the append/realloc code simple and hopefully reasonably fast. */ // saved to file string[] history; // not saved Terminal* terminal; string historyFilename; /// Make sure that the parent terminal struct remains in scope for the duration /// of LineGetter's lifetime, as it does hold on to and use the passed pointer /// throughout. /// /// historyFilename will load and save an input history log to a particular folder. /// Leaving it null will mean no file will be used and history will not be saved across sessions. this(Terminal* tty, string historyFilename = null) { this.terminal = tty; this.historyFilename = historyFilename; line.reserve(128); if(historyFilename.length) loadSettingsAndHistoryFromFile(); regularForeground = cast(Color) terminal._currentForeground; background = cast(Color) terminal._currentBackground; suggestionForeground = Color.blue; } /// Call this before letting LineGetter die so it can do any necessary /// cleanup and save the updated history to a file. void dispose() { if(historyFilename.length) saveSettingsAndHistoryToFile(); } /// Override this to change the directory where history files are stored /// /// Default is $HOME/.arsd-getline on linux and %APPDATA%/arsd-getline/ on Windows. /* virtual */ string historyFileDirectory() { version(Windows) { char[1024] path; // FIXME: this doesn't link because the crappy dmd lib doesn't have it if(0) { // SHGetFolderPathA(null, CSIDL_APPDATA, null, 0, path.ptr) >= 0) { import core.stdc.string; return cast(string) path[0 .. strlen(path.ptr)] ~ "\\arsd-getline"; } else { import std.process; return environment["APPDATA"] ~ "\\arsd-getline"; } } else version(Posix) { import std.process; return environment["HOME"] ~ "/.arsd-getline"; } } /// You can customize the colors here. You should set these after construction, but before /// calling startGettingLine or getline. Color suggestionForeground; Color regularForeground; /// . Color background; /// . //bool reverseVideo; /// Set this if you want a prompt to be drawn with the line. It does NOT support color in string. string prompt; /// Turn on auto suggest if you want a greyed thing of what tab /// would be able to fill in as you type. /// /// You might want to turn it off if generating a completion list is slow. bool autoSuggest = true; /// Override this if you don't want all lines added to the history. /// You can return null to not add it at all, or you can transform it. /* virtual */ string historyFilter(string candidate) { return candidate; } /// You may override this to do nothing /* virtual */ void saveSettingsAndHistoryToFile() { import std.file; if(!exists(historyFileDirectory)) mkdir(historyFileDirectory); auto fn = historyPath(); import std.stdio; auto file = File(fn, "wt"); foreach(item; history) file.writeln(item); } private string historyPath() { import std.path; auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ ".history"; return filename; } /// You may override this to do nothing /* virtual */ void loadSettingsAndHistoryFromFile() { import std.file; history = null; auto fn = historyPath(); if(exists(fn)) { import std.stdio; foreach(line; File(fn, "rt").byLine) history ~= line.idup; } } /** Override this to provide tab completion. You may use the candidate argument to filter the list, but you don't have to (LineGetter will do it for you on the values you return). Ideally, you wouldn't return more than about ten items since the list gets difficult to use if it is too long. Default is to provide recent command history as autocomplete. */ /* virtual */ protected string[] tabComplete(in dchar[] candidate) { return history.length > 20 ? history[0 .. 20] : history; } private string[] filterTabCompleteList(string[] list) { if(list.length == 0) return list; string[] f; f.reserve(list.length); foreach(item; list) { import std.algorithm; if(startsWith(item, line[0 .. cursorPosition])) f ~= item; } return f; } /// Override this to provide a custom display of the tab completion list protected void showTabCompleteList(string[] list) { if(list.length) { // FIXME: allow mouse clicking of an item, that would be cool // FIXME: scroll //if(terminal.type == ConsoleOutputType.linear) { terminal.writeln(); foreach(item; list) { terminal.color(suggestionForeground, background); import std.utf; auto idx = codeLength!char(line[0 .. cursorPosition]); terminal.write(" ", item[0 .. idx]); terminal.color(regularForeground, background); terminal.writeln(item[idx .. $]); } updateCursorPosition(); redraw(); //} } } /// One-call shop for the main workhorse /// If you already have a RealTimeConsoleInput ready to go, you /// should pass a pointer to yours here. Otherwise, LineGetter will /// make its own. public string getline(RealTimeConsoleInput* input = null) { startGettingLine(); if(input is null) { auto i = RealTimeConsoleInput(terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents); while(workOnLine(i.nextEvent())) {} } else while(workOnLine(input.nextEvent())) {} return finishGettingLine(); } private int currentHistoryViewPosition = 0; private dchar[] uncommittedHistoryCandidate; void loadFromHistory(int howFarBack) { if(howFarBack < 0) howFarBack = 0; if(howFarBack > history.length) // lol signed/unsigned comparison here means if i did this first, before howFarBack < 0, it would totally cycle around. howFarBack = cast(int) history.length; if(howFarBack == currentHistoryViewPosition) return; if(currentHistoryViewPosition == 0) { // save the current line so we can down arrow back to it later if(uncommittedHistoryCandidate.length < line.length) { uncommittedHistoryCandidate.length = line.length; } uncommittedHistoryCandidate[0 .. line.length] = line[]; uncommittedHistoryCandidate = uncommittedHistoryCandidate[0 .. line.length]; uncommittedHistoryCandidate.assumeSafeAppend(); } currentHistoryViewPosition = howFarBack; if(howFarBack == 0) { line.length = uncommittedHistoryCandidate.length; line.assumeSafeAppend(); line[] = uncommittedHistoryCandidate[]; } else { line = line[0 .. 0]; line.assumeSafeAppend(); foreach(dchar ch; history[$ - howFarBack]) line ~= ch; } cursorPosition = cast(int) line.length; scrollToEnd(); } bool insertMode = true; bool multiLineMode = false; private dchar[] line; private int cursorPosition = 0; private int horizontalScrollPosition = 0; private void scrollToEnd() { horizontalScrollPosition = (cast(int) line.length); horizontalScrollPosition -= availableLineLength(); if(horizontalScrollPosition < 0) horizontalScrollPosition = 0; } // used for redrawing the line in the right place // and detecting mouse events on our line. private int startOfLineX; private int startOfLineY; // private string[] cachedCompletionList; // FIXME // /// Note that this assumes the tab complete list won't change between actual // /// presses of tab by the user. If you pass it a list, it will use it, but // /// otherwise it will keep track of the last one to avoid calls to tabComplete. private string suggestion(string[] list = null) { import std.algorithm, std.utf; auto relevantLineSection = line[0 .. cursorPosition]; // FIXME: see about caching the list if we easily can if(list is null) list = filterTabCompleteList(tabComplete(relevantLineSection)); if(list.length) { string commonality = list[0]; foreach(item; list[1 .. $]) { commonality = commonPrefix(commonality, item); } if(commonality.length) { return commonality[codeLength!char(relevantLineSection) .. $]; } } return null; } /// Adds a character at the current position in the line. You can call this too if you hook events for hotkeys or something. /// You'll probably want to call redraw() after adding chars. void addChar(dchar ch) { assert(cursorPosition >= 0 && cursorPosition <= line.length); if(cursorPosition == line.length) line ~= ch; else { assert(line.length); if(insertMode) { line ~= ' '; for(int i = cast(int) line.length - 2; i >= cursorPosition; i --) line[i + 1] = line[i]; } line[cursorPosition] = ch; } cursorPosition++; if(cursorPosition >= horizontalScrollPosition + availableLineLength()) horizontalScrollPosition++; } /// . void addString(string s) { // FIXME: this could be more efficient // but does it matter? these lines aren't super long anyway. But then again a paste could be excessively long (prolly accidental, but still) foreach(dchar ch; s) addChar(ch); } /// Deletes the character at the current position in the line. /// You'll probably want to call redraw() after deleting chars. void deleteChar() { if(cursorPosition == line.length) return; for(int i = cursorPosition; i < line.length - 1; i++) line[i] = line[i + 1]; line = line[0 .. $-1]; line.assumeSafeAppend(); } /// void deleteToEndOfLine() { while(cursorPosition < line.length) deleteChar(); } int availableLineLength() { return terminal.width - startOfLineX - cast(int) prompt.length - 1; } private int lastDrawLength = 0; void redraw() { terminal.moveTo(startOfLineX, startOfLineY); auto lineLength = availableLineLength(); if(lineLength < 0) throw new Exception("too narrow terminal to draw"); terminal.write(prompt); auto towrite = line[horizontalScrollPosition .. $]; auto cursorPositionToDrawX = cursorPosition - horizontalScrollPosition; auto cursorPositionToDrawY = 0; if(towrite.length > lineLength) { towrite = towrite[0 .. lineLength]; } terminal.write(towrite); lineLength -= towrite.length; string suggestion; if(lineLength >= 0) { suggestion = ((cursorPosition == towrite.length) && autoSuggest) ? this.suggestion() : null; if(suggestion.length) { terminal.color(suggestionForeground, background); terminal.write(suggestion); terminal.color(regularForeground, background); } } // FIXME: graphemes and utf-8 on suggestion/prompt auto written = cast(int) (towrite.length + suggestion.length + prompt.length); if(written < lastDrawLength) foreach(i; written .. lastDrawLength) terminal.write(" "); lastDrawLength = written; terminal.moveTo(startOfLineX + cursorPositionToDrawX + cast(int) prompt.length, startOfLineY + cursorPositionToDrawY); } /// Starts getting a new line. Call workOnLine and finishGettingLine afterward. /// /// Make sure that you've flushed your input and output before calling this /// function or else you might lose events or get exceptions from this. void startGettingLine() { // reset from any previous call first cursorPosition = 0; horizontalScrollPosition = 0; justHitTab = false; currentHistoryViewPosition = 0; if(line.length) { line = line[0 .. 0]; line.assumeSafeAppend(); } updateCursorPosition(); terminal.showCursor(); lastDrawLength = availableLineLength(); redraw(); } private void updateCursorPosition() { terminal.flush(); // then get the current cursor position to start fresh version(Windows) { CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(terminal.hConsole, &info); startOfLineX = info.dwCursorPosition.X; startOfLineY = info.dwCursorPosition.Y; } else { // request current cursor position // we have to turn off cooked mode to get this answer, otherwise it will all // be messed up. (I hate unix terminals, the Windows way is so much easer.) // We also can't use RealTimeConsoleInput here because it also does event loop stuff // which would be broken by the child destructor :( (maybe that should be a FIXME) ubyte[128] hack2; termios old; ubyte[128] hack; tcgetattr(terminal.fdIn, &old); auto n = old; n.c_lflag &= ~(ICANON | ECHO); tcsetattr(terminal.fdIn, TCSANOW, &n); scope(exit) tcsetattr(terminal.fdIn, TCSANOW, &old); terminal.writeStringRaw("\033[6n"); terminal.flush(); import core.sys.posix.unistd; // reading directly to bypass any buffering ubyte[16] buffer; auto len = read(terminal.fdIn, buffer.ptr, buffer.length); if(len <= 0) throw new Exception("Couldn't get cursor position to initialize get line"); auto got = buffer[0 .. len]; if(got.length < 6) throw new Exception("not enough cursor reply answer"); if(got[0] != '\033' || got[1] != '[' || got[$-1] != 'R') throw new Exception("wrong answer for cursor position"); auto gots = cast(char[]) got[2 .. $-1]; import std.conv; import std.string; auto pieces = split(gots, ";"); if(pieces.length != 2) throw new Exception("wtf wrong answer on cursor position"); startOfLineX = to!int(pieces[1]) - 1; startOfLineY = to!int(pieces[0]) - 1; } // updating these too because I can with the more accurate info from above terminal._cursorX = startOfLineX; terminal._cursorY = startOfLineY; } private bool justHitTab; /// for integrating into another event loop /// you can pass individual events to this and /// the line getter will work on it /// /// returns false when there's nothing more to do bool workOnLine(InputEvent e) { switch(e.type) { case InputEvent.Type.EndOfFileEvent: justHitTab = false; // FIXME: this should be distinct from an empty line when hit at the beginning return false; //break; case InputEvent.Type.KeyboardEvent: auto ev = e.keyboardEvent; if(ev.pressed == false) return true; /* Insert the character (unless it is backspace, tab, or some other control char) */ auto ch = ev.which; switch(ch) { case 4: // ctrl+d will also send a newline-equivalent case '\r': case '\n': justHitTab = false; return false; case '\t': auto relevantLineSection = line[0 .. cursorPosition]; auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection)); import std.utf; if(possibilities.length == 1) { auto toFill = possibilities[0][codeLength!char(relevantLineSection) .. $]; if(toFill.length) { addString(toFill); redraw(); } justHitTab = false; } else { if(justHitTab) { justHitTab = false; showTabCompleteList(possibilities); } else { justHitTab = true; /* fill it in with as much commonality as there is amongst all the suggestions */ auto suggestion = this.suggestion(possibilities); if(suggestion.length) { addString(suggestion); redraw(); } } } break; case '\b': justHitTab = false; if(cursorPosition) { cursorPosition--; for(int i = cursorPosition; i < line.length - 1; i++) line[i] = line[i + 1]; line = line[0 .. $ - 1]; line.assumeSafeAppend(); if(!multiLineMode) { if(horizontalScrollPosition > cursorPosition - 1) horizontalScrollPosition = cursorPosition - 1 - availableLineLength(); if(horizontalScrollPosition < 0) horizontalScrollPosition = 0; } redraw(); } break; case KeyboardEvent.Key.LeftArrow: justHitTab = false; if(cursorPosition) cursorPosition--; if(!multiLineMode) { if(cursorPosition < horizontalScrollPosition) horizontalScrollPosition--; } redraw(); break; case KeyboardEvent.Key.RightArrow: justHitTab = false; if(cursorPosition < line.length) cursorPosition++; if(!multiLineMode) { if(cursorPosition >= horizontalScrollPosition + availableLineLength()) horizontalScrollPosition++; } redraw(); break; case KeyboardEvent.Key.UpArrow: justHitTab = false; loadFromHistory(currentHistoryViewPosition + 1); redraw(); break; case KeyboardEvent.Key.DownArrow: justHitTab = false; loadFromHistory(currentHistoryViewPosition - 1); redraw(); break; case KeyboardEvent.Key.PageUp: justHitTab = false; loadFromHistory(cast(int) history.length); redraw(); break; case KeyboardEvent.Key.PageDown: justHitTab = false; loadFromHistory(0); redraw(); break; case 1: // ctrl+a does home too in the emacs keybindings case KeyboardEvent.Key.Home: justHitTab = false; cursorPosition = 0; horizontalScrollPosition = 0; redraw(); break; case 5: // ctrl+e from emacs case KeyboardEvent.Key.End: justHitTab = false; cursorPosition = cast(int) line.length; scrollToEnd(); redraw(); break; case KeyboardEvent.Key.Insert: justHitTab = false; insertMode = !insertMode; // FIXME: indicate this on the UI somehow // like change the cursor or something break; case KeyboardEvent.Key.Delete: justHitTab = false; if(ev.modifierState & ModifierState.control) deleteToEndOfLine(); else deleteChar(); redraw(); break; case 11: // ctrl+k is delete to end of line from emacs justHitTab = false; deleteToEndOfLine(); redraw(); break; default: justHitTab = false; if(e.keyboardEvent.isCharacter) addChar(ch); redraw(); } break; case InputEvent.Type.PasteEvent: justHitTab = false; addString(e.pasteEvent.pastedText); redraw(); break; case InputEvent.Type.MouseEvent: /* Clicking with the mouse to move the cursor is so much easier than arrowing or even emacs/vi style movements much of the time, so I'ma support it. */ auto me = e.mouseEvent; if(me.eventType == MouseEvent.Type.Pressed) { if(me.buttons & MouseEvent.Button.Left) { if(me.y == startOfLineY) { // FIXME: prompt.length should be graphemes or at least code poitns int p = me.x - startOfLineX - cast(int) prompt.length + horizontalScrollPosition; if(p >= 0 && p < line.length) { justHitTab = false; cursorPosition = p; redraw(); } } } } break; case InputEvent.Type.SizeChangedEvent: /* We'll adjust the bounding box. If you don't like this, handle SizeChangedEvent yourself and then don't pass it to this function. */ // FIXME break; case InputEvent.Type.UserInterruptionEvent: /* I'll take this as canceling the line. */ throw new UserInterruptionException(); //break; case InputEvent.Type.HangupEvent: /* I'll take this as canceling the line. */ throw new HangupException(); //break; default: /* ignore. ideally it wouldn't be passed to us anyway! */ } return true; } string finishGettingLine() { import std.conv; auto f = to!string(line); auto history = historyFilter(f); if(history !is null) this.history ~= history; // FIXME: we should hide the cursor if it was hidden in the call to startGettingLine return f; } } /// Adds default constructors that just forward to the superclass mixin template LineGetterConstructors() { this(Terminal* tty, string historyFilename = null) { super(tty, historyFilename); } } /// This is a line getter that customizes the tab completion to /// fill in file names separated by spaces, like a command line thing. class FileLineGetter : LineGetter { mixin LineGetterConstructors; /// You can set this property to tell it where to search for the files /// to complete. string searchDirectory = "."; override protected string[] tabComplete(in dchar[] candidate) { import std.file, std.conv, std.algorithm, std.string; const(dchar)[] soFar = candidate; auto idx = candidate.lastIndexOf(" "); if(idx != -1) soFar = candidate[idx + 1 .. $]; string[] list; foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { // try without the ./ if(startsWith(name[2..$], soFar)) list ~= text(candidate, name[searchDirectory.length + 1 + soFar.length .. $]); else // and with if(startsWith(name, soFar)) list ~= text(candidate, name[soFar.length .. $]); } return list; } } version(Windows) { // to get the directory for saving history in the line things enum CSIDL_APPDATA = 26; extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); } /* Like getting a line, printing a lot of lines is kinda important too, so I'm including that widget here too. */ struct ScrollbackBuffer { bool demandsAttention; this(string name) { this.name = name; } void write(T...)(T t) { import std.conv : text; addComponent(text(t), foreground_, background_, null); } void writeln(T...)(T t) { write(t, "\n"); } void writef(T...)(string fmt, T t) { import std.format: format; write(format(fmt, t)); } void writefln(T...)(string fmt, T t) { writef(fmt, t, "\n"); } void clear() { lines = null; clickRegions = null; scrollbackPosition = 0; } int foreground_ = Color.DEFAULT, background_ = Color.DEFAULT; void color(int foreground, int background) { this.foreground_ = foreground; this.background_ = background; } void addComponent(string text, int foreground, int background, bool delegate() onclick) { if(lines.length == 0) { addLine(); } bool first = true; import std.algorithm; foreach(t; splitter(text, "\n")) { if(!first) addLine(); first = false; lines[$-1].components ~= LineComponent(t, foreground, background, onclick); } } void addLine() { lines ~= Line(); if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are scrollbackPosition++; } void addLine(string line) { lines ~= Line([LineComponent(line)]); if(scrollbackPosition) // if the user is scrolling back, we want to keep them basically centered where they are scrollbackPosition++; } void scrollUp(int lines = 1) { scrollbackPosition += lines; //if(scrollbackPosition >= this.lines.length) // scrollbackPosition = cast(int) this.lines.length - 1; } void scrollDown(int lines = 1) { scrollbackPosition -= lines; if(scrollbackPosition < 0) scrollbackPosition = 0; } void scrollToBottom() { scrollbackPosition = 0; } // this needs width and height to know how to word wrap it void scrollToTop(int width, int height) { scrollbackPosition = scrollTopPosition(width, height); } struct LineComponent { string text; bool isRgb; union { int color; RGB colorRgb; } union { int background; RGB backgroundRgb; } bool delegate() onclick; // return true if you need to redraw // 16 color ctor this(string text, int color = Color.DEFAULT, int background = Color.DEFAULT, bool delegate() onclick = null) { this.text = text; this.color = color; this.background = background; this.onclick = onclick; this.isRgb = false; } // true color ctor this(string text, RGB colorRgb, RGB backgroundRgb = RGB(0, 0, 0), bool delegate() onclick = null) { this.text = text; this.colorRgb = colorRgb; this.backgroundRgb = backgroundRgb; this.onclick = onclick; this.isRgb = true; } } struct Line { LineComponent[] components; int length() { int l = 0; foreach(c; components) l += c.text.length; return l; } } // FIXME: limit scrollback lines.length Line[] lines; string name; int x, y, width, height; int scrollbackPosition; int scrollTopPosition(int width, int height) { int lineCount; foreach_reverse(line; lines) { int written = 0; comp_loop: foreach(cidx, component; line.components) { auto towrite = component.text; foreach(idx, dchar ch; towrite) { if(written >= width) { lineCount++; written = 0; } if(ch == '\t') written += 8; // FIXME else written++; } } lineCount++; } //if(lineCount > height) return lineCount - height; //return 0; } void drawInto(Terminal* terminal, in int x = 0, in int y = 0, int width = 0, int height = 0) { if(lines.length == 0) return; if(width == 0) width = terminal.width; if(height == 0) height = terminal.height; this.x = x; this.y = y; this.width = width; this.height = height; /* We need to figure out how much is going to fit in a first pass, so we can figure out where to start drawing */ int remaining = height + scrollbackPosition; int start = cast(int) lines.length; int howMany = 0; bool firstPartial = false; static struct Idx { size_t cidx; size_t idx; } Idx firstPartialStartIndex; // this is private so I know we can safe append clickRegions.length = 0; clickRegions.assumeSafeAppend(); // FIXME: should prolly handle \n and \r in here too. // we'll work backwards to figure out how much will fit... // this will give accurate per-line things even with changing width and wrapping // while being generally efficient - we usually want to show the end of the list // anyway; actually using the scrollback is a bit of an exceptional case. // It could probably do this instead of on each redraw, on each resize or insertion. // or at least cache between redraws until one of those invalidates it. foreach_reverse(line; lines) { int written = 0; int brokenLineCount; Idx[16] lineBreaksBuffer; Idx[] lineBreaks = lineBreaksBuffer[]; comp_loop: foreach(cidx, component; line.components) { auto towrite = component.text; foreach(idx, dchar ch; towrite) { if(written >= width) { if(brokenLineCount == lineBreaks.length) lineBreaks ~= Idx(cidx, idx); else lineBreaks[brokenLineCount] = Idx(cidx, idx); brokenLineCount++; written = 0; } if(ch == '\t') written += 8; // FIXME else written++; } } lineBreaks = lineBreaks[0 .. brokenLineCount]; foreach_reverse(lineBreak; lineBreaks) { if(remaining == 1) { firstPartial = true; firstPartialStartIndex = lineBreak; break; } else { remaining--; } if(remaining <= 0) break; } remaining--; start--; howMany++; if(remaining <= 0) break; } // second pass: actually draw it int linePos = remaining; foreach(idx, line; lines[start .. start + howMany]) { int written = 0; if(linePos < 0) { linePos++; continue; } terminal.moveTo(x, y + ((linePos >= 0) ? linePos : 0)); auto todo = line.components; if(firstPartial) { todo = todo[firstPartialStartIndex.cidx .. $]; } foreach(ref component; todo) { if(component.isRgb) terminal.setTrueColor(component.colorRgb, component.backgroundRgb); else terminal.color(component.color, component.background); auto towrite = component.text; again: if(linePos >= height) break; if(firstPartial) { towrite = towrite[firstPartialStartIndex.idx .. $]; firstPartial = false; } foreach(chIdx, dchar ch; towrite) { if(written >= width) { clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); terminal.write(towrite[0 .. chIdx]); towrite = towrite[chIdx .. $]; linePos++; written = 0; terminal.moveTo(x, y + linePos); goto again; } if(ch == '\t') written += 8; // FIXME else written++; } if(towrite.length) { clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); terminal.write(towrite); } } if(written < width) { terminal.color(Color.DEFAULT, Color.DEFAULT); foreach(i; written .. width) terminal.write(" "); } linePos++; if(linePos >= height) break; } if(linePos < height) { terminal.color(Color.DEFAULT, Color.DEFAULT); foreach(i; linePos .. height) { if(i >= 0 && i < height) { terminal.moveTo(x, y + i); foreach(w; 0 .. width) terminal.write(" "); } } } } private struct ClickRegion { LineComponent* component; int xStart; int yStart; int length; } private ClickRegion[] clickRegions; /// Default event handling for this widget. Call this only after drawing it into a rectangle /// and only if the event ought to be dispatched to it (which you determine however you want; /// you could dispatch all events to it, or perhaps filter some out too) /// /// Returns true if it should be redrawn bool handleEvent(InputEvent e) { final switch(e.type) { case InputEvent.Type.KeyboardEvent: auto ev = e.keyboardEvent; demandsAttention = false; switch(ev.which) { case KeyboardEvent.Key.UpArrow: scrollUp(); return true; case KeyboardEvent.Key.DownArrow: scrollDown(); return true; case KeyboardEvent.Key.PageUp: scrollUp(height); return true; case KeyboardEvent.Key.PageDown: scrollDown(height); return true; default: // ignore } break; case InputEvent.Type.MouseEvent: auto ev = e.mouseEvent; if(ev.x >= x && ev.x < x + width && ev.y >= y && ev.y < y + height) { demandsAttention = false; // it is inside our box, so do something with it auto mx = ev.x - x; auto my = ev.y - y; if(ev.eventType == MouseEvent.Type.Pressed) { if(ev.buttons & MouseEvent.Button.Left) { foreach(region; clickRegions) if(ev.x >= region.xStart && ev.x < region.xStart + region.length && ev.y == region.yStart) if(region.component.onclick !is null) return region.component.onclick(); } if(ev.buttons & MouseEvent.Button.ScrollUp) { scrollUp(); return true; } if(ev.buttons & MouseEvent.Button.ScrollDown) { scrollDown(); return true; } } } else { // outside our area, free to ignore } break; case InputEvent.Type.SizeChangedEvent: // (size changed might be but it needs to be handled at a higher level really anyway) // though it will return true because it probably needs redrawing anyway. return true; case InputEvent.Type.UserInterruptionEvent: throw new UserInterruptionException(); case InputEvent.Type.HangupEvent: throw new HangupException(); case InputEvent.Type.EndOfFileEvent: // ignore, not relevant to this break; case InputEvent.Type.CharacterEvent: case InputEvent.Type.NonCharacterKeyEvent: // obsolete, ignore them until they are removed break; case InputEvent.Type.CustomEvent: case InputEvent.Type.PasteEvent: // ignored, not relevant to us break; } return false; } } class UserInterruptionException : Exception { this() { super("Ctrl+C"); } } class HangupException : Exception { this() { super("Hup"); } } /* // more efficient scrolling http://msdn.microsoft.com/en-us/library/windows/desktop/ms685113%28v=vs.85%29.aspx // and the unix sequences rxvt documentation: use this to finish the input magic for that For the keypad, use Shift to temporarily override Application-Keypad setting use Num_Lock to toggle Application-Keypad setting if Num_Lock is off, toggle Application-Keypad setting. Also note that values of Home, End, Delete may have been compiled differently on your system. Normal Shift Control Ctrl+Shift Tab ^I ESC [ Z ^I ESC [ Z BackSpace ^H ^? ^? ^? Find ESC [ 1 ~ ESC [ 1 $ ESC [ 1 ^ ESC [ 1 @ Insert ESC [ 2 ~ paste ESC [ 2 ^ ESC [ 2 @ Execute ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ Select ESC [ 4 ~ ESC [ 4 $ ESC [ 4 ^ ESC [ 4 @ Prior ESC [ 5 ~ scroll-up ESC [ 5 ^ ESC [ 5 @ Next ESC [ 6 ~ scroll-down ESC [ 6 ^ ESC [ 6 @ Home ESC [ 7 ~ ESC [ 7 $ ESC [ 7 ^ ESC [ 7 @ End ESC [ 8 ~ ESC [ 8 $ ESC [ 8 ^ ESC [ 8 @ Delete ESC [ 3 ~ ESC [ 3 $ ESC [ 3 ^ ESC [ 3 @ F1 ESC [ 11 ~ ESC [ 23 ~ ESC [ 11 ^ ESC [ 23 ^ F2 ESC [ 12 ~ ESC [ 24 ~ ESC [ 12 ^ ESC [ 24 ^ F3 ESC [ 13 ~ ESC [ 25 ~ ESC [ 13 ^ ESC [ 25 ^ F4 ESC [ 14 ~ ESC [ 26 ~ ESC [ 14 ^ ESC [ 26 ^ F5 ESC [ 15 ~ ESC [ 28 ~ ESC [ 15 ^ ESC [ 28 ^ F6 ESC [ 17 ~ ESC [ 29 ~ ESC [ 17 ^ ESC [ 29 ^ F7 ESC [ 18 ~ ESC [ 31 ~ ESC [ 18 ^ ESC [ 31 ^ F8 ESC [ 19 ~ ESC [ 32 ~ ESC [ 19 ^ ESC [ 32 ^ F9 ESC [ 20 ~ ESC [ 33 ~ ESC [ 20 ^ ESC [ 33 ^ F10 ESC [ 21 ~ ESC [ 34 ~ ESC [ 21 ^ ESC [ 34 ^ F11 ESC [ 23 ~ ESC [ 23 $ ESC [ 23 ^ ESC [ 23 @ F12 ESC [ 24 ~ ESC [ 24 $ ESC [ 24 ^ ESC [ 24 @ F13 ESC [ 25 ~ ESC [ 25 $ ESC [ 25 ^ ESC [ 25 @ F14 ESC [ 26 ~ ESC [ 26 $ ESC [ 26 ^ ESC [ 26 @ F15 (Help) ESC [ 28 ~ ESC [ 28 $ ESC [ 28 ^ ESC [ 28 @ F16 (Menu) ESC [ 29 ~ ESC [ 29 $ ESC [ 29 ^ ESC [ 29 @ F17 ESC [ 31 ~ ESC [ 31 $ ESC [ 31 ^ ESC [ 31 @ F18 ESC [ 32 ~ ESC [ 32 $ ESC [ 32 ^ ESC [ 32 @ F19 ESC [ 33 ~ ESC [ 33 $ ESC [ 33 ^ ESC [ 33 @ F20 ESC [ 34 ~ ESC [ 34 $ ESC [ 34 ^ ESC [ 34 @ Application Up ESC [ A ESC [ a ESC O a ESC O A Down ESC [ B ESC [ b ESC O b ESC O B Right ESC [ C ESC [ c ESC O c ESC O C Left ESC [ D ESC [ d ESC O d ESC O D KP_Enter ^M ESC O M KP_F1 ESC O P ESC O P KP_F2 ESC O Q ESC O Q KP_F3 ESC O R ESC O R KP_F4 ESC O S ESC O S XK_KP_Multiply * ESC O j XK_KP_Add + ESC O k XK_KP_Separator , ESC O l XK_KP_Subtract - ESC O m XK_KP_Decimal . ESC O n XK_KP_Divide / ESC O o XK_KP_0 0 ESC O p XK_KP_1 1 ESC O q XK_KP_2 2 ESC O r XK_KP_3 3 ESC O s XK_KP_4 4 ESC O t XK_KP_5 5 ESC O u XK_KP_6 6 ESC O v XK_KP_7 7 ESC O w XK_KP_8 8 ESC O x XK_KP_9 9 ESC O y */ /* The Xterm palette progression is: [0, 95, 135, 175, 215, 255] So if I take the color and subtract 55, then div 40, I get it into one of these areas. If I add 20, I get a reasonable rounding. */ ubyte colorToXTermPaletteIndex(RGB color) { /* Here, I will round off to the color ramp or the greyscale. I will NOT use the bottom 16 colors because there's duplicates (or very close enough) to them in here */ if(color.r == color.g && color.g == color.b) { // grey - find one of them: if(color.r == 0) return 0; // meh don't need those two, let's simplify branche //if(color.r == 0xc0) return 7; //if(color.r == 0x80) return 8; // it isn't == 255 because it wants to catch anything // that would wrap the simple algorithm below back to 0. if(color.r >= 248) return 15; // there's greys in the color ramp too, but these // are all close enough as-is, no need to complicate // algorithm for approximation anyway return cast(ubyte) (232 + ((color.r - 8) / 10)); } // if it isn't grey, it is color // the ramp goes blue, green, red, with 6 of each, // so just multiplying will give something good enough // will give something between 0 and 5, with some rounding auto r = (cast(int) color.r - 35) / 40; auto g = (cast(int) color.g - 35) / 40; auto b = (cast(int) color.b - 35) / 40; return cast(ubyte) (16 + b + g*6 + r*36); } /++ Represents a 24-bit color. $(TIP You can convert these to and from [arsd.color.Color] using `.tupleof`: --- RGB rgb; Color c = Color(rgb.tupleof); --- ) +/ struct RGB { ubyte r; /// ubyte g; /// ubyte b; /// // terminal can't actually use this but I want the value // there for assignment to an arsd.color.Color private ubyte a = 255; } // This is an approximation too for a few entries, but a very close one. RGB xtermPaletteIndexToColor(int paletteIdx) { RGB color; if(paletteIdx < 16) { if(paletteIdx == 7) return RGB(0xc0, 0xc0, 0xc0); else if(paletteIdx == 8) return RGB(0x80, 0x80, 0x80); color.r = (paletteIdx & 0b001) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; color.g = (paletteIdx & 0b010) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; color.b = (paletteIdx & 0b100) ? ((paletteIdx & 0b1000) ? 0xff : 0x80) : 0x00; } else if(paletteIdx < 232) { // color ramp, 6x6x6 cube color.r = cast(ubyte) ((paletteIdx - 16) / 36 * 40 + 55); color.g = cast(ubyte) (((paletteIdx - 16) % 36) / 6 * 40 + 55); color.b = cast(ubyte) ((paletteIdx - 16) % 6 * 40 + 55); if(color.r == 55) color.r = 0; if(color.g == 55) color.g = 0; if(color.b == 55) color.b = 0; } else { // greyscale ramp, from 0x8 to 0xee color.r = cast(ubyte) (8 + (paletteIdx - 232) * 10); color.g = color.r; color.b = color.g; } return color; } int approximate16Color(RGB color) { int c; c |= color.r > 64 ? RED_BIT : 0; c |= color.g > 64 ? GREEN_BIT : 0; c |= color.b > 64 ? BLUE_BIT : 0; c |= (((color.r + color.g + color.b) / 3) > 80) ? Bright : 0; return c; }