char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7b01146
main
1use std:: collections:: VecDeque ; 2use std:: io::{ self , IsTerminal , Write }; 3use std:: sync:: atomic::{ AtomicU64 , Ordering }; 4use std:: sync::{ Arc , mpsc}; 5use std:: thread; 6 7use avril_runtime:: registry::{ ProgressEvent , ProgressSink }; 8 9const CAPACITY : usize =128 ; 10 11enum Message { 12Event { generation : u64 , line : String }, 13Flush ( mpsc:: SyncSender <()>), 14Stop , 15} 16 17pub ( crate ) struct Reporter { 18sender : mpsc:: SyncSender < Message >, 19// Odd generations accept events; even generations belong to the input prompt. 20generation : Arc < AtomicU64 >, 21thread : Option < thread:: JoinHandle <()>>, 22} 23 24impl Reporter { 25pub ( crate ) fn terminal () ->Option < Self > { 26if !io:: stderr (). is_terminal () || std:: env:: var ( "TERM" ). is_ok_and ( |term| term =="dumb" ) { 27return None ; 28} 29Self :: start ( io:: stderr (), avril_ext_console:: colors ( true )). ok () 30} 31 32fn start ( mut output : impl Write +Send +' static , colors : bool ) -> io:: Result < Self > { 33let ( sender, receiver) = mpsc:: sync_channel ( CAPACITY ); 34let generation =Arc :: new ( AtomicU64 :: new ( 1 )); 35let current = generation. clone (); 36let thread = thread:: Builder :: new () 37. name ( "avril:progress" . into ()) 38. spawn ( move ||{ 39let mut recent =VecDeque :: new (); 40let mut seen_generation =0 ; 41let mut failed =false ; 42while let Ok ( message) = receiver. recv () { 43match message{ 44Message :: Event { generation, line} =>{ 45if failed || current. load ( Ordering :: Acquire ) != generation{ 46continue ; 47} 48if generation != seen_generation{ 49 recent. clear (); 50 seen_generation = generation; 51} 52if recent. contains ( & line) { 53continue ; 54} 55if recent. len () ==CAPACITY { 56 recent. pop_front (); 57} 58 recent. push_back ( line. clone ()); 59let line =color_action ( line, colors); 60// Never hold the terminal lock while waiting for the next event. 61 failed = output 62. write_all ( format! ( "{line}\n" ). as_bytes ()) 63. and_then ( |() | output. flush ()) 64. is_err (); 65} 66Message :: Flush ( done) =>{ 67let _ = done. send (()); 68} 69Message :: Stop =>break , 70} 71} 72}) ?; 73Ok ( Self { 74 sender, 75 generation, 76thread : Some ( thread), 77}) 78} 79 80pub ( crate ) fn sink ( & self , registry : url:: Url ) ->ProgressSink { 81let sender =self . sender . clone (); 82let current =self . generation . clone (); 83Arc :: new ( move |event|{ 84let generation = current. load ( Ordering :: Acquire ); 85if generation& 1 ==0 { 86return ; 87} 88if let Some ( line) =label ( event, & registry) { 89let _ = sender. try_send ( Message :: Event { generation, line}); 90} 91}) 92} 93 94pub ( crate ) fn scope ( & self , active : bool ) { 95let current =self . generation . load ( Ordering :: Acquire ); 96if ( current& 1 !=0 ) != active{ 97self . generation . fetch_add ( 1 , Ordering :: AcqRel ); 98} 99if !active{ 100self . flush (); 101} 102} 103 104pub ( crate ) fn flush ( & self ) { 105let ( done, wait) = mpsc:: sync_channel ( 0 ); 106if self . sender . send ( Message :: Flush ( done)). is_ok () { 107let _ = wait. recv (); 108} 109} 110} 111 112impl Drop for Reporter { 113fn drop ( & mut self ) { 114self . scope ( false ); 115// Store-owned sink clones may outlive the reporter, so channel EOF is not shutdown. 116let _ =self . sender . send ( Message :: Stop ); 117if let Some ( thread) =self . thread . take () { 118let _ = thread. join (); 119} 120} 121} 122 123pub ( crate ) fn color_action ( line : String , colors : bool ) ->String { 124if !colors{ 125return line; 126} 127let end = line. find ( ' ' ). unwrap_or ( line. len ()); 128format! ( "\x1b[1;36m{}\x1b[0m{}" , & line[ ..end], & line[ end..]) 129} 130 131fn label ( event : & ProgressEvent , registry : & url:: Url ) ->Option < String > { 132let text =match event{ 133ProgressEvent :: NpmMetadata { name} =>format! ( "Fetching npm metadata for {name}" ), 134ProgressEvent :: NpmArchive { name, version} =>{ 135format! ( "Downloading npm:{name}@{version}" ) 136} 137ProgressEvent :: NpmMaterializing { name, version} =>{ 138format! ( "Extracting and verifying npm:{name}@{version}" ) 139} 140ProgressEvent :: NpmMaterialized { ..} =>return None , 141ProgressEvent :: NpmResolving =>"Resolving npm dependencies" . into (), 142ProgressEvent :: SourceDownload { url} =>source_label ( url, registry), 143}; 144Some ( 145 text. chars () 146. filter ( |ch| !ch. is_control ()) 147. take ( 240 ) 148. collect (), 149) 150} 151 152fn source_label ( value : & str , registry : & url:: Url ) ->String { 153let Ok ( mut url) = url:: Url :: parse ( value) else { 154return "Downloading source" . into (); 155}; 156let _ = url. set_username ( "" ); 157let _ = url. set_password ( None ); 158 url. set_query ( None ); 159 url. set_fragment ( None ); 160let prefix =format! ( "{}/" , registry. path (). trim_end_matches ( '/' )); 161if url. origin () == registry. origin () 162 &&let Some ( suffix) = url. path (). strip_prefix ( & prefix) 163{ 164let parts: Vec < _ > = suffix. split ( '/' ). take ( 3 ). collect (); 165if let [ scope, name, version] = parts. as_slice () 166 && scope. starts_with ( '@' ) 167{ 168return if * version =="meta.json" { 169format! ( "Fetching JSR metadata for {scope}/{name}" ) 170} else { 171let version = version. strip_suffix ( "_meta.json" ). unwrap_or ( version); 172format! ( "Downloading jsr:{scope}/{name}@{version}" ) 173}; 174} 175} 176format! ( "Downloading {url}" ) 177}