Skip to content

Commit

Permalink
feat: hyperlink flag
Browse files Browse the repository at this point in the history
(exa PR) 1177
  • Loading branch information
cafkafk committed Jul 29, 2023
2 parents 05605e5 + ec68e17 commit 1b32678
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 11 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions completions/fish/exa.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions completions/zsh/_exa
Original file line number Diff line number Diff line change
Expand Up @@ -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]" \
Expand Down
3 changes: 3 additions & 0 deletions man/exa.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
=============================
Expand Down
14 changes: 12 additions & 2 deletions src/options/file_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
}

Expand Down Expand Up @@ -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) }
}
}
3 changes: 2 additions & 1 deletion src/options/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/options/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 68 additions & 5 deletions src/output/file_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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 {
Expand All @@ -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);
}

Expand Down Expand Up @@ -279,19 +290,20 @@ 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
/// to the screen directly, because then there’ll be newlines in weird places.
///
/// 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(),
Expand All @@ -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,
Expand Down Expand Up @@ -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()
}
}


Expand Down
9 changes: 6 additions & 3 deletions src/output/grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down

0 comments on commit 1b32678

Please sign in to comment.