char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somadd progress to module imports from registries7b01146

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