aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-08 03:12:05 +0900
committernsfisis <nsfisis@gmail.com>2026-05-08 03:12:05 +0900
commite6f35b567564665c6cb741a06e4c4afcdc5ab317 (patch)
treea1ae0e55afab95598ff0f6bec5b6a2e9f4f86cc0 /crates/mozart-core/src
parentb3fcdf3b5cf0d6b43109c4bcb3dfcbb6576abce5 (diff)
downloadphp-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.tar.gz
php-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.tar.zst
php-mozart-e6f35b567564665c6cb741a06e4c4afcdc5ab317.zip
fix(search): align with Composer's RepositoryInterface::search
Replace the HTTP-only post-filtered implementation with a Repository::search trait dispatch that mirrors ComposerRepository::search semantics for all three modes (FULLTEXT/NAME/VENDOR). --only-name now does an OR-of-tokens regex match against the full Packagist list.json index instead of a substring match against a fulltext page, so e.g. \`mozart search --only-name mono log\` matches \`monolog/monolog\` like Composer does. Other parity fixes: regex::escape on non-fulltext queries, format check before mutex check, 4-space JSON indent, OSC 8 terminal hyperlink emission when a result has a url, <warning>\! Abandoned \!</warning> styling on abandoned rows, and the Mozart-only "No packages found" warning is dropped to match Composer's silent empty-result behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src')
-rw-r--r--crates/mozart-core/src/console.rs47
1 files changed, 47 insertions, 0 deletions
diff --git a/crates/mozart-core/src/console.rs b/crates/mozart-core/src/console.rs
index b8db17e..e0c224f 100644
--- a/crates/mozart-core/src/console.rs
+++ b/crates/mozart-core/src/console.rs
@@ -43,6 +43,30 @@ pub fn __format_warning_message(message: &str) -> ColoredString {
}
// ---------------------------------------------------------------------------
+// Terminal hyperlinks (OSC 8)
+// ---------------------------------------------------------------------------
+
+/// Wrap `text` in an OSC 8 terminal hyperlink escape sequence pointing at `url`.
+///
+/// Mirrors Composer's `<href=URL>text</>` formatter tag, which Symfony Console
+/// renders to the same OSC 8 sequence. Mozart's tag formatter is a
+/// compile-time proc-macro that doesn't accept runtime attributes, so this
+/// helper is the runtime path.
+///
+/// When `decorated` is `false`, returns `text` unchanged (matching Symfony's
+/// behavior of suppressing hyperlinks on non-decorated outputs).
+///
+/// Control characters in `url` (`\x00`–`\x1f`, `\x7f`) are stripped to prevent
+/// terminating the escape sequence early.
+pub fn hyperlink(url: &str, text: &str, decorated: bool) -> String {
+ if !decorated {
+ return text.to_string();
+ }
+ let safe_url: String = url.chars().filter(|c| !c.is_control()).collect();
+ format!("\x1b]8;;{safe_url}\x1b\\{text}\x1b]8;;\x1b\\")
+}
+
+// ---------------------------------------------------------------------------
// Verbosity
// ---------------------------------------------------------------------------
@@ -413,4 +437,27 @@ mod tests {
assert!(!make_console(Verbosity::VeryVerbose).is_debug());
assert!(make_console(Verbosity::Debug).is_debug());
}
+
+ #[test]
+ fn test_hyperlink_decorated() {
+ let out = hyperlink("https://example.com", "click", true);
+ assert!(out.contains("https://example.com"));
+ assert!(out.contains("click"));
+ assert!(out.starts_with("\x1b]8;;"));
+ assert!(out.ends_with("\x1b]8;;\x1b\\"));
+ }
+
+ #[test]
+ fn test_hyperlink_undecorated_returns_plain_text() {
+ let out = hyperlink("https://example.com", "click", false);
+ assert_eq!(out, "click");
+ }
+
+ #[test]
+ fn test_hyperlink_strips_control_chars_from_url() {
+ let out = hyperlink("https://example.com/\x00\x07path", "click", true);
+ assert!(out.contains("https://example.com/path"));
+ assert!(!out.contains('\x00'));
+ assert!(!out.contains('\x07'));
+ }
}