Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(exa PR) 1177: feature: hyperlink flag #17

Merged
merged 7 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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