123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Line_discipline.mli *)(*****************************************************************************)(* Types *)(*****************************************************************************)typemode=Cooked|Rawtypeevent=Lineofstring|Keyofstring|Interrupt|End_of_filetypet={mode:mode;echo:bool;line:string}letcreate()={mode=Cooked;echo=true;line=""}letmode(t:t)=t.modeletset_mode(t:t)mode={twithmode}letset_echo(t:t)echo={twithecho}letpending(t:t)=t.line(*****************************************************************************)(* Keys *)(*****************************************************************************)letsplit_keys(s:string):stringlist=letn=String.lengthsin(* where the key starting at [i] ends (excluded) *)letkey_endi=letc=Char.codes.[i]inifs.[i]='\x1b'&&i+1<nthenmatchs.[i+1]with|'['->(* the parameters, then a final byte from @ to ~ *)letj=ref(i+2)inwhile!j<n&¬(s.[!j]>='@'&&s.[!j]<='~')doincrjdone;minn(!j+1)|'O'->minn(i+3)(* ESC and a character: Alt and that character *)|_->i+2elseifcland0xE0=0xC0thenminn(i+2)elseifcland0xF0=0xE0thenminn(i+3)elseifcland0xF8=0xF0thenminn(i+4)elsei+1inletrecgoiacc=ifi>=nthenList.revaccelseletj=key_endiingoj(String.subsi(j-i)::acc)ingo0[](* the line without its last UTF-8 character (continuation bytes are
10xxxxxx: drop them, then the lead) *)letdrop_last(s:string):string=letn=ref(String.lengths)inwhile!n>0&&Char.codes.[!n-1]land0xC0=0x80dodecrndone;if!n>0thenString.subs0(!n-1)else""(* how many characters (not bytes) the screen shows for [s] *)letlength(s:string):int=String.fold_left(funkc->ifChar.codecland0xC0=0x80thenkelsek+1)0sletrubout(k:int):string=String.concat""(List.initk(fun_->"\b \b"))(*****************************************************************************)(* Cooked mode *)(*****************************************************************************)(* one key: the tty after it, its echo, what the program reads *)letcooked(t:t)(key:string):t*string*eventlist=letechos=ift.echothenselse""inmatchkeywith|"\r"|"\n"->({twithline=""},echo"\r\n",[Linet.line])|"\x7f"|"\b"->ift.line=""then(t,"",[])else({twithline=drop_lastt.line},echo(rubout1),[])(* Control-U *)|"\x15"->({twithline=""},echo(rubout(lengtht.line)),[])(* Control-W: the spaces before the word, then the word *)|"\x17"->letrecstrippreds=ifs<>""&&preds.[String.lengths-1]thenstrippred(drop_lasts)elsesinletline=strip(func->c<>' ')(strip(func->c=' ')t.line)in({twithline},echo(rubout(lengtht.line-lengthline)),[])(* Control-C *)|"\x03"->({twithline=""},echo"^C\r\n",[Interrupt])(* Control-D *)|"\x04"->ift.line=""then(t,"",[End_of_file])else(t,"",[])(* other controls, and escape sequences (arrows...): dropped *)|_whenkey.[0]<' '||key.[0]='\x7f'->(t,"",[])|_->({twithline=t.line^key},echokey,[])(*****************************************************************************)(* Entry points *)(*****************************************************************************)letinput(t:t)(bytes:string):t*string*eventlist=List.fold_left(fun(t,echoed,events)key->matcht.modewith|Raw->(t,echoed,events@[Keykey])|Cooked->lett,e,evs=cookedtkeyin(t,echoed^e,events@evs))(t,"",[])(split_keysbytes)letoutput(bytes:string):string=String.concat"\r\n"(String.split_on_char'\n'bytes)