@@ -6,6 +6,7 @@ use anstream::AutoStream;
6
6
use anstyle:: Style ;
7
7
8
8
use crate :: util:: errors:: CargoResult ;
9
+ use crate :: util:: hostname;
9
10
use crate :: util:: style:: * ;
10
11
11
12
pub enum TtyWidth {
@@ -57,6 +58,7 @@ pub struct Shell {
57
58
/// Flag that indicates the current line needs to be cleared before
58
59
/// printing. Used when a progress bar is currently displayed.
59
60
needs_clear : bool ,
61
+ hostname : Option < String > ,
60
62
}
61
63
62
64
impl fmt:: Debug for Shell {
@@ -85,6 +87,7 @@ enum ShellOut {
85
87
stderr : AutoStream < std:: io:: Stderr > ,
86
88
stderr_tty : bool ,
87
89
color_choice : ColorChoice ,
90
+ hyperlinks : bool ,
88
91
} ,
89
92
}
90
93
@@ -111,10 +114,12 @@ impl Shell {
111
114
stdout : AutoStream :: new ( std:: io:: stdout ( ) , stdout_choice) ,
112
115
stderr : AutoStream :: new ( std:: io:: stderr ( ) , stderr_choice) ,
113
116
color_choice : auto_clr,
117
+ hyperlinks : supports_hyperlinks ( ) ,
114
118
stderr_tty : std:: io:: stderr ( ) . is_terminal ( ) ,
115
119
} ,
116
120
verbosity : Verbosity :: Verbose ,
117
121
needs_clear : false ,
122
+ hostname : None ,
118
123
}
119
124
}
120
125
@@ -124,6 +129,7 @@ impl Shell {
124
129
output : ShellOut :: Write ( AutoStream :: never ( out) ) , // strip all formatting on write
125
130
verbosity : Verbosity :: Verbose ,
126
131
needs_clear : false ,
132
+ hostname : None ,
127
133
}
128
134
}
129
135
@@ -314,6 +320,16 @@ impl Shell {
314
320
Ok ( ( ) )
315
321
}
316
322
323
+ pub fn set_hyperlinks ( & mut self , yes : bool ) -> CargoResult < ( ) > {
324
+ if let ShellOut :: Stream {
325
+ ref mut hyperlinks, ..
326
+ } = self . output
327
+ {
328
+ * hyperlinks = yes;
329
+ }
330
+ Ok ( ( ) )
331
+ }
332
+
317
333
/// Gets the current color choice.
318
334
///
319
335
/// If we are not using a color stream, this will always return `Never`, even if the color
@@ -340,6 +356,61 @@ impl Shell {
340
356
}
341
357
}
342
358
359
+ pub fn out_hyperlink < D : fmt:: Display > ( & self , url : D ) -> Hyperlink < D > {
360
+ let supports_hyperlinks = match & self . output {
361
+ ShellOut :: Write ( _) => false ,
362
+ ShellOut :: Stream {
363
+ stdout, hyperlinks, ..
364
+ } => stdout. current_choice ( ) == anstream:: ColorChoice :: AlwaysAnsi && * hyperlinks,
365
+ } ;
366
+ Hyperlink {
367
+ url : supports_hyperlinks. then_some ( url) ,
368
+ }
369
+ }
370
+
371
+ pub fn err_hyperlink < D : fmt:: Display > ( & self , url : D ) -> Hyperlink < D > {
372
+ let supports_hyperlinks = match & self . output {
373
+ ShellOut :: Write ( _) => false ,
374
+ ShellOut :: Stream {
375
+ stderr, hyperlinks, ..
376
+ } => stderr. current_choice ( ) == anstream:: ColorChoice :: AlwaysAnsi && * hyperlinks,
377
+ } ;
378
+ if supports_hyperlinks {
379
+ Hyperlink { url : Some ( url) }
380
+ } else {
381
+ Hyperlink { url : None }
382
+ }
383
+ }
384
+
385
+ pub fn out_file_hyperlink ( & mut self , path : & std:: path:: Path ) -> Hyperlink < url:: Url > {
386
+ let url = self . file_hyperlink ( path) ;
387
+ url. map ( |u| self . out_hyperlink ( u) ) . unwrap_or_default ( )
388
+ }
389
+
390
+ pub fn err_file_hyperlink ( & mut self , path : & std:: path:: Path ) -> Hyperlink < url:: Url > {
391
+ let url = self . file_hyperlink ( path) ;
392
+ url. map ( |u| self . err_hyperlink ( u) ) . unwrap_or_default ( )
393
+ }
394
+
395
+ fn file_hyperlink ( & mut self , path : & std:: path:: Path ) -> Option < url:: Url > {
396
+ let mut url = url:: Url :: from_file_path ( path) . ok ( ) ?;
397
+ // Do a best-effort of setting the host in the URL to avoid issues with opening a link
398
+ // scoped to the computer you've SSHed into
399
+ let hostname = if cfg ! ( windows) {
400
+ // Not supported correctly on windows
401
+ None
402
+ } else {
403
+ if let Some ( hostname) = self . hostname . as_deref ( ) {
404
+ Some ( hostname)
405
+ } else {
406
+ self . hostname = hostname ( ) . ok ( ) . and_then ( |h| h. into_string ( ) . ok ( ) ) ;
407
+ self . hostname . as_deref ( )
408
+ }
409
+ } ;
410
+ let _ = url. set_host ( hostname) ;
411
+ Some ( url)
412
+ }
413
+
343
414
/// Prints a message to stderr and translates ANSI escape code into console colors.
344
415
pub fn print_ansi_stderr ( & mut self , message : & [ u8 ] ) -> CargoResult < ( ) > {
345
416
if self . needs_clear {
@@ -439,6 +510,44 @@ fn supports_color(choice: anstream::ColorChoice) -> bool {
439
510
}
440
511
}
441
512
513
+ fn supports_hyperlinks ( ) -> bool {
514
+ #[ allow( clippy:: disallowed_methods) ] // We are reading the state of the system, not config
515
+ if std:: env:: var_os ( "TERM_PROGRAM" ) . as_deref ( ) == Some ( std:: ffi:: OsStr :: new ( "iTerm.app" ) ) {
516
+ // Override `supports_hyperlinks` as we have an unknown incompatibility with iTerm2
517
+ return false ;
518
+ }
519
+
520
+ supports_hyperlinks:: supports_hyperlinks ( )
521
+ }
522
+
523
+ pub struct Hyperlink < D : fmt:: Display > {
524
+ url : Option < D > ,
525
+ }
526
+
527
+ impl < D : fmt:: Display > Default for Hyperlink < D > {
528
+ fn default ( ) -> Self {
529
+ Self { url : None }
530
+ }
531
+ }
532
+
533
+ impl < D : fmt:: Display > Hyperlink < D > {
534
+ pub fn open ( & self ) -> impl fmt:: Display {
535
+ if let Some ( url) = self . url . as_ref ( ) {
536
+ format ! ( "\x1B ]8;;{url}\x1B \\ " )
537
+ } else {
538
+ String :: new ( )
539
+ }
540
+ }
541
+
542
+ pub fn close ( & self ) -> impl fmt:: Display {
543
+ if self . url . is_some ( ) {
544
+ "\x1B ]8;;\x1B \\ "
545
+ } else {
546
+ ""
547
+ }
548
+ }
549
+ }
550
+
442
551
#[ cfg( unix) ]
443
552
mod imp {
444
553
use super :: { Shell , TtyWidth } ;
0 commit comments