11use crate :: { Browser , Error , ErrorKind , Result } ;
2- use std:: os:: unix:: process:: ExitStatusExt ;
3- use std:: process:: { Command , ExitStatus } ;
2+ use std:: os:: unix:: fs:: PermissionsExt ;
3+ use std:: path:: PathBuf ;
4+ use std:: process:: { Command , Stdio } ;
45
5- mod common;
6- use common:: from_status;
6+ macro_rules! try_browser {
7+ ( $name: expr, $( $arg: expr ) ,+ ) => {
8+ for_matching_path( $name, |pb| {
9+ let mut cmd = Command :: new( pb) ;
10+ $(
11+ cmd. arg( $arg) ;
12+ ) +
13+ run_command( & mut cmd, !is_text_browser( & pb) )
14+ } )
15+ }
16+ }
717
818/// Deal with opening of browsers on Linux and *BSD - currently supports only the default browser
919///
@@ -12,68 +22,155 @@ use common::from_status;
1222/// 2. Attempt to open the url via xdg-open, gvfs-open, gnome-open, open, respectively, whichever works
1323/// first
1424#[ inline]
15- pub fn open_browser_internal ( browser : Browser , url : & str ) -> Result < ( ) > {
16- from_status ( open_browser_unix ( browser, url) )
17- }
18-
19- fn open_browser_unix ( browser : Browser , url : & str ) -> Result < ExitStatus > {
20- match browser {
21- Browser :: Default => open_on_unix_using_browser_env ( url)
22- . or_else ( |_| -> Result < ExitStatus > { Command :: new ( "xdg-open" ) . arg ( url) . status ( ) } )
23- . or_else ( |r| -> Result < ExitStatus > {
24- if let Ok ( desktop) = :: std:: env:: var ( "XDG_CURRENT_DESKTOP" ) {
25- if desktop == "KDE" {
26- return Command :: new ( "kioclient" ) . arg ( "exec" ) . arg ( url) . status ( ) ;
27- }
28- }
29- Err ( r) // If either `if` check fails, fall through to the next or_else
30- } )
31- . or_else ( |_| -> Result < ExitStatus > { Command :: new ( "gvfs-open" ) . arg ( url) . status ( ) } )
32- . or_else ( |_| -> Result < ExitStatus > { Command :: new ( "gnome-open" ) . arg ( url) . status ( ) } )
33- . or_else ( |_| -> Result < ExitStatus > {
34- Command :: new ( "kioclient" ) . arg ( "exec" ) . arg ( url) . status ( )
35- } )
36- . or_else ( |e| -> Result < ExitStatus > {
37- if let Ok ( _child) = Command :: new ( "x-www-browser" ) . arg ( url) . spawn ( ) {
38- return Ok ( ExitStatusExt :: from_raw ( 0 ) ) ;
39- }
40- Err ( e)
41- } ) ,
42- _ => Err ( Error :: new (
43- ErrorKind :: NotFound ,
44- "Only the default browser is supported on this platform right now" ,
45- ) ) ,
46- }
25+ pub fn open_browser_internal ( _: Browser , url : & str ) -> Result < ( ) > {
26+ // we first try with the $BROWSER env
27+ try_with_browser_env ( url)
28+ // then we try with xdg-open
29+ . or_else ( |_| try_browser ! ( "xdg-open" , url) )
30+ // else do desktop specific stuff
31+ . or_else ( |r| {
32+ // detect desktop
33+ let desktop_env: String = std:: env:: var ( "XDG_CURRENT_DESKTOP" )
34+ . unwrap_or_else ( |_| String :: from ( "unknown" ) )
35+ . to_ascii_uppercase ( ) ;
36+ match desktop_env. as_str ( ) {
37+ "KDE" => try_browser ! ( "kde-open" , url) . or_else ( |_| try_browser ! ( "kde-open5" , url) ) ,
38+ "GNOME" | "CINNAMON" => try_browser ! ( "gio" , "open" , url)
39+ . or_else ( |_| try_browser ! ( "gvfs-open" , url) )
40+ . or_else ( |_| try_browser ! ( "gnome-open" , url) ) ,
41+ "MATE" => try_browser ! ( "gio" , "open" , url)
42+ . or_else ( |_| try_browser ! ( "gvfs-open" , url) )
43+ . or_else ( |_| try_browser ! ( "mate-open" , url) ) ,
44+ "XFCE" => try_browser ! ( "exo-open" , url)
45+ . or_else ( |_| try_browser ! ( "gio" , "open" , url) )
46+ . or_else ( |_| try_browser ! ( "gvfs-open" , url) ) ,
47+ _ => Err ( r) ,
48+ }
49+ } )
50+ // at the end, we'll try x-www-browser and return the result as is
51+ . or_else ( |_| try_browser ! ( "x-www-browser" , url) )
52+ // and convert the result into a () on success
53+ . and_then ( |_| Ok ( ( ) ) )
54+ . or_else ( |_| {
55+ Err ( Error :: new (
56+ ErrorKind :: NotFound ,
57+ "No valid browsers detected. You can specify one in BROWSERS environment variable" ,
58+ ) )
59+ } )
4760}
4861
49- fn open_on_unix_using_browser_env ( url : & str ) -> Result < ExitStatus > {
50- let browsers = :: std:: env:: var ( "BROWSER" )
51- . map_err ( |_| -> Error { Error :: new ( ErrorKind :: NotFound , "BROWSER env not set" ) } ) ?;
52- for browser in browsers. split ( ':' ) {
53- // $BROWSER can contain ':' delimited options, each representing a potential browser command line
62+ #[ inline]
63+ fn try_with_browser_env ( url : & str ) -> Result < ( ) > {
64+ // $BROWSER can contain ':' delimited options, each representing a potential browser command line
65+ for browser in std:: env:: var ( "BROWSER" )
66+ . unwrap_or_else ( |_| String :: from ( "" ) )
67+ . split ( ':' )
68+ {
5469 if !browser. is_empty ( ) {
5570 // each browser command can have %s to represent URL, while %c needs to be replaced
5671 // with ':' and %% with '%'
5772 let cmdline = browser
5873 . replace ( "%s" , url)
5974 . replace ( "%c" , ":" )
6075 . replace ( "%%" , "%" ) ;
61- let cmdarr: Vec < & str > = cmdline. split_whitespace ( ) . collect ( ) ;
62- let mut cmd = Command :: new ( & cmdarr[ 0 ] ) ;
63- if cmdarr. len ( ) > 1 {
64- cmd. args ( & cmdarr[ 1 ..cmdarr. len ( ) ] ) ;
65- }
66- if !browser. contains ( "%s" ) {
67- // append the url as an argument only if it was not already set via %s
68- cmd. arg ( url) ;
69- }
70- if let Ok ( status) = cmd. status ( ) {
71- return Ok ( status) ;
76+ let cmdarr: Vec < & str > = cmdline. split_ascii_whitespace ( ) . collect ( ) ;
77+ let browser_cmd = cmdarr[ 0 ] ;
78+ let env_exit = for_matching_path ( browser_cmd, |pb| {
79+ let mut cmd = Command :: new ( pb) ;
80+ for i in 1 ..cmdarr. len ( ) {
81+ cmd. arg ( cmdarr[ i] ) ;
82+ }
83+ if !browser. contains ( "%s" ) {
84+ // append the url as an argument only if it was not already set via %s
85+ cmd. arg ( url) ;
86+ }
87+ run_command ( & mut cmd, !is_text_browser ( & pb) )
88+ } ) ;
89+ if env_exit. is_ok ( ) {
90+ return Ok ( ( ) ) ;
7291 }
7392 }
7493 }
7594 Err ( Error :: new (
7695 ErrorKind :: NotFound ,
77- "No valid command in $ BROWSER" ,
96+ "No valid browser configured in BROWSER environment variable " ,
7897 ) )
7998}
99+
100+ /// Returns true if specified command refers to a known list of text browsers
101+ #[ inline]
102+ fn is_text_browser ( pb : & PathBuf ) -> bool {
103+ for browser in TEXT_BROWSERS . iter ( ) {
104+ if pb. ends_with ( & browser) {
105+ return true ;
106+ }
107+ }
108+ false
109+ }
110+
111+ #[ inline]
112+ fn for_matching_path < F > ( name : & str , op : F ) -> Result < ( ) >
113+ where
114+ F : FnOnce ( & PathBuf ) -> Result < ( ) > ,
115+ {
116+ let err = Err ( Error :: new ( ErrorKind :: NotFound , "command not found" ) ) ;
117+
118+ // if the name already includes path separator, we should not try to do a PATH search on it
119+ // as it's likely an absolutely or relative name, so we treat it as such.
120+ if name. contains ( std:: path:: MAIN_SEPARATOR ) {
121+ let pb = std:: path:: PathBuf :: from ( name) ;
122+ if let Ok ( metadata) = pb. metadata ( ) {
123+ if metadata. is_file ( ) && metadata. permissions ( ) . mode ( ) & 0o111 != 0 {
124+ return op ( & pb) ;
125+ }
126+ } else {
127+ return err;
128+ }
129+ } else {
130+ // search for this name inside PATH
131+ if let Ok ( path) = std:: env:: var ( "PATH" ) {
132+ for entry in path. split ( ":" ) {
133+ let mut pb = std:: path:: PathBuf :: from ( entry) ;
134+ pb. push ( name) ;
135+ if let Ok ( metadata) = pb. metadata ( ) {
136+ if metadata. is_file ( ) && metadata. permissions ( ) . mode ( ) & 0o111 != 0 {
137+ return op ( & pb) ;
138+ }
139+ }
140+ }
141+ }
142+ }
143+ // return the not found err, if we didn't find anything above
144+ err
145+ }
146+
147+ /// Run the specified command in foreground/background
148+ #[ inline]
149+ fn run_command ( cmd : & mut Command , background : bool ) -> Result < ( ) > {
150+ if background {
151+ // if we're in background, set stdin/stdout to null and spawn a child, as we're
152+ // not supposed to have any interaction.
153+ cmd. stdin ( Stdio :: null ( ) )
154+ . stdout ( Stdio :: null ( ) )
155+ . stderr ( Stdio :: null ( ) )
156+ . spawn ( )
157+ . and_then ( |_| Ok ( ( ) ) )
158+ } else {
159+ // if we're in foreground, use status() instead of spawn(), as we'd like to wait
160+ // till completion
161+ cmd. status ( ) . and_then ( |status| {
162+ if status. success ( ) {
163+ Ok ( ( ) )
164+ } else {
165+ Err ( Error :: new (
166+ ErrorKind :: Other ,
167+ "command present but exited unsuccessfully" ,
168+ ) )
169+ }
170+ } )
171+ }
172+ }
173+
174+ static TEXT_BROWSERS : [ & ' static str ; 9 ] = [
175+ "lynx" , "links" , "links2" , "elinks" , "w3m" , "eww" , "netrik" , "retawq" , "curl" ,
176+ ] ;
0 commit comments