10203040506070809010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099010001010102010301040105010601070108010901100111011201130114011501160117011801190120012101220123012401250126012701280129013001310132013301340135013601370138013901400141014201430144014501460147014801490150015101520153015401550156015701580159016001610162016301640165016601670168016901700171017201730174017501760177017801790180018101820183018401850186018701880189019001910192019301940195019601970198019902000201020202030204020502060207020802090210021102120213021402150216021702180219022002210222022302240225022602270228022902300231023202330234023502360237023802390240024102420243024402450246024702480249025002510252025302540255025602570258025902600261026202630264026502660267026802690270027102720273027402750276027702780279028002810282028302840285028602870288028902900291029202930294029502960297029802990300030103020303030403050306030703080309031003110312031303140315031603170318031903200321132212323332403250326132703280329033003310332033303340335033603370338033903400341034203430344034513461347034813490350035133352153530354183551835618357173580359204360503610362183630364036503661433671536815369037012837110372118373118374100375118376037711837818379038003811382432383143384038503860387038803890390039103920393039403951396139703981399140014010402040304040405040604070408040904100411041204130414041504160417041804190420042114221423142404250426142714280429343004310432043304340435043604370438043904400441044204430444044504460447044804490450045104520453045404550456045704580459046004610462046304640465046604670468046904700471047204730474047504760477047804790480048104820483048404850486048704880489049004910492049304940495049604970498049905000501050205030504050505060507050805090510051105120513051405150516051705180519052005210522052305240525052605270528052905300531053205330534053505360537053805390540054105420543054405450546054705480549055005510552055305540555055605570558055905600561056205630564056505660567056805690570057105720573057405750576057705780579058005810582058305840585058605870588058905900591059205930594059505960597059805990600060106020603060406051606160716081609161006111612061316140615061606170618161906200621062206230624162506260627062806290630063106320633063406350636063706380639064006410642064306440645064606470648064906500651065206530654065506560657065806590660066106620663066406650666066706680669067006710672067306740675067606770678067906800681068206830684068506860687068806890690069106920693069406950696069706980699070007010702070307040705070607070708070907100711071207130714071507160717071807190720072107220723072407250726072707280729073007310732073307340735073607370738073907400741074207430744074507460747074807490750075107520753075407550756075707580759076007610762076307640765076607670768076961437700771077207730774077507760777077807790780078107820783078407850786078707880789079007910792079307940795079607970798079908000801080208030804080508066143807614380808096143810081161438126143813081408150816184298176143818122868190820082161438220823082408250826082761438286143829614383008310832083308340835083608370838083908400841084208430844084508460847084808490850085108520853085408556143856085761438586143859614386061438610862086308640865086608670868086908700871087218730874087508760877087808790880088108820883088408850886088708880889089008910892089308940895089608970898089909000901090209030904090509060907090809090910091109120913091409150916091709180919092009210922092309240925092609270928092909300931093209330934093509360937093809390940094109420943094409450946094709480949095009510952095309540955095609570958095909600961096209630964096509660967096809690970122869710972097309741228697509760977122869780979245729801228698112286982098312286984098509860987098809890990099109920993099409950996099709980999010000100101002010030100401005010060100701008010090101001011010120101315277810141527781015152778101615277810170101801019010200102101022152778102315277810241527781025010260102701028010290103001031010327638910337638910347638910350103601037010380103976389104076389104176389104201043010440104501046010470104801049010500105101052010530105401055010560105701058010590106061431061614310626143106301064010656143106601067010680106901070010710107201073010740107501076010770107801079010802475961081763891082137610831376108413761085137610860108701088010890109001091010920109375013109475013109572020109601097010981527781099720201100720201101011020110376389110401105011060110701108011096143111001111011120111301114011150111601117011180111901120011210112218429112301124011250112601127011280112901130011310113201133011340113501136011370113801139011400114101142011430114401145011460114701148011490115001151011520115301154011550115601157011580115901160011610116201163011640116501166011670116801169011700117101172011730117401175011760117701178011790118001181011820118301184011850118601187011880118901190011910119201193011940119501196011970119801199012000120101202012030120401205012060120701208012090121001211012120121301214012150121601217012180121901220012210122201223012240122501226012270122801229012300123101232012330123401235012360123701238012390124001241012420124301244012450124601247012480124901250012510125201253012540125501256012570125801259012600126101262012630126401265012660126701268012690127001271012720127301274012750127601277012780127901280012810128201283012840128501286012870128801289012900129101292012930129401295012960129701298012990130001301013020130301304013050130601307013080130901310013110131201313013140131501316013170131801319013200132101322013230132401325013260132701328013290133001331013320133301334013350133601337013380133901340013410134201343013440134501346013470134801349013500135101352013530135401355013560135701358013590136001361013620136301364013650136601367013680136901370013710137201373013740137501376013770137801379013800138101382013830138401385013860138701388013890139001391013920139301394013950139601397013980139901400014010140201403014040140501406014070140801409014100141101412014130141401415014160141701418014190142001421014220142301424014250142601427014280142901430014310143201433014340143501436014370143801439014400144101442014430144401445014460144701448014490145001451014520145301454014550145601457014580145901460014610146201463014640146501466014670146801469014700147101472014730147401475014760147701478014790148001481014820148301484014850148601487014880148901490014910149201493014940149501496014970149801499015000150101502015030150401505015060150701508015090151001511015120151301514015150151601517015180151901520015210152201523015240152501526015270152801529015300153101532015330153401535015360153701538015390154001541015420154301544015450154601547015480154901550015510155201553015540155501556015570155801559015600156101562015630156401565015660156701568015690157001571015720157301574015750157601577015780157901580015810158201583015840158501586015870158801589015900159101592015930159401595015960159701598015990160001601016020160301604016050160601607016080160901610016110161201613016140161501616016170161801619016200162101622016230162401625016260162701628016290163001631016320163301634016350163601637016380163901640016410164201643016440164501646016470164801649016500165101652016530165401655016560165701658016590166001661016620166301664016650166601667016680166901670016710167201673016740167501676016770167801679016800168101682016830168401685016860168701688016890169001691016920169301694016950169601697016980169901700017010170201703017040170501706017070170801709017100171101712017130171401715017160171701718017190172001721017220172301724017250172601727017280172901730017310173201733017340173501736017370173801739017400174101742017430174401745017460174701748017490175001751017520175301754017550175601757017580175901760017610176201763017640176501766017670176801769017700177101772017730177401775017760177701778017790178001781017820178301784017850178601787017880178901790017910179201793017940179501796017970179801799018000180101802018030180401805018060180701808018090181001811018120181301814018150181601817018180181901820018210182201823018240182501826018270182801829018300183101832018330183401835018360183701838018390184001841018420184301844018450184601847018480184901850018510185201853018540185501856018570185801859018600186101862018630186401865018660186701868018690187001871018720187301874018750187601877018780187901880018810188201883018840188501886018870188801889018900189101892018930189401895018960189701898018990190001901019020190301904019050190601907019080190901910019110191201913019140191501916019170191801919019200192101922019230192401925019260192701928019290193001931019320193301934019350193601937019380193901940019410194201943019440194501946019470194801949019500195101952019530195401955019560195701958019590196001961019620196301964019650196601967019680196901970019710197201973019740197501976019770197801979019800198101982019830198401985019860198701988019890199001991019920199301994019950199601997019980199902000020010200202003020040200502006020070200802009020100201102012020130201402015020160201702018020190202002021020220202302024020250202602027020280202902030020310203202033020340203502036020370203802039020400204102042020430204402045020460204702048020490205002051020520205302054020550205602057020580205902060020610206202063020640206502066020670206802069020700207102072020730207402075020760207702078020790208002081020820208302084020850208602087020880208902090020910209202093020940209502096020970209802099021000210102102021030210402105021060210702108021090211002111021120211302114021150211602117021180211902120021210212202123021240212502126021270212802129021300213102132021330213402135021360213702138021390214002141021420214302144021450214602147021480214902150021510215202153021540215502156021570215802159021600216102162021630216402165021660216702168021690217002171021720217302174021750217602177021780217902180021810218202183021840218502186021870218802189021900219102192021930219402195021960219702198021990220002201022020220302204022050220602207022080220902210022110221202213022140221502216022170221802219022200222102222022230222402225022260222702228022290223002231022320223302234022350223602237022380223902240022410224202243022440224502246022470224802249022500225102252022530225402255022560225702258022590226002261022620226302264022650226602267022680226902270022710227202273022740227502276022770227802279022800228102282022830228402285022860228702288022890229002291022920229302294022950229602297022980229902300023010230202303023040230502306023070230802309023100231102312023130231402315023160231702318023190232002321023220232302324023250232602327023280232902330023310233202333023340233502336023370233802339023400234102342023430234402345023460234702348023490235002351023520235302354023550235602357023580235902360023610236202363023640236502366023670236802369023700237102372023730237402375023760237702378023790238002381023820238302384023850238602387023880238902390023910239202393023940239502396023970239802399024000240102402024030240402405024060240702408024090241002411024120241302414024150241602417024180241902420024210242202423024240242502426024270242802429024300243102432024330243402435024360243702438024390244002441024420244302444024450244602447024480244902450024510245202453024540245502456024570245802459024600246102462024630246402465024660246702468024690247002471024720247302474024750247602477024780247902480024810248202483024840248502486024870248802489024900249102492024930249402495024960249702498024990250002501025020250302504025050250602507025080250902510025110251202513025140251502516025170251802519025200252102522025230252402525025260252702528025290253002531025320253302534025350253602537025380253902540025410254202543025440254502546025470254802549025500255102552025530255402555025560255702558025590256002561025620256302564025650256602567025680256902570025710257202573025740257502576025770257802579025800258102582025830258402585025860258702588025890259002591025920259302594025950259602597025980259902600026010260202603026040260502606026070260802609026100261102612026130261402615026160261702618026190262002621026220262302624026250262602627026280262902630026310263202633026340263502636026370263802639026400264102642026430264402645026460264702648026490265002651026520265302654026550265602657026580265902660026610266202663026640266502666026670266802669026700267102672026730267402675026760267702678026790268002681026820268302684026850268602687026880268902690026910269202693026940269502696026970269802699027000270102702027030270402705027060270702708027090271002711027120271302714027150271602717027180271902720027210272202723027240272502726027270272802729027300273102732027330273402735027360273702738027390274002741027420274302744027450274602747027480274902750027510275202753027540275502756027570275802759027600276102762027630276402765027660276702768027690277002771027720277302774027750277602777027780277902780027810278202783027840278502786027870278802789027900279102792027930279402795027960279702798027990280002801028020280302804028050280602807028080280902810028110281202813028140281502816028170281802819028200282102822028230282402825028260282702828028290283002831028320283302834028350283602837028380283902840028410284202843028440284502846028470284802849028500285102852028530285402855028560285702858028590286002861028620286302864028650286602867028680286902870028710287202873028740287502876028770287802879028800288102882028830288402885028860288702888028890289002891028920289302894028950289602897028980289902900029010290202903029040290502906029070290802909029100291102912029130291402915029160291702918029190292002921029220292302924029250292602927029280292902930029310293202933029340293502936029370293802939029400294102942029430294402945029460294702948029490295002951029520295302954029550295602957029580295902960029610296202963029640296502966029670296802969029700297102972029730297402975029760297702978029790298002981029820298302984029850298602987029880298902990029910299202993029940299502996029970299802999030000300103002030030300403005030060300703008030090301003011030120301303014030150301603017030180301903020030210302203023030240302503026030270302803029030300303103032030330303403035030360303703038030390304003041030420304303044030450304603047030480304903050030510305203053030540305503056030570305803059030600306103062030630306403065030660306703068030690307003071030720307303074030750307603077030780307903080030810308203083030840308503086030870308803089030900309103092030930309403095030960309703098030990310003101031020310303104031050310603107031080310903110031110311203113031140311503116031170311803119031200312103122031230312403125031260312703128031290313003131031320313303134031350313603137031380313903140031410314203143031440314503146031470314803149031500315103152031530315403155031560315703158031590316003161031620316303164031650316603167031680316903170031710317203173031740317503176031770317803179031800318103182031830318403185031860318703188031890319003191031920319303194031950319603197031980319903200032010320203203032040320503206032070320803209032100321103212032130321403215032160321703218032190322003221032220322303224032250322603227032280322903230032310323203233032340323503236032370323803239032400324103242032430324403245032460324703248032490325003251032520325303254032550325603257032580325903260032610326203263032640326503266032670326803269032700327103272032730327403275032760327703278032790328003281032820328303284032850328603287032880328903290032910329203293032940329503296032970329803299033000330103302033030330403305033060330703308033090331003311033120331303314033150331603317033180331903320033210332203323033240332503326033270332803329033300333103332033330333403335033360333703338033390334003341033420334303344033450334603347033480334903350033510335203353033540335503356033570335803359033600336103362033630336403365033660336703368033690337003371033720337303374033750337603377033780337903380033810338203383033840338503386033870338803389033900339103392033930339403395033960339703398033990340003401034020340303404034050340603407034080340903410034110341203413034140341503416034170341803419034200342103422034230342403425034260342703428034290343003431034320343303434034350343603437034380343903440034410344203443034440344503446034470344803449034500345103452034530345403455034560345703458034590346003461034620346303464034650346603467034680346903470034710347203473034740347503476034770347803479034800348103482034830348403485034860348703488034890349003491034920349303494034950349603497034980349903500035010350203503035040350503506035070350803509035100351103512035130351403515035160351703518035190352003521035220352303524035250352603527035280352903530035310353203533035340353503536035370353803539035400354103542035430354403545035460354703548035490355003551035520355303554035550355603557035580355903560035610356203563035640356503566035670356803569035700357103572035730357403575035760357703578035790358003581035820358303584035850358603587035880358903590035910359203593035940359503596035970359803599036000360103602036030360403605036060360703608036090361003611036120361303614036150361603617036180361903620036210362203623036240362503626036270362803629036300363103632036330363403635036360363703638036390364003641036420 module trial.terminal; version(Posix) { enum SIGWINCH = 28; __gshared bool windowSizeChanged = false; __gshared bool interrupted = false; __gshared bool hangedUp = false; 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) {} } } } 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; } 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; 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, } enum ConsoleInputFlags { raw = 0, echo = 1, mouse = 2, paste = 4, size = 8, releasedKeys = 64, allInputEvents = 8|4|2, allInputEventsWithRelease = allInputEvents|releasedKeys, } enum ConsoleOutputType { linear = 0, cellular = 1, minimalProcessing = 255, } enum ForceOption { automatic = 0, neverSend = -1, alwaysSend = 1, } 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; } 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; } 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) { commitCurrentEntry(); return; } if(line[0] == '#') return; size_t termination = line.length; if(line[$-1] == '\\') termination--; current ~= strip(line[0 .. termination]); 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, ":")) { 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; } 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; 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) 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); } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[22;0t"); } } version(Windows) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO originalSbi; } version(Windows) 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); COORD size; clear(); } else { hConsole = GetStdHandle(STD_OUTPUT_HANDLE); } GetConsoleScreenBufferInfo(hConsole, &originalSbi); } bool _suppressDestruction; version(Posix) ~this() { if(_suppressDestruction) { flush(); return; } if(type == ConsoleOutputType.cellular) { doTermcap("te"); } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[23;0t"); } showCursor(); reset(); flush(); if(lineGetter !is null) lineGetter.dispose(); } version(Windows) ~this() { flush(); reset(); showCursor(); if(lineGetter !is null) lineGetter.dispose(); auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleActiveScreenBuffer(stdo); if(hConsole !is stdo) CloseHandle(hConsole); } LineGetter lineGetter; int _currentForeground = Color.DEFAULT; int _currentBackground = Color.DEFAULT; RGB _currentForegroundRGB; RGB _currentBackgroundRGB; bool reverseVideo = false; 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 { import std.process; import std.string; if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { 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; } writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", colorToXTermPaletteIndex(foreground), colorToXTermPaletteIndex(background) )); return true; } } void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { if(force != ForceOption.neverSend) { version(Windows) { ushort setTof = cast(ushort) foreground; ushort setTob = cast(ushort) background; 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(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; ushort setTof = cast(ushort) foreground & ~Bright; ushort setTob = cast(ushort) background & ~Bright; if(foreground & Color.DEFAULT) setTof = 9; 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; 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; } void reset() { version(Windows) SetConsoleTextAttribute( hConsole, originalSbi.wAttributes); else writeStringRaw("\033[0m"); _underlined = false; _currentForeground = Color.DEFAULT; _currentBackground = Color.DEFAULT; reverseVideo = false; } @property int cursorX() { return _cursorX; } @property int cursorY() { return _cursorY; } private int _cursorX; private int _cursorY; 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(); COORD coord = {cast(short) x, cast(short) y}; SetConsoleCursorPosition(hConsole, coord); } else static assert(0); } _cursorX = x; _cursorY = y; } void showCursor() { version(Posix) doTermcap("ve"); else { CONSOLE_CURSOR_INFO info; GetConsoleCursorInfo(hConsole, &info); info.bVisible = true; SetConsoleCursorInfo(hConsole, &info); } } 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; void autoHideCursor() { autoHidingCursor = true; } private void executeAutoHideCursor() { if(autoHidingCursor) { version(Windows) hideCursor(); else version(Posix) { writeBuffer = "\033[?25l" ~ writeBuffer; } autoHiddenCursor = true; autoHidingCursor = false; } } void autoShowCursor() { if(autoHiddenCursor) showCursor(); autoHidingCursor = false; autoHiddenCursor = false; } 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)); } } 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; 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; @property int width() { if(_width == 0 || _height == 0) updateSize(); return _width; } @property int height() { if(_width == 0 || _height == 0) updateSize(); return _height; } void writef(T...)(string f, T t) { import std.string; writePrintableString(format(f, t)); } void writefln(T...)(string f, T t) { writef(f ~ "\n", t); } void write(T...)(T t) { import std.conv; string data; foreach(arg; t) { data ~= to!string(arg); } writePrintableString(data); } void writeln(T...)(T t) { write(t, "\n"); } void writePrintableString(in char[] s, ForceOption force = ForceOption.automatic) { foreach(ch; s) { switch(ch) { case '\n': _cursorX = 0; _cursorY++; break; case '\r': _cursorX = 0; break; case '\t': _cursorX ++; _cursorX += _cursorX % 8; break; default: if(ch <= 127) _cursorX++; } if(_wrapAround && _cursorX > width) { _cursorX = 0; _cursorY++; } if(_cursorY == height) _cursorY--; } writeStringRaw(s); } bool _wrapAround = true; deprecated alias writePrintableString writeString; private string writeBuffer; void writeStringRaw(in char[] s) { version(Posix) { writeBuffer ~= s; } else version(Windows) { writeBuffer ~= s; } else static assert(0); } void clear() { version(Posix) { doTermcap("cl"); } else version(Windows) { 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; } string getline(string prompt = null) { if(lineGetter is null) lineGetter = new LineGetter(&this); lineGetter.terminal = &this; if(prompt !is null) lineGetter.prompt = prompt; auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw); auto line = lineGetter.getline(&input); writePrintableString("\n"); return line; } } 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; } version(Windows) { private DWORD oldInput; private DWORD oldOutput; HANDLE inputHandle; } private ConsoleInputFlags flags; private Terminal* terminal; private void delegate()[] destructor; 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 ; mode |= ENABLE_WINDOW_INPUT ; if(flags & ConsoleInputFlags.echo) mode |= ENABLE_ECHO_INPUT; if(flags & ConsoleInputFlags.mouse) mode |= ENABLE_MOUSE_INPUT; SetConsoleMode(inputHandle, mode); destructor ~= { SetConsoleMode(inputHandle, oldInput); }; GetConsoleMode(terminal.hConsole, &oldOutput); mode = 0; mode |= ENABLE_PROCESSED_OUTPUT; mode |= ENABLE_WRAP_AT_EOL_OUTPUT; SetConsoleMode(terminal.hConsole, mode); destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; } 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); } 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) { terminal.writeStringRaw("\033[?1000h"); destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; import std.process : environment; if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { terminal.writeStringRaw("\033[?1003h"); destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; } else if(terminal.terminalInFamily("rxvt", "screen") || environment.get("MOUSE_HACK") == "1002") { terminal.writeStringRaw("\033[?1002h"); destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; } } if(flags & ConsoleInputFlags.paste) { if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { terminal.writeStringRaw("\033[?2004h"); destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; } } 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() { version(Posix) if(fdIn != -1) tcsetattr(fdIn, TCSANOW, &old); version(Posix) { if(flags & ConsoleInputFlags.size) { sigaction(SIGWINCH, &oldSigWinch, null); } sigaction(SIGINT, &oldSigIntr, null); sigaction(SIGHUP, &oldHupIntr, null); } foreach_reverse(d; destructor) d(); } bool kbhit() { auto got = getch(true); if(got == dchar.init) return false; getchBuffer = got; return true; } bool timedCheckForInput(int milliseconds) { version(Windows) { auto response = WaitForSingleObject(terminal.hConsole, milliseconds); if(response == 0) return true; 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); } } bool anyInput_internal() { if(inputQueue.length || timedCheckForInput(0)) return true; version(Posix) if(interrupted || windowSizeChanged || hangedUp) return true; return false; } private dchar getchBuffer; 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; } 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; if(ret == -1) { import core.stdc.errno; if(errno == EINTR) if(interruptable) return -1; else goto try_again; else throw new Exception("read failed"); } if(ret == 1) return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; else assert(0); } 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; 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; 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); } 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; 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(); INPUT_RECORD[32] buffer; DWORD actuallyRead; 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; if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) break; e.modifierState = ev.dwControlKeyState; ne.modifierState = ev.dwControlKeyState; ke.modifierState = ev.dwControlKeyState; if(ev.UnicodeChar) { ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(ke, terminal); e.character = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(e, terminal); } else { ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 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: e.eventType = MouseEvent.Type.Pressed; static DWORD lastButtonState; auto lastButtonState2 = lastButtonState; e.buttons = ev.dwButtonState; lastButtonState = e.buttons; if(cast(DWORD) e.buttons < lastButtonState2) { e.eventType = MouseEvent.Type.Released; e.buttons = lastButtonState2 & ~e.buttons; } break; case MOUSE_MOVED: e.eventType = MouseEvent.Type.Moved; e.buttons = ev.dwButtonState; break; case 0x0004: 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; default: } } return newEvents; } version(Posix) InputEvent[] readNextEvents() { terminal.flush(); auto initial = readNextEventsHelper(); while(timedCheckForInput(0)) { auto ne = readNextEventsHelper(); initial ~= ne; foreach(n; ne) if(n.type == InputEvent.Type.EndOfFileEvent) return initial; } return initial; } version(Posix) InputEvent[] readNextEventsHelper() { InputEvent[] charPressAndRelease(dchar character) { if((flags & ConsoleInputFlags.releasedKeys)) return [ InputEvent(KeyboardEvent(true, character, 0), terminal), InputEvent(KeyboardEvent(false, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal), ]; else return [ InputEvent(KeyboardEvent(true, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal) ]; } InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { if((flags & ConsoleInputFlags.releasedKeys)) return [ InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), ]; else return [ InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) ]; } char[30] sequenceBuffer; char[] readEscapeSequence(char[] sequence) { int sequenceLength = 2; sequence[0] = '\033'; sequence[1] = '['; while(sequenceLength < sequence.length) { auto n = nextRaw(); sequence[sequenceLength++] = cast(char) n; if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) break; } return sequence[0 .. sequenceLength]; } InputEvent[] translateTermcapName(string cap) { switch(cap) { 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": case "kh": case "K1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); case "kH": return keyPressAndRelease(NonCharacterKeyEvent.Key.End); case "kI": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); default: } return null; } InputEvent[] doEscapeSequence(in char[] sequence) { switch(sequence) { case "\033[200~": string data; for(;;) { auto n = nextRaw(); if(n == '\033') { n = nextRaw(); if(n == '[') { auto esc = readEscapeSequence(sequenceBuffer); if(esc == "\033[201~") { break; } else { data ~= esc; } } else { data ~= '\033'; data ~= cast(char) n; } } else { data ~= cast(char) n; } } return [InputEvent(PasteEvent(data), terminal)]; case "\033[M": auto buttonCode = nextRaw() - 32; auto x = cast(int) ((nextRaw())) - 33; auto y = cast(int) ((nextRaw())) - 33; bool isRelease = (buttonCode & 0b11) == 3; int buttonNumber; if(!isRelease) { buttonNumber = (buttonCode & 0b11); if(buttonCode & 64) buttonNumber += 3; buttonNumber++; if(buttonNumber == 2) buttonNumber = 3; else if(buttonNumber == 3) buttonNumber = 2; } auto modifiers = buttonCode & (0b0001_1100); MouseEvent m; if(buttonCode & 32) m.eventType = MouseEvent.Type.Moved; else m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; static int buttonsDown = 0; if(!isRelease && buttonNumber <= 3) buttonsDown++; if(isRelease && m.eventType != MouseEvent.Type.Moved) { if(buttonsDown) buttonsDown--; else m.eventType = MouseEvent.Type.Moved; } if(buttonNumber == 0) m.buttons = 0; else m.buttons = 1 << (buttonNumber - 1); m.x = x; m.y = y; m.modifierState = modifiers; return [InputEvent(m, terminal)]; default: 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(";"); 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; 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 '~': 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")) { } else { } } } return null; } auto c = nextRaw(true); if(c == -1) return null; if(c == 0) return [InputEvent(EndOfFileEvent(), terminal)]; if(c == '\033') { if(timedCheckForInput(50)) { c = nextRaw(); if(c == '[') { return doEscapeSequence(readEscapeSequence(sequenceBuffer)); } else if(c == 'O') { 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 { return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease(nextChar(c)); } } else { return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); } } else { auto next = nextChar(c); if(next == 127) next = '\b'; return charPressAndRelease(next); } } } struct KeyboardEvent { bool pressed; dchar which; uint modifierState; bool isCharacter() { return !(which >= Key.min && which <= Key.max); } 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, } } struct CharacterEvent { enum Type { Released, Pressed } Type eventType; dchar character; uint modifierState; } struct NonCharacterKeyEvent { enum Type { Released, Pressed } Type eventType; 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; } struct PasteEvent { string pastedText; } struct MouseEvent { enum Type { Moved = 0, Pressed = 1, Released = 2, Clicked, } Type eventType; enum Button : uint { None = 0, Left = 1, Middle = 4, Right = 2, ScrollUp = 8, ScrollDown = 16 } uint buttons; int x; int y; uint modifierState; } struct SizeChangedEvent { int oldWidth; int oldHeight; int newWidth; int newHeight; } struct UserInterruptionEvent {} struct HangupEvent {} struct EndOfFileEvent {} interface CustomEvent {} version(Windows) enum ModifierState : uint { shift = 0x10, control = 0x8 | 0x4, alt = 2 | 1, windows = 512, meta = 4096, scrollLock = 0x40, } else enum ModifierState : uint { shift = 4, alt = 2, control = 16, meta = 8, windows = 512 } version(DDoc) enum ModifierState : uint { shift = 4, alt = 2, control = 16, } struct InputEvent { enum Type { KeyboardEvent, CharacterEvent, NonCharacterKeyEvent, PasteEvent, MouseEvent, SizeChangedEvent, UserInterruptionEvent, EndOfFileEvent, HangupEvent, CustomEvent } @property Type type() { return t; } @property Terminal* terminal() { return term; } @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"); } 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; } } } class LineGetter { string[] history; Terminal* terminal; string historyFilename; 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; } void dispose() { if(historyFilename.length) saveSettingsAndHistoryToFile(); } string historyFileDirectory() { version(Windows) { char[1024] path; if(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"; } } Color suggestionForeground; Color regularForeground; Color background; string prompt; bool autoSuggest = true; string historyFilter(string candidate) { return candidate; } 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; } 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; } } 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; } protected void showTabCompleteList(string[] list) { if(list.length) { 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(); } } 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) howFarBack = cast(int) history.length; if(howFarBack == currentHistoryViewPosition) return; if(currentHistoryViewPosition == 0) { 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; } private int startOfLineX; private int startOfLineY; private string suggestion(string[] list = null) { import std.algorithm, std.utf; auto relevantLineSection = line[0 .. cursorPosition]; 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; } 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) { foreach(dchar ch; s) addChar(ch); } 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); } } 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); } void startGettingLine() { 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(); version(Windows) { CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(terminal.hConsole, &info); startOfLineX = info.dwCursorPosition.X; startOfLineY = info.dwCursorPosition.Y; } else { 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; 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; } terminal._cursorX = startOfLineX; terminal._cursorY = startOfLineY; } private bool justHitTab; bool workOnLine(InputEvent e) { switch(e.type) { case InputEvent.Type.EndOfFileEvent: justHitTab = false; return false; case InputEvent.Type.KeyboardEvent: auto ev = e.keyboardEvent; if(ev.pressed == false) return true; auto ch = ev.which; switch(ch) { case 4: 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; 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: case KeyboardEvent.Key.Home: justHitTab = false; cursorPosition = 0; horizontalScrollPosition = 0; redraw(); break; case 5: case KeyboardEvent.Key.End: justHitTab = false; cursorPosition = cast(int) line.length; scrollToEnd(); redraw(); break; case KeyboardEvent.Key.Insert: justHitTab = false; insertMode = !insertMode; break; case KeyboardEvent.Key.Delete: justHitTab = false; if(ev.modifierState & ModifierState.control) deleteToEndOfLine(); else deleteChar(); redraw(); break; case 11: 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: auto me = e.mouseEvent; if(me.eventType == MouseEvent.Type.Pressed) { if(me.buttons & MouseEvent.Button.Left) { if(me.y == startOfLineY) { 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: break; case InputEvent.Type.UserInterruptionEvent: throw new UserInterruptionException(); case InputEvent.Type.HangupEvent: throw new HangupException(); default: } return true; } string finishGettingLine() { import std.conv; auto f = to!string(line); auto history = historyFilter(f); if(history !is null) this.history ~= history; return f; } } mixin template LineGetterConstructors() { this(Terminal* tty, string historyFilename = null) { super(tty, historyFilename); } } class FileLineGetter : LineGetter { mixin LineGetterConstructors; 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)) { if(startsWith(name[2..$], soFar)) list ~= text(candidate, name[searchDirectory.length + 1 + soFar.length .. $]); else if(startsWith(name, soFar)) list ~= text(candidate, name[soFar.length .. $]); } return list; } } version(Windows) { enum CSIDL_APPDATA = 26; extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); } 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) scrollbackPosition++; } void addLine(string line) { lines ~= Line([LineComponent(line)]); if(scrollbackPosition) scrollbackPosition++; } void scrollUp(int lines = 1) { scrollbackPosition += lines; } void scrollDown(int lines = 1) { scrollbackPosition -= lines; if(scrollbackPosition < 0) scrollbackPosition = 0; } void scrollToBottom() { scrollbackPosition = 0; } 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; 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; } 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; } } 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; else written++; } } lineCount++; } return lineCount - height; } 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; 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; clickRegions.length = 0; clickRegions.assumeSafeAppend(); 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; 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; } 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(idx, dchar ch; towrite) { if(written >= width) { clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); terminal.write(towrite[0 .. idx]); towrite = towrite[idx .. $]; linePos++; written = 0; terminal.moveTo(x, y + linePos); goto again; } if(ch == '\t') written += 8; 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; 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: } 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; 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 { } break; case InputEvent.Type.SizeChangedEvent: return true; case InputEvent.Type.UserInterruptionEvent: throw new UserInterruptionException(); case InputEvent.Type.HangupEvent: throw new HangupException(); case InputEvent.Type.EndOfFileEvent: break; case InputEvent.Type.CharacterEvent: case InputEvent.Type.NonCharacterKeyEvent: break; case InputEvent.Type.CustomEvent: case InputEvent.Type.PasteEvent: break; } return false; } } class UserInterruptionException : Exception { this() { super("Ctrl+C"); } } class HangupException : Exception { this() { super("Hup"); } } ubyte colorToXTermPaletteIndex(RGB color) { if(color.r == color.g && color.g == color.b) { if(color.r == 0) return 0; if(color.r >= 248) return 15; return cast(ubyte) (232 + ((color.r - 8) / 10)); } 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); } struct RGB { ubyte r; ubyte g; ubyte b; private ubyte a = 255; } 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.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 { 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; }
module trial.terminal; version(Posix) { enum SIGWINCH = 28; __gshared bool windowSizeChanged = false; __gshared bool interrupted = false; __gshared bool hangedUp = false; 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) {} } } } 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; } 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; 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, } enum ConsoleInputFlags { raw = 0, echo = 1, mouse = 2, paste = 4, size = 8, releasedKeys = 64, allInputEvents = 8|4|2, allInputEventsWithRelease = allInputEvents|releasedKeys, } enum ConsoleOutputType { linear = 0, cellular = 1, minimalProcessing = 255, } enum ForceOption { automatic = 0, neverSend = -1, alwaysSend = 1, } 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; } 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; } 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) { commitCurrentEntry(); return; } if(line[0] == '#') return; size_t termination = line.length; if(line[$-1] == '\\') termination--; current ~= strip(line[0 .. termination]); 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, ":")) { 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; } 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; 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) 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); } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[22;0t"); } } version(Windows) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO originalSbi; } version(Windows) 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); COORD size; clear(); } else { hConsole = GetStdHandle(STD_OUTPUT_HANDLE); } GetConsoleScreenBufferInfo(hConsole, &originalSbi); } bool _suppressDestruction; version(Posix) ~this() { if(_suppressDestruction) { flush(); return; } if(type == ConsoleOutputType.cellular) { doTermcap("te"); } if(terminalInFamily("xterm", "rxvt", "screen")) { writeStringRaw("\033[23;0t"); } showCursor(); reset(); flush(); if(lineGetter !is null) lineGetter.dispose(); } version(Windows) ~this() { flush(); reset(); showCursor(); if(lineGetter !is null) lineGetter.dispose(); auto stdo = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleActiveScreenBuffer(stdo); if(hConsole !is stdo) CloseHandle(hConsole); } LineGetter lineGetter; int _currentForeground = Color.DEFAULT; int _currentBackground = Color.DEFAULT; RGB _currentForegroundRGB; RGB _currentBackgroundRGB; bool reverseVideo = false; 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 { import std.process; import std.string; if(environment.get("TERM") == "rxvt" || environment.get("TERM") == "linux") { 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; } writeStringRaw(format("\033[38;5;%dm\033[48;5;%dm", colorToXTermPaletteIndex(foreground), colorToXTermPaletteIndex(background) )); return true; } } void color(int foreground, int background, ForceOption force = ForceOption.automatic, bool reverseVideo = false) { if(force != ForceOption.neverSend) { version(Windows) { ushort setTof = cast(ushort) foreground; ushort setTob = cast(ushort) background; 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(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; ushort setTof = cast(ushort) foreground & ~Bright; ushort setTob = cast(ushort) background & ~Bright; if(foreground & Color.DEFAULT) setTof = 9; 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; 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; } void reset() { version(Windows) SetConsoleTextAttribute( hConsole, originalSbi.wAttributes); else writeStringRaw("\033[0m"); _underlined = false; _currentForeground = Color.DEFAULT; _currentBackground = Color.DEFAULT; reverseVideo = false; } @property int cursorX() { return _cursorX; } @property int cursorY() { return _cursorY; } private int _cursorX; private int _cursorY; 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(); COORD coord = {cast(short) x, cast(short) y}; SetConsoleCursorPosition(hConsole, coord); } else static assert(0); } _cursorX = x; _cursorY = y; } void showCursor() { version(Posix) doTermcap("ve"); else { CONSOLE_CURSOR_INFO info; GetConsoleCursorInfo(hConsole, &info); info.bVisible = true; SetConsoleCursorInfo(hConsole, &info); } } 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; void autoHideCursor() { autoHidingCursor = true; } private void executeAutoHideCursor() { if(autoHidingCursor) { version(Windows) hideCursor(); else version(Posix) { writeBuffer = "\033[?25l" ~ writeBuffer; } autoHiddenCursor = true; autoHidingCursor = false; } } void autoShowCursor() { if(autoHiddenCursor) showCursor(); autoHidingCursor = false; autoHiddenCursor = false; } 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)); } } 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; 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; @property int width() { if(_width == 0 || _height == 0) updateSize(); return _width; } @property int height() { if(_width == 0 || _height == 0) updateSize(); return _height; } void writef(T...)(string f, T t) { import std.string; writePrintableString(format(f, t)); } void writefln(T...)(string f, T t) { writef(f ~ "\n", t); } void write(T...)(T t) { import std.conv; string data; foreach(arg; t) { data ~= to!string(arg); } writePrintableString(data); } void writeln(T...)(T t) { write(t, "\n"); } void writePrintableString(in char[] s, ForceOption force = ForceOption.automatic) { foreach(ch; s) { switch(ch) { case '\n': _cursorX = 0; _cursorY++; break; case '\r': _cursorX = 0; break; case '\t': _cursorX ++; _cursorX += _cursorX % 8; break; default: if(ch <= 127) _cursorX++; } if(_wrapAround && _cursorX > width) { _cursorX = 0; _cursorY++; } if(_cursorY == height) _cursorY--; } writeStringRaw(s); } bool _wrapAround = true; deprecated alias writePrintableString writeString; private string writeBuffer; void writeStringRaw(in char[] s) { version(Posix) { writeBuffer ~= s; } else version(Windows) { writeBuffer ~= s; } else static assert(0); } void clear() { version(Posix) { doTermcap("cl"); } else version(Windows) { 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; } string getline(string prompt = null) { if(lineGetter is null) lineGetter = new LineGetter(&this); lineGetter.terminal = &this; if(prompt !is null) lineGetter.prompt = prompt; auto input = RealTimeConsoleInput(&this, ConsoleInputFlags.raw); auto line = lineGetter.getline(&input); writePrintableString("\n"); return line; } } 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; } version(Windows) { private DWORD oldInput; private DWORD oldOutput; HANDLE inputHandle; } private ConsoleInputFlags flags; private Terminal* terminal; private void delegate()[] destructor; 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 ; mode |= ENABLE_WINDOW_INPUT ; if(flags & ConsoleInputFlags.echo) mode |= ENABLE_ECHO_INPUT; if(flags & ConsoleInputFlags.mouse) mode |= ENABLE_MOUSE_INPUT; SetConsoleMode(inputHandle, mode); destructor ~= { SetConsoleMode(inputHandle, oldInput); }; GetConsoleMode(terminal.hConsole, &oldOutput); mode = 0; mode |= ENABLE_PROCESSED_OUTPUT; mode |= ENABLE_WRAP_AT_EOL_OUTPUT; SetConsoleMode(terminal.hConsole, mode); destructor ~= { SetConsoleMode(terminal.hConsole, oldOutput); }; } 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); } 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) { terminal.writeStringRaw("\033[?1000h"); destructor ~= { terminal.writeStringRaw("\033[?1000l"); }; import std.process : environment; if(terminal.terminalInFamily("xterm") && environment.get("MOUSE_HACK") != "1002") { terminal.writeStringRaw("\033[?1003h"); destructor ~= { terminal.writeStringRaw("\033[?1003l"); }; } else if(terminal.terminalInFamily("rxvt", "screen") || environment.get("MOUSE_HACK") == "1002") { terminal.writeStringRaw("\033[?1002h"); destructor ~= { terminal.writeStringRaw("\033[?1002l"); }; } } if(flags & ConsoleInputFlags.paste) { if(terminal.terminalInFamily("xterm", "rxvt", "screen")) { terminal.writeStringRaw("\033[?2004h"); destructor ~= { terminal.writeStringRaw("\033[?2004l"); }; } } 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() { version(Posix) if(fdIn != -1) tcsetattr(fdIn, TCSANOW, &old); version(Posix) { if(flags & ConsoleInputFlags.size) { sigaction(SIGWINCH, &oldSigWinch, null); } sigaction(SIGINT, &oldSigIntr, null); sigaction(SIGHUP, &oldHupIntr, null); } foreach_reverse(d; destructor) d(); } bool kbhit() { auto got = getch(true); if(got == dchar.init) return false; getchBuffer = got; return true; } bool timedCheckForInput(int milliseconds) { version(Windows) { auto response = WaitForSingleObject(terminal.hConsole, milliseconds); if(response == 0) return true; 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); } } bool anyInput_internal() { if(inputQueue.length || timedCheckForInput(0)) return true; version(Posix) if(interrupted || windowSizeChanged || hangedUp) return true; return false; } private dchar getchBuffer; 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; } 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; if(ret == -1) { import core.stdc.errno; if(errno == EINTR) if(interruptable) return -1; else goto try_again; else throw new Exception("read failed"); } if(ret == 1) return inputPrefilter ? inputPrefilter(buf[0]) : buf[0]; else assert(0); } 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; 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; 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); } 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; 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(); INPUT_RECORD[32] buffer; DWORD actuallyRead; 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; if(!(flags & ConsoleInputFlags.releasedKeys) && !ev.bKeyDown) break; e.modifierState = ev.dwControlKeyState; ne.modifierState = ev.dwControlKeyState; ke.modifierState = ev.dwControlKeyState; if(ev.UnicodeChar) { ke.which = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(ke, terminal); e.character = cast(dchar) cast(wchar) ev.UnicodeChar; newEvents ~= InputEvent(e, terminal); } else { ne.key = cast(NonCharacterKeyEvent.Key) ev.wVirtualKeyCode; ke.which = cast(KeyboardEvent.Key) (ev.wVirtualKeyCode + 0xF0000); 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: e.eventType = MouseEvent.Type.Pressed; static DWORD lastButtonState; auto lastButtonState2 = lastButtonState; e.buttons = ev.dwButtonState; lastButtonState = e.buttons; if(cast(DWORD) e.buttons < lastButtonState2) { e.eventType = MouseEvent.Type.Released; e.buttons = lastButtonState2 & ~e.buttons; } break; case MOUSE_MOVED: e.eventType = MouseEvent.Type.Moved; e.buttons = ev.dwButtonState; break; case 0x0004: 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; default: } } return newEvents; } version(Posix) InputEvent[] readNextEvents() { terminal.flush(); auto initial = readNextEventsHelper(); while(timedCheckForInput(0)) { auto ne = readNextEventsHelper(); initial ~= ne; foreach(n; ne) if(n.type == InputEvent.Type.EndOfFileEvent) return initial; } return initial; } version(Posix) InputEvent[] readNextEventsHelper() { InputEvent[] charPressAndRelease(dchar character) { if((flags & ConsoleInputFlags.releasedKeys)) return [ InputEvent(KeyboardEvent(true, character, 0), terminal), InputEvent(KeyboardEvent(false, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Released, character, 0), terminal), ]; else return [ InputEvent(KeyboardEvent(true, character, 0), terminal), InputEvent(CharacterEvent(CharacterEvent.Type.Pressed, character, 0), terminal) ]; } InputEvent[] keyPressAndRelease(NonCharacterKeyEvent.Key key, uint modifiers = 0) { if((flags & ConsoleInputFlags.releasedKeys)) return [ InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(KeyboardEvent(false, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Released, key, modifiers), terminal), ]; else return [ InputEvent(KeyboardEvent(true, cast(dchar)(key) + 0xF0000, modifiers), terminal), InputEvent(NonCharacterKeyEvent(NonCharacterKeyEvent.Type.Pressed, key, modifiers), terminal) ]; } char[30] sequenceBuffer; char[] readEscapeSequence(char[] sequence) { int sequenceLength = 2; sequence[0] = '\033'; sequence[1] = '['; while(sequenceLength < sequence.length) { auto n = nextRaw(); sequence[sequenceLength++] = cast(char) n; if(n >= 0x40 && !(sequenceLength == 3 && n == '[')) break; } return sequence[0 .. sequenceLength]; } InputEvent[] translateTermcapName(string cap) { switch(cap) { 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": case "kh": case "K1": return keyPressAndRelease(NonCharacterKeyEvent.Key.Home); case "kH": return keyPressAndRelease(NonCharacterKeyEvent.Key.End); case "kI": return keyPressAndRelease(NonCharacterKeyEvent.Key.Insert); default: } return null; } InputEvent[] doEscapeSequence(in char[] sequence) { switch(sequence) { case "\033[200~": string data; for(;;) { auto n = nextRaw(); if(n == '\033') { n = nextRaw(); if(n == '[') { auto esc = readEscapeSequence(sequenceBuffer); if(esc == "\033[201~") { break; } else { data ~= esc; } } else { data ~= '\033'; data ~= cast(char) n; } } else { data ~= cast(char) n; } } return [InputEvent(PasteEvent(data), terminal)]; case "\033[M": auto buttonCode = nextRaw() - 32; auto x = cast(int) ((nextRaw())) - 33; auto y = cast(int) ((nextRaw())) - 33; bool isRelease = (buttonCode & 0b11) == 3; int buttonNumber; if(!isRelease) { buttonNumber = (buttonCode & 0b11); if(buttonCode & 64) buttonNumber += 3; buttonNumber++; if(buttonNumber == 2) buttonNumber = 3; else if(buttonNumber == 3) buttonNumber = 2; } auto modifiers = buttonCode & (0b0001_1100); MouseEvent m; if(buttonCode & 32) m.eventType = MouseEvent.Type.Moved; else m.eventType = isRelease ? MouseEvent.Type.Released : MouseEvent.Type.Pressed; static int buttonsDown = 0; if(!isRelease && buttonNumber <= 3) buttonsDown++; if(isRelease && m.eventType != MouseEvent.Type.Moved) { if(buttonsDown) buttonsDown--; else m.eventType = MouseEvent.Type.Moved; } if(buttonNumber == 0) m.buttons = 0; else m.buttons = 1 << (buttonNumber - 1); m.x = x; m.y = y; m.modifierState = modifiers; return [InputEvent(m, terminal)]; default: 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(";"); 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; 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 '~': 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")) { } else { } } } return null; } auto c = nextRaw(true); if(c == -1) return null; if(c == 0) return [InputEvent(EndOfFileEvent(), terminal)]; if(c == '\033') { if(timedCheckForInput(50)) { c = nextRaw(); if(c == '[') { return doEscapeSequence(readEscapeSequence(sequenceBuffer)); } else if(c == 'O') { 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 { return keyPressAndRelease(NonCharacterKeyEvent.Key.escape) ~ charPressAndRelease(nextChar(c)); } } else { return keyPressAndRelease(NonCharacterKeyEvent.Key.escape); } } else { auto next = nextChar(c); if(next == 127) next = '\b'; return charPressAndRelease(next); } } } struct KeyboardEvent { bool pressed; dchar which; uint modifierState; bool isCharacter() { return !(which >= Key.min && which <= Key.max); } 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, } } struct CharacterEvent { enum Type { Released, Pressed } Type eventType; dchar character; uint modifierState; } struct NonCharacterKeyEvent { enum Type { Released, Pressed } Type eventType; 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; } struct PasteEvent { string pastedText; } struct MouseEvent { enum Type { Moved = 0, Pressed = 1, Released = 2, Clicked, } Type eventType; enum Button : uint { None = 0, Left = 1, Middle = 4, Right = 2, ScrollUp = 8, ScrollDown = 16 } uint buttons; int x; int y; uint modifierState; } struct SizeChangedEvent { int oldWidth; int oldHeight; int newWidth; int newHeight; } struct UserInterruptionEvent {} struct HangupEvent {} struct EndOfFileEvent {} interface CustomEvent {} version(Windows) enum ModifierState : uint { shift = 0x10, control = 0x8 | 0x4, alt = 2 | 1, windows = 512, meta = 4096, scrollLock = 0x40, } else enum ModifierState : uint { shift = 4, alt = 2, control = 16, meta = 8, windows = 512 } version(DDoc) enum ModifierState : uint { shift = 4, alt = 2, control = 16, } struct InputEvent { enum Type { KeyboardEvent, CharacterEvent, NonCharacterKeyEvent, PasteEvent, MouseEvent, SizeChangedEvent, UserInterruptionEvent, EndOfFileEvent, HangupEvent, CustomEvent } @property Type type() { return t; } @property Terminal* terminal() { return term; } @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"); } 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; } } } class LineGetter { string[] history; Terminal* terminal; string historyFilename; 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; } void dispose() { if(historyFilename.length) saveSettingsAndHistoryToFile(); } string historyFileDirectory() { version(Windows) { char[1024] path; if(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"; } } Color suggestionForeground; Color regularForeground; Color background; string prompt; bool autoSuggest = true; string historyFilter(string candidate) { return candidate; } 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; } 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; } } 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; } protected void showTabCompleteList(string[] list) { if(list.length) { 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(); } } 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) howFarBack = cast(int) history.length; if(howFarBack == currentHistoryViewPosition) return; if(currentHistoryViewPosition == 0) { 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; } private int startOfLineX; private int startOfLineY; private string suggestion(string[] list = null) { import std.algorithm, std.utf; auto relevantLineSection = line[0 .. cursorPosition]; 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; } 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) { foreach(dchar ch; s) addChar(ch); } 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); } } 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); } void startGettingLine() { 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(); version(Windows) { CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(terminal.hConsole, &info); startOfLineX = info.dwCursorPosition.X; startOfLineY = info.dwCursorPosition.Y; } else { 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; 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; } terminal._cursorX = startOfLineX; terminal._cursorY = startOfLineY; } private bool justHitTab; bool workOnLine(InputEvent e) { switch(e.type) { case InputEvent.Type.EndOfFileEvent: justHitTab = false; return false; case InputEvent.Type.KeyboardEvent: auto ev = e.keyboardEvent; if(ev.pressed == false) return true; auto ch = ev.which; switch(ch) { case 4: 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; 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: case KeyboardEvent.Key.Home: justHitTab = false; cursorPosition = 0; horizontalScrollPosition = 0; redraw(); break; case 5: case KeyboardEvent.Key.End: justHitTab = false; cursorPosition = cast(int) line.length; scrollToEnd(); redraw(); break; case KeyboardEvent.Key.Insert: justHitTab = false; insertMode = !insertMode; break; case KeyboardEvent.Key.Delete: justHitTab = false; if(ev.modifierState & ModifierState.control) deleteToEndOfLine(); else deleteChar(); redraw(); break; case 11: 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: auto me = e.mouseEvent; if(me.eventType == MouseEvent.Type.Pressed) { if(me.buttons & MouseEvent.Button.Left) { if(me.y == startOfLineY) { 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: break; case InputEvent.Type.UserInterruptionEvent: throw new UserInterruptionException(); case InputEvent.Type.HangupEvent: throw new HangupException(); default: } return true; } string finishGettingLine() { import std.conv; auto f = to!string(line); auto history = historyFilter(f); if(history !is null) this.history ~= history; return f; } } mixin template LineGetterConstructors() { this(Terminal* tty, string historyFilename = null) { super(tty, historyFilename); } } class FileLineGetter : LineGetter { mixin LineGetterConstructors; 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)) { if(startsWith(name[2..$], soFar)) list ~= text(candidate, name[searchDirectory.length + 1 + soFar.length .. $]); else if(startsWith(name, soFar)) list ~= text(candidate, name[soFar.length .. $]); } return list; } } version(Windows) { enum CSIDL_APPDATA = 26; extern(Windows) HRESULT SHGetFolderPathA(HWND, int, HANDLE, DWORD, LPSTR); } 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) scrollbackPosition++; } void addLine(string line) { lines ~= Line([LineComponent(line)]); if(scrollbackPosition) scrollbackPosition++; } void scrollUp(int lines = 1) { scrollbackPosition += lines; } void scrollDown(int lines = 1) { scrollbackPosition -= lines; if(scrollbackPosition < 0) scrollbackPosition = 0; } void scrollToBottom() { scrollbackPosition = 0; } 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; 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; } 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; } } 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; else written++; } } lineCount++; } return lineCount - height; } 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; 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; clickRegions.length = 0; clickRegions.assumeSafeAppend(); 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; 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; } 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(idx, dchar ch; towrite) { if(written >= width) { clickRegions ~= ClickRegion(&component, terminal.cursorX, terminal.cursorY, written); terminal.write(towrite[0 .. idx]); towrite = towrite[idx .. $]; linePos++; written = 0; terminal.moveTo(x, y + linePos); goto again; } if(ch == '\t') written += 8; 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; 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: } 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; 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 { } break; case InputEvent.Type.SizeChangedEvent: return true; case InputEvent.Type.UserInterruptionEvent: throw new UserInterruptionException(); case InputEvent.Type.HangupEvent: throw new HangupException(); case InputEvent.Type.EndOfFileEvent: break; case InputEvent.Type.CharacterEvent: case InputEvent.Type.NonCharacterKeyEvent: break; case InputEvent.Type.CustomEvent: case InputEvent.Type.PasteEvent: break; } return false; } } class UserInterruptionException : Exception { this() { super("Ctrl+C"); } } class HangupException : Exception { this() { super("Hup"); } } ubyte colorToXTermPaletteIndex(RGB color) { if(color.r == color.g && color.g == color.b) { if(color.r == 0) return 0; if(color.r >= 248) return 15; return cast(ubyte) (232 + ((color.r - 8) / 10)); } 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); } struct RGB { ubyte r; ubyte g; ubyte b; private ubyte a = 255; } 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.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 { 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; }