123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037(* This file is part of BOGUE, by San Vu Ngoc *)(* This module construct a layout implementing a file dialog:
+ file or directory chooser
+ one selection or multiple selections
+ file changes monitor to refresh the layout
+ filter by mimetype
2024-2025: IN PROGRESS
TODO::
+ dynamic filtering
+ create directory button
*)(*
* liste directory
https://v2.ocaml.org/releases/5.0/api/Unix.html#1_Directories
Use Unix.readdir ou Sys.readdir to read the file list, then (List.filter
Sys.is_directory) to separate files and dirs (works also for symlinks). Use
Unix.stat (ou UnixLabels) to get info.
http://caml.inria.fr/pub/docs/manual-ocaml/libref/Unix.html
See also
https://caml.inria.fr/pub/docs/manual-ocaml/libref/Filename.html
* Attention aux dossiers avec grand nombre de fichiers => utiliser Long_list (ou
Table)
* faire un filtre dynamique pour les noms de fichiers
* aperçu d'images?
* création de dossier ?
* fontawesome:
fa-folder fa-folder-open fa-folder-o fa-folder-open-o
fa-file fa-file-text fa-file-o fa-files-o fa-file-text-o fa-file-pdf-o fa-file-photo-o fa-file-word-o fa-file-excel-o fa-file-powerpoint-o fa-file-picture-o fa-file-zip-o fa-file-sound-o fa-file-movie-o fa-file-code-o
* type de fichier
https://v2.ocaml.org/releases/5.0/api/Unix.LargeFile.html#VALstat
https://v2.ocaml.org/releases/5.0/api/Unix.html#TYPEfile_kind
character devices: fa-plug
block devive: fa-hdd-o
link: fa-link
pipe: fa-exchange
soxket: fa-phone ?? fa-phone-square
* icones
https://specifications.freedesktop.org/icon-naming-spec/latest/
$ man xdg-icon-resource
https://www.freedesktop.org/wiki/Specifications/icon-theme-spec/
#!/bin/bash
mime=$(xdg-mime query filetype "$1")
icon_name=$(echo "$mime" | sed 's|/|-|g')
echo "Nom de l'icône : $icon_name"
$ gio info b_file.ml | grep icon
standard::icon: /home/san/Images/icons/ocaml-icon.png, text-x-ocaml, text-x-generic, /home/san/Images/icons/ocaml-icon.png-symbolic, text-x-ocaml-symbolic, text-x-generic-symbolic
standard::symbolic-icon: /home/san/Images/icons/ocaml-icon.png-symbolic, text-x-ocaml-symbolic, text-x-generic-symbolic, /home/san/Images/icons/ocaml-icon.png, text-x-ocaml, text-x-generic
san@san-XPS-13-9350 ~/prog/ocaml/bogue (file_dialog) $ gwenview /home/san/Images/icons/ocaml-icon
ou
$ gio info -a standard::icon b_file.ml
uri: file:///home/san/prog/ocaml/bogue/b_file.ml
local path: /home/san/prog/ocaml/bogue/b_file.ml
unix mount: /dev/nvme0n1p6 /home ext4 rw,relatime
attributes:
standard::icon: /home/san/Images/icons/ocaml-icon.png, text-x-ocaml, text-x-generic, /home/san/Images/icons/ocaml-icon.png-symbolic, text-x-ocaml-symbolic, text-x-generic-symbolic*
* mimetype
$ file --mime b_file.ml
b_file.ml: text/x-ruby; charset=utf-8
$ file -b --mime-type themes/paper/paper.png
image/png
** mieux:
$ mimetype -b b_file.ml
text/x-ocaml
$ xdg-mime query filetype b_file.ml
text/x-ocaml
$ kioclient stat b_file.ml
NAME b_file.ml
SIZE 24795
FILE_TYPE 0100000
DEVICE_ID 66310
INODE 15893614
MIME_TYPE text/x-ocaml
ACCESS 0664
MODIFICATION_TIME Fri Dec 13 13:55:10 2024
ACCESS_TIME Fri Dec 13 13:48:29 2024
CREATION_TIME Fri Dec 13 12:54:59 2024
voir aussi
$ kioclient --commands
$ gio info -a standard::content-type b_file.ml
uri: file:///home/san/prog/ocaml/bogue/b_file.ml
local path: /home/san/prog/ocaml/bogue/b_file.ml
unix mount: /dev/nvme0n1p6 /home ext4 rw,relatime
attributes:
standard::content-type: text/x-ocaml
https://github.com/mirage/conan
* recentyl used files
.local/share/recently-used.xbel
https://stackoverflow.com/questions/24007459/how-to-query-recent-items-in-mac-os-x
*)moduleAvar=B_avarmoduleButton=B_buttonmoduleDraw=B_drawmoduleEmpty=B_emptymoduleI=B_i18n.FilemoduleLabel=B_labelmoduleL=B_layoutmoduleLong_list=B_long_listmoduleMain=B_mainmodulePopup=B_popupmoduleSelection=B_selectionmoduleSpace=B_spacemoduleStyle=B_stylemoduleSync=B_syncmoduleTable=B_tablemoduleText_input=B_text_inputmoduleTheme=B_thememoduleTrigger=B_triggermoduleUpdate=B_updatemoduleVar=B_varmoduleW=B_widgetopenB_utilsopenTsdltypeentry={name:string;target_stat:Unix.statsoption;(* computed only in case of link, to get the target stats *)stat:Unix.statsoption}letfilenamee=e.nameletlstat_opte=e.statletstat_opte=e.target_stat(* type t = { *)(* dir : string; *)(* data : entry list *)(* } *)let(//)=Filename.concatletfile_statname=trySome(Unix.lstatname)with|_->Noneletis_directorypath=trySys.is_directorypathwith_->falseletentry_sizee=matche.statwith|Somes->s.Unix.st_size|None->-1letentry_mtimee=matche.statwith|None->0.|Somes->s.st_mtime(* for Ocaml < 4.14 *)letinput_lineic=matchStdlib.input_lineicwith|s->Somes|exceptionEnd_of_file->Noneletdummy_stat=Unix.{st_dev=0;(* Device number *)st_ino=0;(* Inode number *)st_kind=S_REG;(* Kind of the file *)st_perm=0;(* Access rights *)st_nlink=0;(* Number of links *)st_uid=0;(* User id of the owner *)st_gid=0;(* Group ID of the file's group *)st_rdev=0;(* Device ID (if special file) *)st_size=0;(* Size in bytes *)st_atime=0.;(* Last access time *)st_mtime=0.;(* Last modification time *)st_ctime=0.;(* Last status change time *)}moduleSSet=Set.Make(String)(* Monitoring changes in a directory. All functions are non-blocking and return
very fast, even if the path is remote or the directory is huge. To achieve
this, monitoring is done in a separate thread and one has to accept a delay
before actual changes to the filesystem are reported. We provide two
implementations, one is based on the external [fswatch] program, and the
other is based only on built-in Ocaml functions (which are probably more
memory and cpu intensive). *)moduletypeMonitor=sigtypetvalpath:t->stringvalstart:?delay:float->?action:(t->unit)->string->t(* [start path] starts monitoring the directory or file [path] and immediately
returns. It is not an error if [path] does not exist or is deleted, see
below. The [delay] parameter is the time interval (in seconds) between
polls. The [action] function is executed for each modification (it might be
a false positive; it should be fast and non blocking (typically just
sending an event). Check [modified] below to get the actual changes.) Thus,
what we call "current" will always mean "not older than delay". The
default delay is 1 second. It may be internally increased if the polls take
too much time. *)valdelay:t->floatvalstop:t->unitvalls:t->stringlist(* [ls m] returns the "old" list of files watched by the monitor [m] when the
last [*modified] function was called. If [m] monitors a directory, [ls m]
is the content of the directory (without "." and ".."), otherwise [ls m] is
"." if the file exists, or [] if not. This function takes advdantage of the
monitoring data to return faster (in general) than rescanning the directory
with [Sys.readdir]. *)valsize:t->intoption(* If [t] monitors a directory, then [size t] is the number of elements of
this directory, before recent modifications. Otherwize, [size t] returns
None. Calling [size t] is equivalent to but faster than [List.length (ls
t)]. *)valmodified:t->stringlist*stringlist*stringlist(* Return three lists of files (or directories) names that have been modified
since last call of this function or of [was_modified]:
list of deleted elements, list of added elements, list of modified elements
File names do not include the directory path. These lists can be equal to
the singleton ["."] in some special cases:
+ if the [path] now exists but did not exist in the previous call to
[*modified], then the [added] list is ["."] and the others are empty (even
if some contents of [path] were modified in the meantime: remember that we
only compare to the previous call to [*modified].)
+ if the [path] existed in the previous call to [*modified] but not
anymore, then the [deleted] list is ["."] and the others are empty.
+ if the [path] existed in the previous call to [*modified], then has
disappered and then reappeared again, the [modified] function will return
[], ["."], [] instead of the explicit difference, telling you that it is
safer to read the contents again (using [ls] for instance). *)valwas_modified:t->bool(* Simply returns true if files were modified since the last call of this
function or of [modified]. The list of modified files cannot be
retrieved. This is (semantically) equivalent to checking if the list
returned by [modified] is empty, but possibly faster. *)end(* The first Monitor that we propose is based on fswatch, which can be installed
on many OSes. https://emcrisostomo.github.io/fswatch/
The fswatch command is run in the background, and we launch a Thread to poll
it regulary (every [delay=1] second, which is fswatch's default latency).
The accuracy of the [*modified] functions has to be taken modulo [delay]:
files modified more recently than [delay] might not be included.
Corner case: at least on my linux machine, when watching a non-existent path,
the creation of the path is detected by fswatch only after the second
modification of the path...
*)moduleFswatch:Monitor=struct(* see also ? https://github.com/kandu/ocaml-fswatch
or ? https://github.com/whitequark/ocaml-inotify *)let_name="fswatch"typet={path:string;id:int;inch:in_channel;outd:Unix.file_descr;mutableexists:bool;mutableupdated:SSet.t;mutablecreated:SSet.t;mutableremoved:SSet.t;mutablestop:bool;mutableold_content:SSet.t;delay:float}letlatency=1.letdot=SSet.singleton"."letpathp=p.pathletdelayp=p.delaylet_test_looppath=letfswatch_command="fswatch --event-flag-separator '|' --event Updated --event Removed \
--event Created --event Renamed --event MovedFrom --event MovedTo --event \
AttributeModified --event OwnerModified -x \""^(Filename.quotepath)^"\""inletin_channel,out_channel=Unix.open_processfswatch_commandinletrecloop()=matchinput_linein_channelwith|None->close_inin_channel;close_outout_channel|Someline->print_endlineline;loop()inloop()letget_initial_contentpath=tryifSys.is_directorypaththenSys.readdirpath|>Array.to_list|>SSet.of_listelsedotwith_->SSet.empty(* On va plutôt utiliser create_process pour pouvoir le tuer quand on n'en a
plus besoin *)letcreate_processdelaypath=letind,outd=Unix.pipe()inlete="--event"inletfswatch_process=Unix.create_process"fswatch"[|"fswatch";"--event-flag-separator";"|";e;"Updated";e;"Removed";e;"Created";e;"Renamed";e;"MovedFrom";e;"MovedTo";e;"AttributeModified";e;"OwnerModified";"--latency";(string_of_float(max0.1delay));"-x";path|]indoutdUnix.stderrinletinch=Unix.in_channel_of_descrindinletold_content=get_initial_contentpathinletexists=Sys.file_existspathinifnotexiststhenprintd(debug_warning+debug_user+debug_io)"Monitoring path [%s] which does not exist (yet)."pathelseprintddebug_io"Monitoring path [%s]."path;{path;id=fswatch_process;inch;outd;delay;exists;updated=SSet.empty;created=SSet.empty;removed=SSet.empty;old_content;stop=false}(* The output of fswatch should be something like:
/tmp/aaa Removed|MovedFrom
The function [parse_event] should return: ("/tmp/aaa", ["Removed"; "MovedFrom"])
*)letparse_eventdir_paths=matchString.rindex_opts' 'with|Somei->letpath=String.subs0iinletpath=ifpath=dir_paththen"."elseFilename.basenamepathinletflags=String.split_on_char'|'(String.subs(i+1)(String.lengths-(i+1)))in(path,flags)|None->s,[](* On my machine re-creation of the path is not detected at first, fswatch
needs another modif to detect it. *)letnew_pathp=printddebug_io"New path [%s] was created and is now monitored."p.path;p.exists<-true;p.removed<-SSet.empty;p.created<-dot;assert(SSet.is_emptyp.updated);assert(SSet.is_emptyp.old_content);p.old_content<-SSet.emptyletupdate_pathp=(* path is updated, if this is a dir we don't care *)ifnot(is_directoryp.path)thenp.updated<-dot(* On my machine [rmdir] (so on an empty dir) is not reported by fswatch,
hence [remove_path] will not be called... Hopefully this only happens when
the dir is empty. But [remove_path] will be called when the dir is
renamed. *)letremove_pathp=printddebug_io"Monitored path [%s] has vanished (moved or deleted)."p.path;p.exists<-false;p.removed<-dot;p.created<-SSet.empty;p.updated<-SSet.empty;p.old_content<-SSet.empty(* This function is blocking when there is no change to the path *)letrecrecord?actionp=ifnotp.stopthenmatch(* printd (debug_thread + debug_io) "Waiting from fswatch input..."; *)input_linep.inchwith|None->printddebug_io"fswatch: Nothing new";ifnotp.stopthenThread.delayp.delay;record?actionp|Someline->printddebug_io"fswatch sent: %s"line;apply_optionactionp;(* typically we use this to send an event *)letfile,flags=parse_eventp.pathlineinprintddebug_io"Path=[%s], File=[%s], Flags=[%s]"p.pathfile(String.concat"; "flags);iffile<>"."&&p.existsthenbeginifSSet.equalp.createddotthenprintddebug_io"Monitord path [%s] was created but not checked \
yet; we don't record modifications untill then."p.pathelseifList.mem"Created"flagsthenbeginifSSet.memfilep.removedthen(p.removed<-SSet.removefilep.removed;p.updated<-SSet.addfilep.updated)elsep.created<-ifSSet.equalp.createddotthenSSet.singletonfileelseSSet.addfilep.createdendelseifList.mem"Removed"flagsthenbeginifSSet.memfilep.createdthen(p.created<-SSet.removefilep.created;p.updated<-SSet.addfilep.updated)else(ifSSet.memfilep.old_contentthenp.removed<-SSet.addfilep.removedelseprintd(debug_warning+debug_io)"File [%s] was reported as removed but we did not record its \
prior existence. Ignoring it."file)endelseifnot(SSet.memfilep.created)thenbeginifSSet.memfilep.old_contentthenp.updated<-SSet.addfilep.updatedelsebeginprintd(debug_warning+debug_io)"File [%s] was reported as updated but we did not record its prior \
existence. Moving it to 'created'."file;p.created<-SSet.addfilep.created;endendendelsebegin(* Here we treat the special case where the path itself is modified. On
my linux machine, when removing the path, instead of "Removed",
fswatch emits "AttributeModified" for a file, and nothing for a
dir. *)ifSys.file_existsp.paththenifp.existsthenupdate_pathpelsenew_pathp(* path was just created *)elseremove_pathp(* path has disappeared *)end;record?actionpelsebeginprintddebug_io"Closing fswatch input channel.";close_inp.inch;printddebug_thread"fswatch thread terminated.";endletstart?(delay=latency)?actionpath=letpath=letb=Filename.basenamepathinifb=Filename.dir_septhenbelseFilename.(dirnamepath//b)in(* this removes possible trailing "/" *)letp=create_process(max0.1delay)pathinlet_th=Thread.create(record?action)pinpletupdatep=p.old_content<-ifSSet.equalp.createddotthenget_initial_contentp.pathelseSSet.diffp.old_contentp.removed|>SSet.(unionp.created);p.updated<-SSet.empty;p.removed<-SSet.empty;p.created<-SSet.emptyletstopp=ifnotp.stopthenbeginprintddebug_io"Stop monitoring [%s]"p.path;p.stop<-true;updatep;tryprintddebug_thread"sending TERM to fswatch process.";Unix.killp.idSys.sigterm;printddebug_thread"sending KILL to fswatch process.";Unix.killp.idSys.sigkill;(* This would block : why? We do it later *)(* print_endline "Closing inch"; *)(* In_channel.close p.inch; *)printddebug_io"Closing fswatch output channel.";Unix.closep.outdwith_->printd(debug_error+debug_io+debug_thread)"Fswatch process not cleanly stopped"endelseprintddebug_io"Fswatch process for [%s] was already stopped."p.path(* Return the list of files that have been modified since last call of this
function, modulo [delay]. *)letmodifiedp=ifp.stopthenprintd(debug_error+debug_io)"File Monitor is already stopped.";letupdated=p.updated|>SSet.elementsinletremoved=p.removed|>SSet.elementsinletcreated=p.created|>SSet.elementsinupdatep;removed,created,updatedletwas_modifiedp=ifp.stopthenprintd(debug_error+debug_io)"File Monitor is already stopped.";letm=notSSet.(is_emptyp.updated&&is_emptyp.created&&is_emptyp.removed)inupdatep;mletlsp=tryifSys.is_directoryp.paththenSSet.elementsp.old_content(* (SSet.diff p.old_content p.removed) *)(* |> SSet.union p.created *)(* |> SSet.elements *)else["."]with_->[]letsizep=tryifSys.is_directoryp.paththenSome(SSet.cardinalp.old_content)else(printd(debug_error+debug_io+debug_user)"[Monitor.size] (Fswatch): [%s] is not a directory"p.path;None)with_->printddebug_io"Monitor (Fswatch): path [%s] cannot be accessed."p.path;Noneend(* of module Fswatch*)(* Test if [fswatch] is available. One could do a more thorough test. *)letfswatch_check()=Theme.use_fswatch&&which"fswatch"<>NonemoduleDiff=structletaddtablei(key,value)=Hashtbl.addtablekey(i,value)letget_optarrayi=ifi>=0&&i<Array.lengtharraythenSome(Array.unsafe_getarrayi)elseNoneletpoptablex=letkey=fstxinmatchHashtbl.find_opttablekeywith|Some(j,v)->Hashtbl.removetablekey;(* print_string (Printf.sprintf " found j=%i." j); *)Some(j,(key,v))|None->(* print_string " not found "; *)Noneletdiffer_fasta1a2=a1<>a2lettable_to_listtable=Hashtbl.fold(funkey_valuelist->key::list)table[](* [diff a1 a2] returns the diff between the arrays a1 and a2 in the form: list
of deleted elements, list of added element, list of modified elements
More precisely, elements of the arrays have the form (data, time); only the
data is returned. Each of these list is returned with no specific
ordering. (expect random)
[deleted] means present in a1 but not in a2.
[added] means present in a2 but not in a1.
[modified] means present both in a1 and a2, but the version in a2 is newer.
The algo used should be efficient for few additions and deletions, when the
common elements are in the same order in a1 and a2. (Roughly linear time
O(N)). It will be suboptimal if the orders of a1 and a2 are reverse from each
other: O(N^2). For random order, an algorithm based on sorting would be
better (O(N log N)).
TODO optimize by playing with [d2] (currently not used); indeed, if only one
file is added or deleted, setting d2= =/- 1 would roughly divide the number
of operations by 2.
*)letdiffa1a2=letsamex1x2=fstx1=fstx2inletnewerx2x1=sndx2>sndx1inletsuppress_maybe=Hashtbl.create16inletadd_maybe=Hashtbl.create16inletmodified=Hashtbl.create16inletrecloopid2=(* print_newline (); *)(* print_string (Printf.sprintf "(%i, %i)" i (i + d2)); *)matchget_opta1i,get_opta2(i+d2)with|Somex1,Somex2whensamex1x2->ifnewerx2x1thenaddmodifiedix2;loop(i+1)d2|None,None->(table_to_listsuppress_maybe,table_to_listadd_maybe,table_to_listmodified)|ox1,ox2->beginmatchox1with|Somex1->beginmatchpopadd_maybex1with|Some(j,y2)->ifnewery2x1then(addmodifiedjy2);|None->addsuppress_maybeix1;end|None->()end;beginmatchox2with|Somex2->beginmatchpopsuppress_maybex2with|Some(j,y1)->ifnewerx2y1then(addmodifiedjx2);|None->addadd_maybeix2;end|None->()end;loop(i+1)d2inloop00letarray_reva=letn=Array.lengthainArray.initn(funi->Array.unsafe_geta(n-i-1))lettest_diff()=lettest(a,b,(x,y,z))=letr,c,u=diffabinassert(List.lengthr=x&&List.lengthc=y&&List.lengthu=z)inletn=1000000inleta=Array.initn(funi->(i,0))initime_it"[diff] no change"test(a,a,(0,0,0));letaa=array_revainitime_it"[diff] no change but reversed"test(a,aa,(0,0,0));leta1=Array.init(n+2)(funi->(i,0))initime_it"[diff] two added at the end"test(a,a1,(0,2,0));leta2=Array.init(n+2)(funi->(i-2,0))initime_it"[diff] two added at the top"test(a,a2,(0,2,0));letb=Array.initn(funi->(i,1))initime_it"[diff] all updated"test(a,b,(0,0,n));end(* The second Monitor does not use any external tool, only [Unix.stat]. *)moduleUnix_stat:Monitor=structtypestat=(string*float)(* Each files is represented by a pair (base name, time stamp). *)typedir=statarraylet_name="unix_stat"typet={path:string;mutableold_time:float;mutablenew_time:float;old_content:dirVar.t;new_content:dirVar.t;mutablestop:bool;mutabledelay:float}letlatency=1.letdelayt=t.delayletpatht=t.pathletget_timepath=letopenUnixintrylets=lstatpathins.st_ctime+.s.st_mtime+.s.st_atimewith_->printd(debug_io+debug_warning)"Monitor: error while getting stats for [%s]."path;0.letreaddirpath=leta=trySys.readdirpathwithSys_error_|Unix.Unix_error_->printd(debug_io+debug_warning)"Monitor: error while reading directory [%s]."path;[||]inArray.map(funfile->file,get_time(path//file))aletstopt=ifnott.stopthenbegint.stop<-true;Var.sett.old_content[||];Var.sett.new_content[||]endletadjust_delaytdt=ifdt>t.delay/.50.thenbegint.delay<-60.*.dt;printddebug_io"Reading directory %s is taking a long time: %f ms. Increasing the \
Monitor delay to %f."t.pathdtt.delay;endelseift.delay>latency&&dt<t.delay/.70.thenbegint.delay<-max1.(dt*.60.);printddebug_io"Setting Monitor delay to %f."t.delayendletget_time_or_zeropath=tryget_timepathwithUnix.Unix_error_|Sys_error_->0.letdaemon?actiont=letcache=ref(Var.gett.new_content)inletrecloop()=ifSys.file_existst.paththenbegin(* We update [new_content] even if old_time = 0. because [readdir] may
take some time, so we don't want to it on demand with [modified]. *)lett1=t.new_timeint.new_time<-get_time_or_zerot.path;ifis_directoryt.paththenletdt=time_it"dt"(Var.sett.new_content)(readdirt.path)inadjust_delaytdt;ift1=0.thenbeginprintddebug_io"New path [%s] was created and is now monitored."t.path;t.old_time<-0.;Var.sett.old_content[||](* Var.set t.old_content (Var.get t.new_content); *)(* t.old_time <- t.new_time *)endendelseift.new_time<>0.then(printddebug_user"Monitored path [%s] has vanished (moved or deleted)."t.path;t.new_time<-0.;Var.sett.new_content[||]);ifnott.stopthenbeginThread.delayt.delay;let()=matchactionwith|None->()|Somef->ifDiff.differ_fast!cache(Var.gett.new_content)thenbegincache:=Var.gett.new_content;ftendinloop()endinignore(Thread.createloop())letstart?(delay=latency)?actionpath:t=letstop=falseinletold_time=get_time_or_zeropathinifold_time=0.thenprintd(debug_warning+debug_user+debug_io)"Monitoring path [%s] which does not exist (yet)."pathelseprintddebug_io"Monitoring path [%s]."path;letc=readdirpathinletold_content=Var.createcinlett={path;old_time;new_time=old_time;old_content;new_content=Var.createc;stop;delay}indaemon?actiont;t(* In this monitor we can return "for free" the timestamps along with the
files. *)letlsstatt=tryifSys.is_directoryt.paththenVar.gett.old_content|>Array.to_listelse[".",t.old_time]with_->printddebug_io"Monitor: path [%s] cannot be accessed."t.path;[]letlst=lsstatt|>List.mapfstletsizet=tryifSys.is_directoryt.paththenSome(Array.length(Var.gett.old_content))else(printd(debug_error+debug_io+debug_user)"[Monitor.size] (Unix_stat): [%s] is not a directory"t.path;None)with_->printddebug_io"Monitor (Unix_stat): path [%s] cannot be accessed."t.path;Noneletmodifiedt=ift.stopthen(printd(debug_error+debug_io)"File Monitor is already stopped.";[],[],[])elsebeginlett0=t.old_timeint.old_time<-t.new_time;ift.new_time=0.&&t0>0.thenbegin(* path has disappeared *)Var.sett.old_content[||];["."],[],[]endelseift0=0.&&t.new_time>0.thenbegin(* path reappeared *)(let@newc=Var.with_protectt.new_contentinVar.sett.old_contentnewc);[],["."],[]endelsebeginmatchVar.protect_fnt.new_content(funnewc->letdel,add,mdf=Diff.diff(Var.gett.old_content)newcinVar.sett.old_contentnewc;del,add,mdf)with|[],[],[]->ift.new_time>t0then[],[],["."]else[],[],[]|x->x(* deleted, added, modified *)endendletwas_modifiedt=ift.stopthen(printd(debug_error+debug_io)"File Monitor is already stopped.";false)elsebeginift.old_time<t.new_timethenbegint.old_time<-t.new_time;Var.sett.old_content(Var.gett.new_content);trueendelseletdel,add,mdf=modifiedtindel<>[]||add<>[]||mdf<>[]endend(* of module Unix_stat *)moduleWatchman=struct(* TODO *)(* https://github.com/facebook/watchman *)(*
san@san-XPS-13-9350 /tmp $ watchman clock /tmp/
{
"version": "4.9.0",
"clock": "c:1730115752:11471:2:40"
}
san@san-XPS-13-9350 /tmp $ watchman since /tmp/ "c:1730115752:11471:2:40"
{
"is_fresh_instance": false,
"version": "4.9.0",
"warning": "opendir(/tmp/snap-private-tmp) -> Permission denied. Marking this portion of the tree deleted\nTo clear this warning, run:\n`watchman watch-del /tmp ; watchman watch-project /tmp`\n",
"files": [
{
"cclock": "c:1730115752:11471:2:2",
"nlink": 2,
"dev": 36,
"ctime": 1730116576,
"new": false,
"mtime": 1730115752,
"gid": 1000,
"mode": 17856,
"size": 120,
"oclock": "c:1730115752:11471:2:41",
"ino": 2427,
"uid": 1000,
"exists": true,
"name": "san-state"
}
],
"clock": "c:1730115752:11471:2:43"
}
*)end(* GIO ?
$ gio monitor /tmp/
*)moduleMonitor=(val(iffswatch_check()thenbeginprintddebug_io"Using Fswatch for file monitoring.";(moduleFswatch)endelsebeginprintddebug_io"Fswatch program not found; using Unix_stat for file monitoring.";(moduleUnix_stat)end):Monitor)lettest_monitor()=(* Adapted from ocaml 5.1*)lettemp_dirprefixsuffix=letrectry_namecounter=letname=Filename.temp_fileprefixsuffixintrySys.removename;Unix.mkdirname0o700;namewithSys_error_ase->ifcounter>=20thenraiseeelsetry_name(counter+1)intry_name0inlettemp_dir=temp_dir"bogue_file"".d"inlett=Monitor.start~delay:1.temp_dirinThread.delay0.2;letfoo=Filename.temp_file~temp_dir"foo-"".txt"inUnix.mkdir(temp_dir//"foo_dir")0o700;Thread.delay1.2;letd,a,m=Monitor.modifiedtinassert(d=[]);assert(List.sortStdlib.comparea=[Filename.basenamefoo;"foo_dir"]);(* note that '_' comes after '-' in alphabetical order *)assert(m=[]);assert(Monitor.patht=Filename.dirnamefoo);Unix.rmdir(temp_dir//"foo_dir");Sys.removefoo;Thread.delay1.2;letd,_a,_m=Monitor.modifiedtinassert(List.sortStdlib.compared=[Filename.basenamefoo;"foo_dir"]);Monitor.stoptmoduleMime=structmoduleImap=Map.Make(String)letget_extname=String.lowercase_ascii(Filename.extensionname)letext2mime_map=letaddmap(k,v)=Imap.addkvmapinList.fold_leftaddImap.emptyExt2mime_data.alistletfrom_exts=(* s = file extension starting with "." *)Imap.find_optsext2mime_map|>Option.map(String.split_on_char'/')letfrom_filenames=from_ext(get_exts)lettype_stringfile=lets=get_extfileindefault(Imap.find_optsext2mime_map)""letfrom_magic_file=()(* TODO *)lettest()=assert(from_filename"MYIMAGE.JPG"=Some["image";"jpeg"])endletregular_fa_namefile=letdefault="file-o"inmatchFilename.extensionfilewith|""->default|ext->matchMime.from_extextwith|None->printddebug_io"Cannot find Mime type for file [%s]."file;default|Someext->beginmatchextwith|"image"::_->"file-image-o"|"video"::_->"file-video-o"|"audio"::_->"file-audio-o"|"application"::rest::_->beginmatchrestwith|"pdf"->"file-pdf-o"|"x-bzip2"|"x-bzip"|"x-gzip"|"zip"->"file-archive-o"|"vnd.ms-excel"->"file-excel-o"|"msword"|"vnd.wordperfect"|"x-abiword"|"vnd.kde.kword"->"file-word-o"|_->defaultend|_->default(* TBC *)endletentry_fa_namee=matche.statwith|None->"question-circle"(* debug *)|Somes->letopenUnixinmatchs.st_kindwith|S_REG->regular_fa_namee.name(* Regular file *)|S_DIR->"folder-o"(* Directory *)|S_CHR->"plug"(* Character device *)|S_BLK->"plug"(* Block device *)|S_LNK->beginmatche.target_statwith|Someswhens.st_kind=S_DIR->"folder-o"|_->"link"(* Symbolic link *)end|S_FIFO->"?"(* Named pipe *)|S_SOCK->"exchange"(*******)(* GUI *)(*******)(* Widgets: search bar (Text_input), files () *)typeoptions={width:intoption;height:int;dirs_first:bool;(* partially implemented *)show_hidden:bool;hide_backup:bool;max_selected:intoption;hide_dirs:bool;only_dirs:bool;select_dir:bool;allow_new:bool;(* allow entering non-existing files in the new_file entry *)default_name:string;breadcrumb:bool;system_icons:bool;open_dirs_on_click:bool;(* this should not be true if select_dir is true *)mimetype:Str.regexpoption;(* check mimetype -- from file extension only *)on_select:((int*int)->unit)option}letset_options?width?(height=400)?(dirs_first=true)?(show_hidden=false)?(hide_backup=false)?max_selected?(hide_dirs=false)?(only_dirs=false)?(select_dir=false)?(allow_new=false)?(default_name="")?(breadcrumb=true)?(system_icons=false)?(open_dirs_on_click=false)?mimetype?on_select()=assert(notselect_dir||notopen_dirs_on_click);{width;height;dirs_first;show_hidden;hide_backup;max_selected;hide_dirs;only_dirs;select_dir;allow_new;default_name;breadcrumb;system_icons;open_dirs_on_click;mimetype;on_select}typedirectory={monitor:Monitor.t;stats_hash:((string,Unix.stats*Unix.statsoption)Hashtbl.t)Var.t;mutabletable:Table.t;mutableentries:entryarray;(* filtered names; used only for generating the final selection *)}typeinput={text_input:W.t;mutablebreadcrumb:(W.t*string)list;mutablelayout:L.t}typemessage={label:W.t;mutableclicked_entry:entryoption}typet={controller:W.t;input:input;message:message;new_file:W.t;name_filter:(string->bool)option;full_filter:(entry->bool)option;layout:L.t;mutabledirectory:directory;options:options}letsize_to_string=letprefixes=[|"o";"Kio";"Mio";"Gio";"Tio"|]infunx->ifx<0then"?"elseletrecloopprefixx=lety=xlsr10inify=0||prefix=5thenx,prefix-1elseloop(prefix+1)yinletx,prefix=loop1xinstring_of_intx^" "^prefixes.(prefix)lettest_size_to_string()=assert(size_to_string(1lsl30)="1 Gio");assert(size_to_string1023="1023 o");assert(size_to_string2049="2 Kio")letis_hiddenname=name<>""&&name.[0]='.'letis_backupname=name<>""&&(name.[String.lengthname-1]='~'||Filename.extensionname=".bak")(*
# let a = [|0;1;2;3;4;5;6;7|];;
val a : int array = [|0; 1; 2; 3; 4; 5; 6; 7|]
# let show = [|false;true;true;false;true;true;false;false|];;
val show : bool array = [|false; true; true; false; true; true; false; false|]
# extract_array a show;;
- : int array = [|1; 2; 4; 5|]
*)letextract_arrayashow=ifArray.lengtha=0then[||]elselet()=assert(Array.lengtha=Array.lengthshow)inletlen=Array.fold_left(funsb->ifbthens+1elses)0showinletfa=Array.makelenArray.(unsafe_geta0)inletrecloopij=ifj<lenthenifArray.unsafe_getshowithen(Array.unsafe_setfaj(Array.unsafe_getai);loop(i+1)(j+1))elseloop(i+1)jinloop00;faletentry_is_directory(e:entry)=matche.statwith|Someswhens.st_kind=S_DIR->true|Someswhens.st_kind=S_LNK->(matche.target_statwith|Sometswhents.st_kind=S_DIR->true|_->false)|_->falseletfind_entryentriesname=array_find_index(fune->e.name=name)entriesletdir_icon_color=Draw.(opaque(find_color"#887a5f"))letfile_icon_color=Draw.(opaque(find_color"#513d34"))letpath_entry_color=Draw.(opaque(find_color"#dbc8a4"))letfg=Draw.(opaquelabel_color)lethidden_color=Draw.(transplabel_color)(* Final table with filtered entries *)letmake_f_table~optionsmessageentries=letheight=round(floatTheme.label_font_size*.1.5)inletfont_size=round(floatTheme.label_font_size*.0.9)inletlength=Array.lengthentriesinletcompicompare=funij->lete1=entries.(i)inlete2=entries.(j)inifnotoptions.dirs_firstthencomparee1e2elsematchentry_is_directorye1,entry_is_directorye2with|false,false|true,true->comparee1e2|true,false->-1|false,true->1inletgeneratej=lete=entries.(j)inletfg=ifis_hiddene.name||is_backupe.namethenhidden_colorelsefginletlabel=W.label~size:font_size~fge.namein(* NE MARCHE PAS, cf Table.make_long_list *)(* W.connect_main label label (\* controller? *\) (fun w _ ev -> *)(* if Trigger.was_double_click () then begin *)(* print_endline "change dir !"; *)(* Trigger.push_var_changed (W.id w) *)(* end) *)(* Trigger.buttons_up *)(* |> W.add_connection label; *)L.resident~h:heightlabelinletopenIinletname_col=Table.{title=tfname;length;rows=generate;compare=Some(compi(fune1e2->String.comparee1.namee2.name));min_width=Some200;align=SomeDraw.Min}inletgenerate_sizej=lete=entries.(j)inlets=ifentry_is_directoryethen""elsesize_to_string(entry_sizee)inletfg=ifis_hiddenentries.(j).namethenhidden_colorelsefginL.resident~h:height(W.label~fg~size:font_sizes)inletsize_col=Table.{title=tfsize;length;rows=generate_size;compare=Some(compi(fune1e2->Int.compare(entry_sizee1)(entry_sizee2)));min_width=Some58;align=SomeDraw.Max}inletgenerate_iconj=lete=entries.(j)inletfg=ifentry_is_directoryethendir_icon_colorelsefile_icon_colorinleticon=W.icon~fg(entry_fa_namee)inL.resident~h:heighticoninleticon_col=Table.{title="";length;rows=generate_icon;compare=Some(compi(fune1e2->String.compare(entry_fa_namee1)(entry_fa_namee2)));min_width=Some16;align=SomeDraw.Max}inletgenerate_modj=lett=entry_mtimeentries.(j)inlettext=ift=0.then"?"elselettm=Unix.localtimetinifUnix.gettimeofday()-.t<86400.(* 60*60*24 number of seconds in 1 day *)thenPrintf.sprintf"%02u:%02u"tm.tm_hourtm.tm_minelsePrintf.sprintf"%04u/%02u/%02u"(1900+tm.tm_year)(tm.tm_mon+1)tm.tm_mdayinL.resident~h:height(W.label~fg~size:font_sizetext)inletmod_col=Table.{title=tfmodified;length;rows=generate_mod;compare=Some(compi(fune1e2->Float.compare(entry_mtimee1)(entry_mtimee2)));min_width=Some80;align=SomeDraw.Min}inTable.create~h:400~row_height:height?max_selected:options.max_selected~on_select:(fun_->Update.pushmessage.label)~on_click:(fun_j->message.clicked_entry<-Someentries.(j))[icon_col;name_col;size_col;mod_col](* The height of 400 will be changed later. We could also directly call
[update_message] from the [on_select] function (there should not be any
circular definition preventing this); the advantage of using the [Update]
mechanism is that in the (admittedly not likely) case of many calls to
[on_select], we ensure that there will be only one call to [update_message]
per frame. *)(* Get the unix stats and save it in [stats_hash] to avoid multiple calls to the
external [stat_os] function involved in [Sys.is_directory] and
[Unix.lstat]. *)letget_statpathnamestats_hash=matchHashtbl.find_optstats_hashnamewith|None->begintryprintddebug_io"Loading stats for file [%s]"name;letstat=letls=Unix.lstat(path//name)inifls.st_kind=S_LNKthenletts=trySome(Unix.stat(path//name))with_->Nonein(ls,ts)else(ls,None)inHashtbl.addstats_hashnamestat;Somestatwith_->(printddebug_io"Cannot access file %s for stats"name;None)end|s->sletentry_from_namepathstats_hashname=letstat,target_stat=matchget_statpathnamestats_hashwith|None->None,None|Some(ls,ts)->Somels,tsin{name;target_stat;stat}letcompare_fndirs_firstcompare=ifdirs_firstthenfune1e2->matchentry_is_directorye1,entry_is_directorye2with|false,false|true,true->comparee1.namee2.name|true,false->-1|false,true->1elsefune1e2->comparee1.namee2.name(* [make_table] constructs the Table.t layout from the file monitor [mon]. It is
designed to work fast even on very large directories (like /usr/bin). It
only shows entries for which [filter] returns true. TODO?: It should also do
well on remote files systems, where Unix.stat can be slow. We could delay
calling stats, but since [filter] applies on stats, we cannot pre-filter the
names; we need to progressively update the table. *)letmake_table~optionsmessagemon?name_filter?full_filterstats_hash=letpath=Monitor.pathmonin(* We first filter with the fast [name_filter]. *)letnames=Monitor.lsmon|>opt_map(map_optionname_filterList.filter)in(* Now we use the full filter ; TODO move this to Thread if slow, and use
[Var.protect_fn stats_hash...] *)letentry_list=matchfull_filterwith|None->List.map(entry_from_namepathstats_hash)names|Somef->List.filter_map(funname->lete=entry_from_namepathstats_hashnameiniffethenSomeeelseNone)namesin(* We sort the entry list alphabetically and save it in an array for faster
access. *)letentries=List.sort(compare_fnoptions.dirs_firstString.compare)entry_list|>Array.of_listinletfinalize_=()in(* TODO *)(* (\* We will finish populating the stats in a separate Thread *\) *)(* let finalize t = *)(* printd debug_thread *)(* "Starting file dialog [finalize] for [%s]..." (Monitor.path t.monitor); *)(* for i = 0 to length - 1 do *)(* ignore (get_stats i); *)(* done; *)(* print_endline "DONE!"; *)(* let table = Table.create ~h:400 ~row_height:height [name_col; size_col] in *)(* t.table <- table; *)(* Table.refresh table; *)(* printd debug_thread "End of file dialog [finalize]." *)(* in *)lettable=make_f_table~optionsmessageentriesin(* let height_fn _ = Some 30 in *)(* let long = Long_list.create ~w:150 ~h:400 ~generate ~height_fn *)(* ~length () in *)table,entries,finalize(* , long *)letget_table_layoutt=Table.get_layout(t.directory.table)(* Dichotomy search. Returns immediately if x is the first or last element. *)letfind_index_sorted?first?lastaxmy_compare=letopenArrayinletrecloopi1i2=ifmy_comparex(unsafe_getai1)=0thenSomei1elseifmy_comparex(unsafe_getai2)=0thenSomei2elseifi1=i2||i1+1=i2thenNoneelseletmid=(i1+i2)/2inifmy_comparex(unsafe_getamid)>0thenloopmidi2elseloopi1midinloop(defaultfirst0)(defaultlast(Array.lengtha-1))lettest_find_index_sorted()=letc=compareinleta=Array.init100(funi->i+10)inassert(find_index_sorteda15c=Some5);assert(find_index_sorted~first:6~last:50a15c=None);assert(find_index_sorted~first:50~last:70a60c=Some50)(* The elements of [sub] must be a subset of [a]. Both must be sorted according
to the [compare] function. Then [sorted_subarray_to_selection a sub compare]
returns the Selection.t variable corresponding to the indices of the elements
of [sub] within [a]. *)letsorted_subarray_to_selectionasubcompare=letuget=Array.unsafe_getinletrecsubloopsminsmaxaminamax=ifsmin>smaxthen[]elseifsmax-smin=amax-aminthen[Selection.Range(amin,amax)]elseletfind~first~lastj=lete=ugetsubjinmatchfind_index_sorted~first~lastaecomparewith|Somei->i|None->printddebug_error"[sorted_subarray_to_selection]: cannot find entry %i of [sub] inside [a]. Aborting now."j;raiseNot_foundinleti1=find~first:amin~last:amaxsmininleti2=find~first:amin~last:amaxsmaxinifi1=i2||i1+1=i2then[Selection.Range(i1,i2)]elseletrecloopj=ifsmin+j>smax||compare(ugetsub(smin+j))(ugeta(i1+j))<>0thenj-1elseloop(j+1)inletj1=loop1in(* first (smallest) sel index after smin for which
a.(i1+j) and sub.(smin+j) are equal. *)(* print_endline ("Select" ^ Selection.sprint [Selection.Range (i1, i1 + j1)]); *)(Selection.Range(i1,i1+j1))::subloop(smin+j1+1)smax(i1+j1+1)i2insubloop0(Array.lengthsub-1)0(Array.lengtha-1)lettest_sorted_subarray_to_selection()=leta=[|"a";"b";"c";"d";"e";"f";"g";"h"|]inletsub=[|"a";"b";"e";"g";"h"|]inletsel=sorted_subarray_to_selectionasubcompareinSelection.sprintsel|>print_endline;assert(sel=Selection.[Range(0,1);Range(4,4);Range(6,7)]);letsub=[|"b";"d";"f";"h"|]inletsel=sorted_subarray_to_selectionasubcompareinSelection.sprintsel|>print_endline;assert(sel=Selection.[Range(1,1);Range(3,3);Range(5,5);Range(7,7)]);letsel=sorted_subarray_to_selectionaacompareinSelection.sprintsel|>print_endline;assert(sel=Selection.[Range(0,7)])letselected_entriesentriessel=Selection.fold(funilist->entries.(i)::list)sel[]|>List.rev(* TODO: tail modulo cons *)letinstall_new_tableold_tablenew_table=L.setxnew_table(L.getxold_table);L.setynew_table(L.getyold_table);letw,h=L.get_sizeold_tableinL.set_sizenew_table~w~h;ifL.replace_roomold_table~by:new_tablethen(* We reinstall the resize functions: *)L.resize_keep_marginsnew_tableelseprintd(debug_error+debug_io)"Error installing new table for file dialog"(* L.retower ~duration:0 ~margins:0 t.layout; *)(* B_sync.push (fun () -> Trigger.push_from_id Sdl.Event.mouse_motion 0) *)(* Trigger.push_mouse_focus (-1) *)(* If the list of files becomes smaller then the layout, there will be some
blank space below. We could shrink the layout size using: *)(* Sync.push (fun () -> L.fit_content ~sep:0 layout); *)(* But this does not seem to be the usual behaviour amongst file dialogs out
there.*)(* For debugging: *)letprint_entrieslist=Array.iteri(funie->print_endline(Printf.sprintf"%i : %s%s"ie.name(ifentry_is_directoryethen" (d)"else"")))list(* In this function [update_table] we create a new Table.t if the path content
was modified, but the monitor was not changed (same directory). We transfer
the scrollbar position, the choice of sorted column, and the file selection,
from the old table to the new. We also delete the obsolete entries from the
[stats_hash] table. For modified files, this will force a new invocation of
Unix.stats. *)(* TODO? if there are only modified files (no deletion or creation) it's not
necessary to recreate the table, we could update the individual widgets by
keeping a Weay.array of visible widgets, populated by the "generate"
functions. *)letupdate_table?(force=false)t=printddebug_io"[File.update_table].";letd=t.directoryinletmon=d.monitorinletdl,ad,md=Monitor.modifiedmoninifforce||dl<>[]||ad<>[]||md<>[]thenbeginprintddebug_io"Table needs to be udpated.";letpath=Monitor.pathmoninletstats_hash=Var.getd.stats_hashin(* lock ? *)letcomp=compare_fnt.options.dirs_firstString.comparein(* remove [dl] (deleted files) from old selection: *)letdl_sub=List.map(entry_from_namepathstats_hash)dl|>Array.of_listinArray.(sortcompdl_sub);(* print_entries d.entries; print_newline (); *)(* print_entries dl_sub; *)letdl_sel=sorted_subarray_to_selectiond.entriesdl_subcompinprintddebug_io"Deleted entries: %s"(Selection.sprintdl_sel);letsel=Selection.minus(Table.get_selectiond.table)dl_selinprintddebug_io"remaining selection: %s."(Selection.sprintsel);(* Now we can update Hash table: *)List.iter(Hashtbl.removestats_hash)dl;List.iter(Hashtbl.removestats_hash)md;(* Construct the new table: *)lettable2_t,entries,_finalize=make_table~options:t.optionst.messagemon?name_filter:t.name_filter?full_filter:t.full_filterstats_hashin(* 1. Restore the updated selection: *)letselected_files=Array.of_list(selected_entriesd.entriessel)inletsel2=sorted_subarray_to_selectionentriesselected_filescompinprintddebug_io"Restoring selection = %s."(Selection.sprintsel2);Table.set_selectiontable2_tsel2;Update.pusht.message.label;(* 2. Restore choice of sorted column. Warning, this regenerates the
table. Hence the selection must be updated before. *)do_option(Table.get_sorted_columnd.table)(fun(i,reverse)->Table.sort_columntable2_t~reversei);(* Reinstall the new table in the layout: *)install_new_table(get_table_layoutt)(Table.get_layouttable2_t);(* 3. Restore scroll value. Warning, this depends on the size of the layout
(the mapping between the long_table offset and the slider position
depends on the height of the room). *)letscroll=Table.get_scrolld.tableinTable.set_scrolltable2_tscroll;L.update_current_geom(Table.get_layouttable2_t);d.table<-table2_t;d.entries<-entriesendelseprintddebug_io"Table does not need to be updated"letget_layout(t:t)=t.layoutletget_selected_entriest=letsel=Table.get_selectiont.directory.tableinletentries=t.directory.entriesinselected_entriesentriesselletget_selectedt=ift.options.allow_new&&t.options.max_selected=Some1then[W.get_textt.new_file]elseget_selected_entriest|>List.map(fune->e.name)letset_selectiontsel=Table.set_selectiont.directory.tableselletbasedirt=Monitor.patht.directory.monitorletstart_monitorcontrollerpath=letaction_=Update.pushcontrollerinMonitor.start~actionpathletpath_selectortext=letti=W.text_input~prompt:I.(tfenter_path)~text()inText_input.last(W.get_text_inputti);tiletnew_directorycontrollermessage~options?full_filter?name_filterpath=letmonitor=start_monitorcontrollerpathinletstats_hash=Var.create(Hashtbl.create(default(Monitor.sizemonitor)100))inlettable,entries,_finalize=make_table~optionsmessagemonitor?name_filter?full_filter(Var.getstats_hash)in{monitor;stats_hash;table;entries}letpluralx=ifx>1then"s"else""letpluralyx=ifx>1then"ies"else"y"letupdate_messaget=printddebug_io"[File.update_message]";letentries=get_selected_entriestinletn_files,n_dirs=List.fold_left(fun(f,d)e->ifentry_is_directoryethen(f,d+1)else(f+1,d))(0,0)entriesinlettext=beginletopenIinmatchn_files,n_dirswith|0,0->tfno_selection|0,1->tfone_dir_selected|0,d->(tfx_dirs_selected)d|1,0->tfone_file_selected|f,0->(tfx_files_selected)f|f,d->(tfx_files_x_dirs_selected)fdendinW.set_textt.message.labeltext;ifn_files+n_dirs=1thenbeginif(n_files=1&¬t.options.select_dir)||(n_dirs=1&&t.options.select_dir)thenW.set_textt.new_file(lete=List.hdentriesine.name)end;n_dirs,n_filesletbreadcrumbpath=letrecloopaccp=letb=Filename.basenamepinifb=pthenb::accelseloop(p::acc)(Filename.dirnamep)inletsplit=loop[]pathinList.map(funp->W.button(Filename.basenamep),p)splitletinstall_new_directorytpath=printddebug_io"Installing new directory [%s]"path;Monitor.stopt.directory.monitor;letd=new_directory~options:t.optionst.controllert.message?name_filter:t.name_filter?full_filter:t.full_filterpathinletold_table=get_table_layouttint.directory<-d;Update.pusht.message.label;install_new_tableold_table(Table.get_layoutd.table)letget_selected_dirt=matchget_selected_entriestwith|[]->printddebug_error"[File.open_new_dir]: no directory selected!";None|list->lete=matchlistwith|[]->failwith"[open_new_dir] should not happen"|[e]->e|e::_->printddebug_error"[File.open_new_dir]: only one path should be selected";einifentry_is_directoryethenSomee.nameelse(printddebug_io"Selected entry [%s] is not a directory."e.name;None)letvalidate_new_file_inputtti=letname=W.get_texttiinifname<>""thenbeginmatchfind_entryt.directory.entriesnamewith|None->ifget_selected_entriest<>[]thenset_selectiont[]|Somei->(* if possible, we add it to the selection. *)ift.options.select_dir||not(entry_is_directory(t.directory.entries.(i)))thenletsel=Selection.range(i,i)inifTable.get_selectiont.directory.table<>selthenset_selectiontselendletopen_dirtpath=t.message.clicked_entry<-None;install_new_directorytpath;ift.options.select_dirthenW.set_textt.new_file""elseift.options.max_selected=Some1thenvalidate_new_file_inputtt.new_file;Update.pusht.input.text_inputletconnect_breadcrumbt=List.iter(fun(b,path)->W.on_clickb~click:(fun_->open_dirtpath))t.input.breadcrumbletupdate_inputt=letpath=Monitor.patht.directory.monitorinprintddebug_io"Updating file dialog path= [%s]."path;W.set_textt.input.text_inputpath;Text_input.last(W.get_text_inputt.input.text_input);t.input.breadcrumb<-breadcrumbpath;letlayout=L.flat_of_w~sep:0(List.mapfstt.input.breadcrumb)inletx,y=L.getxt.input.layout,L.getyt.input.layoutinletrsz=t.input.layout.resizeinletshow=L.is_shownt.input.layoutinL.setxlayoutx;(* used only for temporary animations *)L.setylayouty;ifL.replace_room~by:layoutt.input.layoutthenprintddebug_board"File dialog: installing new input %s"(L.sprint_idlayout)elseprintd(debug_board+debug_error)"File dialog: cannot install new input %s"(L.sprint_idlayout);layout.resize<-rsz;ifnotshowthenL.hide~duration:0layout;Sync.push(fun()->L.resizelayout);(* : TODO find a way to not wait for Sync? e have to make sure layout is
correctly installed before asking for its size. *)t.input.layout<-layout;(* do_option (L.containing_widget t.input.text_input) (L.hide ~duration:0); *)connect_breadcrumbtletopen_selected_dirt=printddebug_io"[File.open_selected_dir].";do_option(get_selected_dirt)(funname->letpath=Monitor.patht.directory.monitor//nameinopen_dirtpath)letopen_clicked_dirt=do_option(t.message.clicked_entry)(fune->ifentry_is_directoryethenletpath=Monitor.patht.directory.monitor//(e.name)inopen_dirtpath)letvalidate_text_inputtti=letold_path=Monitor.patht.directory.monitorinletpath=W.get_texttiinifpath<>old_paththenbeginifis_directorypaththenbegininstall_new_directorytpath;ift.options.max_selected=Some1thenvalidate_new_file_inputtt.new_file;update_inputtendelseletdir=Filename.dirnamepathinifis_directorydirthenbegin(* We use the "basename" part to automatically select the file *)install_new_directorytdir;W.set_texttidir;W.set_textt.new_file(Filename.basenamepath);validate_new_file_inputtt.new_file;update_inputtendelseW.set_textti(Monitor.patht.directory.monitor)end(* If the user enters a valid dir and presses RETURN, we change to it. If it is
a "dir/bla" when dir is valid, we switch to dir and set bla to the new_file
input. *)letconnect_text_inputt=leton_key_down=funti_ev->ifSdl.Event.(getevkeyboard_keycode)=Sdl.K.returnthenvalidate_text_inputttielseifSdl.Event.(getevkeyboard_keycode)=Sdl.K.escapethenW.set_textti(Monitor.patht.directory.monitor)inletti=t.input.text_inputinW.connect_maintition_key_down[Sdl.Event.key_down]|>W.add_connectionti(* let path_selector_combo path = *)(* L.superpose *)letbg1=L.style_bgStyle.(of_bg(gradient~angle:90.[(Draw.(opaque(paleButton.color_off)));path_entry_color])|>with_border(mk_border~radius:5(mk_line~width:0())))letbg_over=Some(Style.opaque_bgDraw.grey)letshowroom=L.show~duration:0room;L.fade_inroom(* We create a combo layout which superposes a text input (for entering path)
and a "breadcrumb" with a list of buttons that allows to select parent
directories by directly clicking. There is an "edit/check" button that allows
to switch between both. Note that we have a cycle of connections: clicking on
the breadcrumb updates the file table, and clicking on a directory in the
file table updates the breadcrumb. *)letmake_input_layoutwinput=letpath_label=L.resident~name:"path_label"~background:bg1input.text_inputinletok=W.button~kind:Button.Switch~bg_over(* ~border_radius:12 *)~action:(funb->ifbthen(showpath_label;L.fade_out~hide:trueinput.layout)else(L.fade_out~hide:truepath_label;showinput.layout))~label_on:(Label.icon"check")~label_off:(Label.icon"edit")""inletlok=L.resident~name:"ok"okinL.setxlok(w-L.widthlok);L.set_heightlok(L.heightinput.layout);(* Bottom align: *)L.setylok(L.heightinput.layout-L.heightlok);L.setypath_label(L.heightinput.layout-L.heightpath_label);letcontainer=L.superpose~w[input.layout;path_label;lok]inL.set_clipcontainer;path_label.resize<-(letopenL.Resizeinfun(w,_h)->set_widthpath_label(w-L.widthlok));path_label.resize(w,0);lok.resize<-(letopenL.Resizeinfun(w,_h)->setxlok(w-L.widthlok));L.hide~duration:0path_label;input.layout.resize<-(fun(w,_h)->letdx=L.widthinput.layout-w+L.widthlokin(* dx > 0 when the input layout is too wide *)letx0=L.getxinput.layoutinletx1=imin0(-dx)in(* print_endline (Printf.sprintf "dx=%i ==> %i x0=%i x1=%i" dx (imin 0 (-dx)) x0 x1); *)ifx0<>x1thenL.animate_xinput.layout(Avar.fromtox0x1)elseL.stop_posinput.layout;(* setx input.layout () *));container,ok(* This is the layout where the user can enter the file name (for saving, in
general, but it also works for selecting an existing file).
* It is displayed only if one file or dir must be chosen.
* It is updated when the user clicks on a file (if a file should be
selected) or a directory (if a dir should be selected).
* When the name is modified, the selection disappears,
except if the entered text matches an existing file:
then that file is automatically selected. *)letnew_file_layout~labelname=letlabel=W.labellabelinletinp=W.text_input~text:name()inletbg=Style.(of_border(mk_border~radius:5(mk_line~width:1()))|>with_bg(color_bgDraw.(opaquewhite)))|>L.style_bginletinp_l=L.resident~background:bginpinletroom=L.flat~vmargin:0~resize:L.Resize.Disable~align:Draw.Center~sep:10[L.residentlabel;inp_l]inL.resize_keep_marginsinp_l;room,inp(* TODO add connections, etc... *)(* We construct the main layout. Note that the table layout and the breadcrumb
layout are both self-destructing; in order to avoid circular definitions we
use a controller, see the tutorial:
https://sanette.github.io/bogue-tutorials/bogue-tutorials/modif_parent.html *)letdialog?full_filter?optionspath=letpath=ifFilename.is_relativepaththenifpath=""||path=Filename.current_dir_namethenSys.getcwd()elseSys.getcwd()//pathelsepathinletoptions=defaultoptions(set_options())inletcontroller=W.empty~w:0~h:0()inletmessage_label=W.label~fg:hidden_color"No selection"inletmessage={label=message_label;clicked_entry=None}inletlabel=I.(tfname)^" :"inletnew_file_room,new_file=new_file_layout~labeloptions.default_nameinletname_filtername=(options.show_hidden||not(is_hiddenname))&&(notoptions.hide_backup||not(is_backupname))inletfull_filter=matchoptions.mimetypewith|None->full_filter|Somereg->letfe=entry_is_directorye||Str.string_matchreg(Mime.type_stringe.name)0inmatchfull_filterwith|None->Somef|Someff->Some(fune->ffe&&fe)inletdirectory=new_directory~optionscontrollermessage?full_filter~name_filterpathinletpath=Monitor.pathdirectory.monitorinlettext_input=path_selectorpathinletw=L.width(Table.get_layoutdirectory.table)in(* let _background = L.style_bg *)(* Style.(of_bg (color_bg path_entry_color) *)(* |> with_border (mk_border ~radius:12 (mk_line ()))) in *)letariane=breadcrumbpathinletinput={text_input;breadcrumb=ariane;layout=L.flat_of_w~sep:0(List.mapfstariane)}inletpath_selector_combo,combo_btn=make_input_layoutwinputinlettable_room=Table.get_layoutdirectory.tableinletopen_button=W.buttonI.(tfopen_dir)inletopen_btn_room=ifoptions.open_dirs_on_clickthenL.empty~name:"empty"~w~h:0()elseL.residentopen_buttoninletmessage_room=L.resident~name:"message"~wmessage.labelinletlayout=L.tower~resize:L.Resize.Disable~name:"file_dialog"~vmargin:0~hmargin:0~sep:10(List.filter_map(funx->x)[Some(L.resident~name:"controller"controller);Somepath_selector_combo;Sometable_room;Someopen_btn_room;(* If we want to try some tower resize strategies, don't
put this at the end because when not displayed it will
prevent the tower resizing function from working. *)ifoptions.max_selected=Some1thenSomenew_file_roomelseNone;Somemessage_room])inL.resize_keep_marginstable_room;L.resize_follow_widthpath_selector_combo;L.resize_follow_widthmessage_room;L.resize_follow_widthnew_file_room;Space.keep_bottom_sync~reset_scaling:falsenew_file_room;Space.keep_bottom_sync~reset_scaling:trueopen_btn_room;Space.keep_bottom_sync~reset_scaling:falsemessage_room;L.set_sizelayout?w:options.width~h:options.height;lett={controller;input;message;new_file;name_filter=Somename_filter;full_filter;layout;directory;options}inEmpty.on_unload(W.get_emptycontroller)(fun()->Monitor.stopt.directory.monitor);(* let _ = Thread.create finalize t in *)letopen_button_action_w__=open_selected_dirtinW.connect_mainopen_buttoncontrolleropen_button_actionTrigger.buttons_down|>W.add_connectionopen_button;connect_text_inputt;W.connect_maincontrollercontroller(fun___->update_tablet)[Trigger.update](* We could use [W.connect] here to launch the update in a different thread,
provided we use Sync.push for installing the new layout and scroll. *)|>W.add_connectioncontroller;W.connect_maintext_inputtext_input(fun___->update_inputt)[Trigger.update]|>W.add_connectiontext_input;W.on_button_releasecombo_btn~release:(funb->ifnot(W.get_stateb)thenvalidate_text_inputttext_input(* let path = W.get_text text_input in *)(* if is_directory path then (install_new_directory t path; update_input t) *)(* else W.set_text text_input (Monitor.path t.directory.monitor) *));(* Examine the new selection and react. *)W.connect_mainmessage.labelopen_button(fun__btn_->letn_dirs,n_files=update_messagetinapply_optiont.options.on_select(n_dirs,n_files);ifTrigger.mouse_left_button_pressed()&&n_dirs>=1&&t.options.open_dirs_on_clickthenopen_clicked_dirtelseifnott.options.open_dirs_on_clickthenbeginifn_dirs=1&&n_files=0thenL.(showopen_btn_room)elseL.(hideopen_btn_room)end)[Trigger.update]|>W.add_connectionmessage.label;W.connect_mainnew_filenew_file(funti__->validate_new_file_inputtti)Text_input.triggers|>W.add_connectionnew_file;connect_breadcrumbt;Update.pushmessage.label;t(* Return label and max_selected *)letget_label2?n_dirs?n_files()=letopenIinmatchn_dirs,n_fileswith|Some0,Some0->tfcontinue,Some0|Some1,Some0->tfselect_directory,Some1|Some0,Some1->tfselect_file,Some1|_,Some0->tfselect_dirs,n_dirs|Some0,_->tfselect_files,n_files|None,_|_,None->tfselect,None|Somed,Somef->tfselect,Some(d+f)letselect_popup?dst?board?w?hpath?n_files?n_dirs?mimetype?name?(allow_new=false)?button_labelcontinue=letselect_one=(n_files=Some1&&n_dirs=Some0)||(n_dirs=Some0&&n_files=Some1)in(* allow_new=true should be set only for selecting ONE file or ONE dir *)ifallow_newthenassertselect_one;letw,h=matchdstwith|Somedst->letw0,h0=L.widthdst-4*Theme.room_margin,L.heightdst-4*Theme.room_margininletwidth=matchwwith|Somew->iminww0|None->w0inletheight=matchhwith|Someh->iminhh0|None->h0inSomewidth,Someheight|None->w,hinletsel_dirs,sel_files=ref0,ref0inleton_select(n_d,n_f)=sel_dirs:=n_d;sel_files:=n_finletlabel2,max_selected=get_label2?n_dirs?n_files()inletlabel2=defaultbutton_labellabel2inletoptions=set_options(* ~width ~height *)~on_select~default_name:(defaultname"")~select_dir:(n_dirs=Some1&&n_files=Some0)~open_dirs_on_click:(n_dirs=Some0)~allow_new~hide_backup:true?max_selected?mimetype()inletfd=dialog~optionspathin(* New file name should not be an existing dir *)letok_new_filename=matchfind_entryfd.directory.entriesnamewith|None->true|Somei->not(entry_is_directory(fd.directory.entries.(i)))inletenablebtn2b2=letok=((n_dirs=None&&!sel_dirs>0)||Some!sel_dirs=n_dirs)&&((n_files=None&&!sel_files>0)||Some!sel_files=n_files)inletok=ok||(allow_new&&letname=W.get_text(fd.new_file)inname<>""&&(n_dirs=Some1||ok_new_filename))inprintddebug_io"File dialog enabling select button: %b"ok;(* print "[%s] sle_dirs=%i, sel_files=%i, ok=%b" *)(* (W.get_text (fd.new_file)) !sel_dirs !sel_files ok; *)ifokthenbegin(* if allow_new then sel_dirs and sel_files must be 0. The b2 label never changes. *)ifnotallow_new&&!sel_dirs+!sel_files>0(* always true unless n_dirs = n_files = Some 0 *)thenW.set_textbtn2(fst(get_label2~n_dirs:!sel_dirs~n_files:!sel_files()));ifb2.L.disabledthen(L.fade_in~from_alpha:0.5~to_alpha:1.b2;L.enableb2)endelsebeginL.fade_out~to_alpha:0.5b2;L.disableb2;W.set_textbtn2label2endin(* The button2 can be enabled/disabled either by a change of selection, or a
change of the new_file text_input. *)letconnect2tbtn2=do_option(L.containing_widgetbtn2)(funb2->enablebtn2b2;(* We update the "Select" button when the status message changes. *)letc=W.connect_maint.message.labelbtn2(fun___->enablebtn2b2)[Trigger.update]inW.add_connectiont.message.labelc;ifselect_onethenbegin(* If we press ENTER on the new_file input, we simulate pressing the
"Select" button. *)letc=W.connect_maint.new_filebtn2(fun_bev->ifnotb2.L.disabled&&Sdl.Event.(getevkeyboard_keycode)=Sdl.K.returnthen(printddebug_board"[File.dialog] Simulate button up.";W.wake_up_allTrigger.(create_eventE.mouse_button_up)b))Sdl.Event.[key_up]inW.add_connectiont.new_filec;letc=W.connect_maint.new_filebtn2(fun_bev->ifnotb2.L.disabled&&Sdl.Event.(getevkeyboard_keycode)=Sdl.K.returnthen(printddebug_board"[File.dialog] Simulate button down.";W.wake_up_allTrigger.(create_eventE.mouse_button_down)b))Sdl.Event.[key_down]inW.add_connectiont.new_filecend;ifallow_new(* We enable the "Select" button if the user enters a string in the
"new_file" input. *)thenletc=W.connect_maint.new_filebtn2(fun___->enablebtn2b2)Sdl.Event.[key_up]inW.add_connectiont.new_filec)inletbg=Style.SolidDraw.(opaquebg_color)inPopup.two_buttons?dst~bg?board?w?h~label1:I.(tfcancel)~label2~action1:(fun()->Monitor.stopfd.directory.monitor)~action2:(fun()->Monitor.stopfd.directory.monitor;continue(List.map(Filename.concat(basedirfd))(get_selectedfd)))~connect2:(connect2fd)fd.layoutletselect_file?dst?board?w?h?mimetype?namepathcontinue=select_popup?dst?board?w?h?mimetype?namepath~n_files:1~n_dirs:0(funlist->continue(List.hdlist))letselect_files?dst?board?w?h?mimetype?n_filespathcontinue=select_popup?dst?board?w?h?mimetypepath?n_files~n_dirs:0continue(* FIXME when selecting all with CTRL-A, if the selection contains a unique dir,
this dir is immediately opened, this should not happen. **)letselect_dir?dst?board?w?h?namepathcontinue=select_popup?dst?board?w?h?namepath~n_files:0~n_dirs:1(funlist->continue(List.hdlist))letselect_dirs?dst?board?w?h?n_dirspathcontinue=select_popup?dst?board?w?hpath~n_files:0?n_dirscontinueletsave_as?dst?board?w?h?namepathcontinue=select_popup?dst?board?w?h?namepath~n_files:1~n_dirs:0~allow_new:true~button_label:I.(tfsave)(funlist->continue(List.hdlist))(* Not used any more. Use [select_file] with no [dst] option instead. *)(* let select_file_new_window ?board ?w ?(h=400) path _continue = *)(* let options = set_options ?width:w ~height:h ~open_dirs_on_click:true *)(* ~hide_backup:true ~max_selected:1 () in *)(* let fd = dialog ~options path in *)(* let m = Theme.room_margin in *)(* L.setx fd.layout m; *)(* L.sety fd.layout m; *)(* let frame = L.superpose [ fd.layout ] *)(* ~w:(L.width fd.layout + 2 * m) *)(* ~h:(L.height fd.layout + 2 * m) in *)(* L.resize_keep_margins fd.layout; *)(* match board with (\* etc. à faire dans Popup, use [continue]... *\) *)(* | Some b -> ignore (Main.add_window b ((\* L.cover ~name:"file-dialog cover" *\) frame)) *)(* | None -> L.add_window fd.layout *)let(let@)dialogf=dialogf(* same as Utils.( let@ ) *)(* let () = let@ file = select_file "essai" in *)(* print_endline file *)