• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Blightmud / Blightmud / 22756411130

06 Mar 2026 08:59AM UTC coverage: 73.55% (-5.2%) from 78.779%
22756411130

push

github

web-flow
Vendors pulldown-cmark-mdcat as pulldown-cmark-blightmud (#1364)

Due to the crates.io yank of pulldown-cmark-mdcat it has now been
vendored in blightmud source while respecting the original licensing.
The name was changed to pulldown-cmark-blightmud to make clear that we
don't intend to maintain a fork of the original pulldown-cmark-mdcat.

762 of 1831 new or added lines in 20 files covered. (41.62%)

4 existing lines in 2 files now uncovered.

9610 of 13066 relevant lines covered (73.55%)

409.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.95
/src/ui/help_handler.rs
1
use crate::{event::Event, model::Line, VERSION};
2

3
use std::path::{Path, PathBuf};
4
use std::{borrow::Cow, collections::HashMap, fs, sync::mpsc::Sender};
5

6
use anyhow::Result;
7
use log::debug;
8
use pulldown_cmark_blightmud::pulldown_cmark::{Options, Parser};
9
use pulldown_cmark_blightmud::terminal::{TerminalProgram, TerminalSize};
10
use pulldown_cmark_blightmud::{ResourceUrlHandler, Settings as MDSettings, Theme};
11
use std::fmt::Write;
12
use syntect::parsing::SyntaxSet;
13

14
pub struct HelpHandler {
15
    writer: Sender<Event>,
16
    files: HashMap<&'static str, &'static str>,
17
}
18

19
impl HelpHandler {
20
    pub fn new(writer: Sender<Event>) -> Self {
131✔
21
        let files = load_files();
131✔
22
        Self { writer, files }
131✔
23
    }
131✔
24

25
    pub fn show_help(&self, file: &str, lock: bool) -> Result<()> {
42✔
26
        debug!("Drawing help file: {}", file);
42✔
27
        if lock {
42✔
28
            self.writer.send(Event::ScrollLock(true))?;
×
29
        }
42✔
30
        if let Some(line) = self.parse_helpfile(file) {
42✔
31
            self.writer.send(Event::Output(line)).unwrap();
41✔
32
        } else if let Some(line) = self.search_helpfiles(file) {
41✔
33
            self.writer.send(Event::Output(line)).unwrap();
×
34
        } else {
1✔
35
            self.writer
1✔
36
                .send(Event::Info("No help files found".to_string()))
1✔
37
                .unwrap();
1✔
38
        }
1✔
39
        if lock {
42✔
40
            self.writer.send(Event::ScrollLock(false))?;
×
41
        }
42✔
42
        Ok(())
42✔
43
    }
42✔
44

45
    fn read_from_file(&'_ self, file: &str) -> Cow<'_, str> {
123✔
46
        Cow::from(fs::read_to_string(file).unwrap_or_else(|_| panic!("Can't find {file}")))
123✔
47
    }
123✔
48

49
    /// Load helpfiles from disk in debug mode, from memory otherwise.
50
    fn file_content(&'_ self, file: &str) -> Cow<'_, str> {
123✔
51
        if cfg!(debug_assertions) {
123✔
52
            self.read_from_file(self.files[file])
123✔
53
        } else {
54
            Cow::from(self.files[file])
×
55
        }
56
    }
123✔
57

58
    fn get_plugin_helpfile_path(&self, file: &str) -> PathBuf {
84✔
59
        crate::DATA_DIR.join("plugins").join(file).join("README.md")
84✔
60
    }
84✔
61

62
    fn parse_markdown(&self, file_content: &str) -> Option<Line> {
82✔
63
        let mut options = Options::empty();
82✔
64
        options.insert(Options::ENABLE_TASKLISTS);
82✔
65
        options.insert(Options::ENABLE_STRIKETHROUGH);
82✔
66

67
        let parser = Parser::new_ext(file_content, options);
82✔
68

69
        // Useless as files are embedded into binary.
70
        let base_dir = Path::new("/");
82✔
71

72
        let mut md_bytes = vec![];
82✔
73
        let env = pulldown_cmark_blightmud::Environment::for_local_directory(&base_dir).unwrap();
82✔
74
        let resource_handler = NoopResourceUrlHandler {};
82✔
75
        if pulldown_cmark_blightmud::push_tty(
82✔
76
            &md_settings(&SyntaxSet::load_defaults_newlines()),
82✔
77
            &env,
82✔
78
            &resource_handler,
82✔
79
            &mut md_bytes,
82✔
80
            parser,
82✔
81
        )
82✔
82
        .is_ok()
82✔
83
        {
84
            if let Ok(md_string) = String::from_utf8(md_bytes) {
82✔
85
                Some(Line::from(format!("\n\n{md_string}")))
82✔
86
            } else {
87
                None
×
88
            }
89
        } else {
90
            None
×
91
        }
92
    }
82✔
93

94
    fn parse_helpfile(&self, file: &str) -> Option<Line> {
84✔
95
        let plugin_help_path = self.get_plugin_helpfile_path(file);
84✔
96
        if plugin_help_path.exists() {
84✔
97
            if let Some(path) = plugin_help_path.to_str() {
×
98
                let content = self.read_from_file(path);
×
99
                self.parse_markdown(&content)
×
100
            } else {
101
                None
×
102
            }
103
        } else if self.files.contains_key(file) {
84✔
104
            let data_dir = crate::DATA_DIR.clone();
82✔
105
            let log_path = data_dir.join("logs");
82✔
106
            let datadir = data_dir.to_str().unwrap_or("$USER_DATA_DIR");
82✔
107
            let logdir = log_path.to_str().unwrap_or("$USER_DATA_DIR/logs");
82✔
108
            let config_path = crate::CONFIG_DIR.to_path_buf();
82✔
109
            let config_dir = config_path.to_str().unwrap_or("$USER_CONFIG_DIR");
82✔
110

111
            let file_content = self
82✔
112
                .file_content(file)
82✔
113
                .replace("$VERSION", VERSION)
82✔
114
                .replace("$LOGDIR", logdir)
82✔
115
                .replace("$DATADIR", datadir)
82✔
116
                .replace("$CONFIGDIR", config_dir);
82✔
117

118
            self.parse_markdown(&file_content)
82✔
119
        } else {
120
            None
2✔
121
        }
122
    }
84✔
123

124
    pub fn search_helpfiles(&self, pattern: &str) -> Option<Line> {
1✔
125
        let mut matches = vec![];
1✔
126
        for key in self.files.keys() {
41✔
127
            let content = self.file_content(key);
41✔
128
            if content.contains(pattern) {
41✔
129
                matches.push(key);
×
130
            }
41✔
131
        }
132
        if !matches.is_empty() {
1✔
133
            let mut output = "No such help file exists.\nThe following help files contain a match for your search:".to_string();
×
134
            for key in matches {
×
135
                write!(output, "\n- {key}").unwrap();
×
136
            }
×
137
            Some(Line::from(output))
×
138
        } else {
139
            None
1✔
140
        }
141
    }
1✔
142
}
143

144
fn md_settings(syntax_set: &'_ SyntaxSet) -> MDSettings<'_> {
82✔
145
    let terminal_size = TerminalSize::detect().unwrap_or_default();
82✔
146

147
    MDSettings {
82✔
148
        terminal_capabilities: TerminalProgram::Ansi.capabilities(),
82✔
149
        terminal_size,
82✔
150
        theme: Theme::default(),
82✔
151
        syntax_set,
82✔
152
    }
82✔
153
}
82✔
154

155
struct NoopResourceUrlHandler;
156

157
impl std::fmt::Debug for NoopResourceUrlHandler {
158
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
159
        f.debug_struct("NoopResourceUrlHandler").finish()
×
160
    }
×
161
}
162

163
impl ResourceUrlHandler for NoopResourceUrlHandler {
164
    fn read_resource(
×
165
        &self,
×
166
        _: &reqwest::Url,
×
NEW
167
    ) -> std::io::Result<pulldown_cmark_blightmud::resources::MimeData> {
×
168
        Err(std::io::Error::from(std::io::ErrorKind::Unsupported))
×
169
    }
×
170
}
171

172
macro_rules! help_files {
173
    ($(
174
        $(#[$attr:meta])*
175
        $name:literal => $path:literal,
176
    )+) => {
177
        let mut files: HashMap<&str, &str> = HashMap::new();
178
        $(
179
            $(#[$attr])*
180
            files.insert(
181
                $name,
182
                if cfg!(debug_assertions) {
183
                    concat!(env!("CARGO_MANIFEST_DIR"), "/resources/help/", $path)
184
                } else {
185
                    include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/resources/help/", $path))
186
                }
187
            );
188
        )+
189
        files
190
    };
191
    // Same as above but allows no trailing comma.
192
    ($($(#[$attr:meta])* $file:literal => $path:literal),+) => {
193
        help_files!($($(#[$attr])* $file => $path,)+)
194
    };
195
}
196

197
fn load_files() -> HashMap<&'static str, &'static str> {
131✔
198
    help_files! {
131✔
199
        "help" => "help.md",
131✔
200
        "changes" => "changes.md",
131✔
201
        "welcome" => "welcome.md",
131✔
202
        "logging" => "logging.md",
131✔
203
        "blight" => "blight.md",
131✔
204
        "bindings" => "bindings.md",
131✔
205
        "core" => "core.md",
131✔
206
        #[cfg(feature = "tts")]
207
        "tts" => "tts.md",
131✔
208
        #[cfg(not(feature = "tts"))]
209
        "tts" => "no_tts.md",
210
        "status_area" => "status_area.md",
131✔
211
        "alias" => "aliases.md",
131✔
212
        "script" => "script.md",
131✔
213
        "spellcheck" => "spellcheck.md",
131✔
214
        "trigger" => "trigger.md",
131✔
215
        "timers" => "timers.md",
131✔
216
        "gmcp" => "gmcp.md",
131✔
217
        "msdp" => "msdp.md",
131✔
218
        "mssp" => "mssp.md",
131✔
219
        "regex" => "regex.md",
131✔
220
        "line" => "line.md",
131✔
221
        "mud" => "mud.md",
131✔
222
        "fs" => "fs.md",
131✔
223
        "audio" => "audio.md",
131✔
224
        "log" => "log.md",
131✔
225
        "config_scripts" => "config_scripts.md",
131✔
226
        "scripting" => "scripting.md",
131✔
227
        "settings" => "settings.md",
131✔
228
        "storage" => "storage.md",
131✔
229
        "colors" => "colors.md",
131✔
230
        "tasks" => "tasks.md",
131✔
231
        "socket" => "socket.md",
131✔
232
        "plugin" => "plugin.md",
131✔
233
        "plugin_developer" => "plugin_developer.md",
131✔
234
        "servers" => "servers.md",
131✔
235
        "search" => "search.md",
131✔
236
        "scrolling" => "scrolling.md",
131✔
237
        "ttype" => "ttype.md",
131✔
238
        "json" => "json.md",
131✔
239
        "prompt" => "prompt.md",
131✔
240
        "prompt_mask" => "prompt_mask.md",
131✔
241
        "history" => "history.md",
131✔
242
        "script_example" => "scripte_example.md"
131✔
243
    }
244
}
131✔
245

246
#[cfg(test)]
247
mod help_test {
248

249
    use super::HelpHandler;
250
    use crate::event::Event;
251
    use std::sync::mpsc::{channel, Receiver, Sender};
252

253
    fn handler() -> HelpHandler {
2✔
254
        let (writer, _): (Sender<Event>, Receiver<Event>) = channel();
2✔
255
        HelpHandler::new(writer)
2✔
256
    }
2✔
257

258
    #[test]
259
    fn confirm_markdown_parsing() {
1✔
260
        let handler = handler();
1✔
261
        for file in handler.files.keys() {
41✔
262
            assert!(handler.parse_helpfile(file).is_some());
41✔
263
        }
264
    }
1✔
265

266
    #[test]
267
    fn file_not_present() {
1✔
268
        let handler = handler();
1✔
269
        assert_eq!(handler.parse_helpfile("nothing"), None);
1✔
270
    }
1✔
271

272
    #[test]
273
    fn confirm_help_render() {
1✔
274
        let (writer, reader): (Sender<Event>, Receiver<Event>) = channel();
1✔
275
        let handler = HelpHandler::new(writer);
1✔
276
        handler
1✔
277
            .show_help("defintitelydoesntmatchanything", false)
1✔
278
            .unwrap();
1✔
279
        assert_eq!(
1✔
280
            reader.recv(),
1✔
281
            Ok(Event::Info("No help files found".to_string()))
1✔
282
        );
283
        handler.show_help("help", false).unwrap();
1✔
284
        let line = if let Ok(Event::Output(line)) = reader.recv() {
1✔
285
            Some(line)
1✔
286
        } else {
287
            None
×
288
        };
289
        assert_ne!(line, None);
1✔
290
        assert!(!line.unwrap().is_empty());
1✔
291
    }
1✔
292
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc