Просмотр исходного кода

feat: hyperlink flag

(exa PR) 1177
Christina Sørensen 2 лет назад
Родитель
Сommit
1b3267863d

+ 1 - 0
README.md

@@ -44,6 +44,7 @@ exa’s options are almost, but not quite, entirely unlike `ls`’s.
 - **--colo[u]r-scale**: highlight levels of file sizes distinctly
 - **--icons**: display icons
 - **--no-icons**: don't display icons (always overrides --icons)
+- **--hyperlink**: display entries as hyperlinks
 
 ### Filtering options
 

+ 1 - 0
completions/fish/exa.fish

@@ -20,6 +20,7 @@ complete -c exa        -l 'color-scale' \
                        -l 'colour-scale' -d "Highlight levels of file sizes distinctly"
 complete -c exa        -l 'icons'        -d "Display icons"
 complete -c exa        -l 'no-icons'     -d "Don't display icons"
+complete -c exa        -l 'hyperlink'    -d "Display entries as hyperlinks"
 
 # Filtering and sorting options
 complete -c exa -l 'group-directories-first' -d "Sort directories before other files"

+ 1 - 0
completions/zsh/_exa

@@ -23,6 +23,7 @@ __exa() {
         --colo{,u}r-scale"[Highlight levels of file sizes distinctly]" \
         --icons"[Display icons]" \
         --no-icons"[Hide icons]" \
+        --hyperlink"[Display entries as hyperlinks]" \
         --group-directories-first"[Sort directories before other files]" \
         --git-ignore"[Ignore files mentioned in '.gitignore']" \
         {-a,--all}"[Show hidden and 'dot' files]" \

+ 3 - 0
man/exa.1.md

@@ -75,6 +75,9 @@ Valid settings are ‘`always`’, ‘`automatic`’, and ‘`never`’.
 `--no-icons`
 : Don't display icons. (Always overrides --icons)
 
+`--hyperlink`
+: Display entries as hyperlinks
+
 
 FILTERING AND SORTING OPTIONS
 =============================

+ 12 - 2
src/options/file_name.rs

@@ -2,15 +2,16 @@ use crate::options::{flags, OptionsError, NumberSource};
 use crate::options::parser::MatchedFlags;
 use crate::options::vars::{self, Vars};
 
-use crate::output::file_name::{Options, Classify, ShowIcons};
+use crate::output::file_name::{Options, Classify, ShowIcons, EmbedHyperlinks};
 
 
 impl Options {
     pub fn deduce<V: Vars>(matches: &MatchedFlags<'_>, vars: &V) -> Result<Self, OptionsError> {
         let classify = Classify::deduce(matches)?;
         let show_icons = ShowIcons::deduce(matches, vars)?;
+        let embed_hyperlinks = EmbedHyperlinks::deduce(matches)?;
 
-        Ok(Self { classify, show_icons })
+        Ok(Self { classify, show_icons, embed_hyperlinks })
     }
 }
 
@@ -44,3 +45,12 @@ impl ShowIcons {
         }
     }
 }
+
+impl EmbedHyperlinks {
+    fn deduce(matches: &MatchedFlags<'_>) -> Result<Self, OptionsError> {
+        let flagged = matches.has(&flags::HYPERLINK)?;
+
+        if flagged { Ok(Self::On) }
+              else { Ok(Self::Off) }
+    }
+}

+ 2 - 1
src/options/flags.rs

@@ -51,6 +51,7 @@ pub static TIME:       Arg = Arg { short: Some(b't'), long: "time",       takes_
 pub static ACCESSED:   Arg = Arg { short: Some(b'u'), long: "accessed",   takes_value: TakesValue::Forbidden };
 pub static CREATED:    Arg = Arg { short: Some(b'U'), long: "created",    takes_value: TakesValue::Forbidden };
 pub static TIME_STYLE: Arg = Arg { short: None,       long: "time-style", takes_value: TakesValue::Necessary(Some(TIME_STYLES)) };
+pub static HYPERLINK:  Arg = Arg { short: None,       long: "hyperlink",  takes_value: TakesValue::Forbidden};
 const TIMES: Values = &["modified", "changed", "accessed", "created"];
 const TIME_STYLES: Values = &["default", "long-iso", "full-iso", "iso"];
 
@@ -80,7 +81,7 @@ pub static ALL_ARGS: Args = Args(&[
     &IGNORE_GLOB, &GIT_IGNORE, &ONLY_DIRS,
 
     &BINARY, &BYTES, &GROUP, &NUMERIC, &HEADER, &ICONS, &INODE, &LINKS, &MODIFIED, &CHANGED,
-    &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE,
+    &BLOCKS, &TIME, &ACCESSED, &CREATED, &TIME_STYLE, &HYPERLINK,
     &NO_PERMISSIONS, &NO_FILESIZE, &NO_USER, &NO_TIME, &NO_ICONS,
 
     &GIT, &GIT_REPOS, &GIT_REPOS_NO_STAT, &EXTENDED, &OCTAL, &SECURITY_CONTEXT

+ 1 - 0
src/options/help.rs

@@ -24,6 +24,7 @@ DISPLAY OPTIONS
   --colo[u]r-scale   highlight levels of file sizes distinctly
   --icons            display icons
   --no-icons         don't display icons (always overrides --icons)
+  --hyperlink        display entries as hyperlinks
 
 FILTERING AND SORTING OPTIONS
   -a, --all                  show hidden and 'dot' files

+ 68 - 5
src/output/file_name.rs

@@ -19,6 +19,9 @@ pub struct Options {
 
     /// Whether to prepend icon characters before file names.
     pub show_icons: ShowIcons,
+    
+    /// Whether to make file names hyperlinks.
+    pub embed_hyperlinks: EmbedHyperlinks,
 }
 
 impl Options {
@@ -84,6 +87,13 @@ pub enum ShowIcons {
     On(u32),
 }
 
+/// Whether to embed hyperlinks.
+#[derive(PartialEq, Eq, Debug, Copy, Clone)]
+pub enum EmbedHyperlinks{
+    
+    Off,
+    On,
+}
 
 /// A **file name** holds all the information necessary to display the name
 /// of the given file. This is used in all of the views.
@@ -151,7 +161,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         	// indicate this fact. But when showing targets, we can just
         	// colour the path instead (see below), and leave the broken
         	// link’s filename as the link colour.
-            for bit in self.coloured_file_name() {
+            for bit in self.escaped_file_name() {
                 bits.push(bit);
             }
         }
@@ -171,6 +181,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                         let target_options = Options {
                             classify: Classify::JustFilenames,
                             show_icons: ShowIcons::Off,
+                            embed_hyperlinks: EmbedHyperlinks::Off,
                         };
 
                         let target_name = FileName {
@@ -181,7 +192,7 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
                             options: target_options,
                         };
 
-                        for bit in target_name.coloured_file_name() {
+                        for bit in target_name.escaped_file_name() {
                             bits.push(bit);
                         }
 
@@ -279,6 +290,8 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
     /// Returns at least one ANSI-highlighted string representing this file’s
     /// name using the given set of colours.
     ///
+    /// If --hyperlink flag is provided, it will escape the filename accordingly.
+    ///
     /// Ordinarily, this will be just one string: the file’s complete name,
     /// coloured according to its file type. If the name contains control
     /// characters such as newlines or escapes, though, we can’t just print them
@@ -286,12 +299,11 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
     ///
     /// So in that situation, those characters will be escaped and highlighted in
     /// a different colour.
-    fn coloured_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
+    fn escaped_file_name<'unused>(&self) -> Vec<ANSIString<'unused>> {
         let file_style = self.style();
         let mut bits = Vec::new();
 
-        escape(
-            self.file.name.clone(),
+        self.escape_color_and_hyperlinks(
             &mut bits,
             file_style,
             self.colours.control_char(),
@@ -300,6 +312,52 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
         bits
     }
 
+    // An adapted version of escape::escape.
+    // afaik of all the calls to escape::escape, only for escaped_file_name, the call to escape needs to be checked for hyper links
+    // and if that's the case then I think it's best to not try and generalize escape::escape to this case,
+    // as this adaptation would incur some unneeded operations there
+    pub fn escape_color_and_hyperlinks(&self, bits: &mut Vec<ANSIString<'_>>, good: Style, bad: Style) {
+        let string = self.file.name.to_owned();
+
+        if string.chars().all(|c| c >= 0x20 as char && c != 0x7f as char) {
+            let painted = good.paint(string);
+
+            let adjusted_filename = if let EmbedHyperlinks::On = self.options.embed_hyperlinks {
+                ANSIString::from(format!("\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", self.file.path.display(), painted))
+            } else {
+                painted
+            };
+            bits.push(adjusted_filename);
+            return;
+        }
+
+        // again adapted from escape::escape
+        // still a slow route, but slightly improved to at least not reallocate buff + have a predetermined buff size
+        //
+        // also note that buff would never need more than len,
+        // even tho 'in total' it will be lenghier than len (as we expand with escape_default),
+        // because we clear it after an irregularity
+        let mut buff = String::with_capacity(string.len());
+        for c in string.chars() {
+            // The `escape_default` method on `char` is *almost* what we want here, but
+            // it still escapes non-ASCII UTF-8 characters, which are still printable.
+
+            if c >= 0x20 as char && c != 0x7f as char {
+                buff.push(c);
+            }
+            else {
+                if ! buff.is_empty() {
+                    bits.push(good.paint(std::mem::take(&mut buff)));
+                }
+                // biased towards regular characters, so we still collect on first sight of bad char
+                for e in c.escape_default() {
+                    buff.push(e);
+                }
+                bits.push(bad.paint(std::mem::take(&mut buff)));
+            }
+        }
+    }
+
     /// Figures out which colour to paint the filename part of the output,
     /// depending on which “type” of file it appears to be — either from the
     /// class on the filesystem or from its name. (Or the broken link colour,
@@ -330,6 +388,11 @@ impl<'a, 'dir, C: Colours> FileName<'a, 'dir, C> {
             _                            => self.colours.colour_file(self.file),
         }
     }
+
+    /// For grid's use, to cover the case of hyperlink escape sequences
+    pub fn bare_width(&self) -> usize {
+        self.file.name.len()
+    }
 }
 
 

+ 6 - 3
src/output/grid.rs

@@ -41,11 +41,14 @@ impl<'a> Render<'a> {
 
         self.filter.sort_files(&mut self.files);
         for file in &self.files {
-            let filename = self.file_style.for_file(file, self.theme).paint();
+            let filename = self.file_style.for_file(file, self.theme);
+            let contents = filename.paint();
 
             grid.add(tg::Cell {
-                contents:  filename.strings().to_string(),
-                width:     *filename.width(),
+                contents:  contents.strings().to_string(),
+                // with hyperlink escape sequences,
+                // the actual *contents.width() is larger than actually needed, so we take only the filename
+                width:     filename.bare_width(),
                 alignment: tg::Alignment::Left,
             });
         }