Skip to content

Commit

Permalink
Format files and commits as OSC 8 hyperlinks
Browse files Browse the repository at this point in the history
Closes #257
  • Loading branch information
dandavison committed Jul 18, 2020
1 parent e5ea59c commit a6440ef
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 24 deletions.
16 changes: 9 additions & 7 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ use syntect::parsing::SyntaxSet;
use crate::bat::assets::HighlightingAssets;
use crate::bat::output::PagingMode;
use crate::git_config::GitConfig;
use crate::git_config_entry::GitConfigEntry;
use crate::options;
use crate::style::Style;

#[derive(StructOpt, Clone, Default)]
#[structopt(
Expand Down Expand Up @@ -341,6 +341,14 @@ pub struct Opt {
/// (overline), or the combination 'ul ol'.
pub file_decoration_style: String,

#[structopt(long = "hyperlinks")]
/// Format commit hash and file names as hyperlinks according to the hyperlink spec for
/// terminal emulators: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
/// Note that these hyperlinks are not yet supported by less, so they will not work in delta
/// unless you disable paging or install a fork of less. They are supported by several but not
/// all terminal emulators, but they are not supported yet by tmux.
pub hyperlinks: bool,

#[structopt(long = "hunk-header-style", default_value = "syntax")]
/// Style (foreground, background, attributes) for the hunk-header. See STYLES section. The
/// style 'omit' can be used to remove the hunk header section from the output.
Expand Down Expand Up @@ -523,12 +531,6 @@ pub struct Opt {
pub git_config_entries: HashMap<String, GitConfigEntry>,
}

#[derive(Clone, Debug)]
pub enum GitConfigEntry {
Style(Style),
String(String),
}

#[derive(Default, Clone, Debug)]
pub struct ComputedValues {
pub is_light_mode: bool,
Expand Down
5 changes: 4 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::color;
use crate::delta::State;
use crate::env;
use crate::features::side_by_side;
use crate::git_config_entry::GitConfigEntry;
use crate::style::Style;

pub struct Config {
Expand All @@ -26,9 +27,10 @@ pub struct Config {
pub file_removed_label: String,
pub file_renamed_label: String,
pub file_style: Style,
pub git_config_entries: HashMap<String, cli::GitConfigEntry>,
pub git_config_entries: HashMap<String, GitConfigEntry>,
pub keep_plus_minus_markers: bool,
pub hunk_header_style: Style,
pub hyperlinks: bool,
pub max_buffered_lines: usize,
pub max_line_distance: f64,
pub max_line_distance_for_naively_paired_lines: f64,
Expand Down Expand Up @@ -139,6 +141,7 @@ impl From<cli::Opt> for Config {
git_config_entries: opt.git_config_entries,
keep_plus_minus_markers: opt.keep_plus_minus_markers,
hunk_header_style,
hyperlinks: opt.hyperlinks,
max_buffered_lines: 32,
max_line_distance: opt.max_line_distance,
max_line_distance_for_naively_paired_lines,
Expand Down
25 changes: 22 additions & 3 deletions src/delta.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::io::BufRead;
use std::io::Write;

Expand All @@ -7,6 +8,7 @@ use unicode_segmentation::UnicodeSegmentation;

use crate::config::Config;
use crate::draw;
use crate::format;
use crate::paint::Painter;
use crate::parse;
use crate::style::DecorationStyle;
Expand Down Expand Up @@ -155,7 +157,11 @@ where
continue;
} else {
painter.emit()?;
writeln!(painter.writer, "{}", raw_line)?;
writeln!(
painter.writer,
"{}",
format::format_raw_line(&raw_line, config)
)?;
}
}

Expand Down Expand Up @@ -240,10 +246,23 @@ fn handle_commit_meta_header_line(
draw::write_no_decoration
}
};
let (formatted_line, formatted_raw_line) = if config.hyperlinks {
(
Cow::from(format::format_commit_line_with_osc8_commit_hyperlink(
line, config,
)),
Cow::from(format::format_commit_line_with_osc8_commit_hyperlink(
raw_line, config,
)),
)
} else {
(Cow::from(line), Cow::from(raw_line))
};

draw_fn(
painter.writer,
&format!("{}{}", line, if pad { " " } else { "" }),
&format!("{}{}", raw_line, if pad { " " } else { "" }),
&format!("{}{}", formatted_line, if pad { " " } else { "" }),
&format!("{}{}", formatted_raw_line, if pad { " " } else { "" }),
&config.decorations_width,
config.commit_style,
decoration_ansi_term_style,
Expand Down
66 changes: 66 additions & 0 deletions src/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::borrow::Cow;

use atty;
use lazy_static::lazy_static;
use regex::{Captures, Regex};

use crate::config::Config;
use crate::git_config_entry::{GitConfigEntry, GitRemoteRepo};

/// If output is going to a tty, emit hyperlinks if requested.
// Although raw output should basically be emitted unaltered, we do this.
pub fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> {
if config.hyperlinks && atty::is(atty::Stream::Stdout) {
format_commit_line_with_osc8_commit_hyperlink(line, config)
} else {
Cow::from(line)
}
}

pub fn format_commit_line_with_osc8_commit_hyperlink<'a>(
line: &'a str,
config: &Config,
) -> Cow<'a, str> {
if let Some(GitConfigEntry::GitRemote(GitRemoteRepo::GitHubRepo(repo))) =
config.git_config_entries.get("remote.origin.url")
{
COMMIT_LINE_REGEX.replace(line, |captures: &Captures| {
format_commit_line_captures_with_osc8_commit_hyperlink(captures, repo)
})
} else {
Cow::from(line)
}
}

pub fn format_osc8_file_hyperlink(file: &str) -> String {
format!(
"{osc}8;;file:///{file}{st}{file}{osc}8;;{st}",
file = file,
osc = "\x1b]",
st = "\x1b\\"
)
}

lazy_static! {
static ref COMMIT_LINE_REGEX: Regex = Regex::new("(.* )([0-9a-f]{40})(.*)").unwrap();
}

fn format_commit_line_captures_with_osc8_commit_hyperlink<'a, 'b>(
captures: &'a Captures,
github_repo: &'b str,
) -> String {
let commit = captures.get(2).unwrap().as_str();
format!(
"{prefix}{osc}8;;{url}{st}{commit}{osc}8;;{st}{suffix}",
url = format_github_commit_url(commit, github_repo),
commit = commit,
prefix = captures.get(1).unwrap().as_str(),
suffix = captures.get(3).unwrap().as_str(),
osc = "\x1b]",
st = "\x1b\\"
)
}

fn format_github_commit_url(commit: &str, github_repo: &str) -> String {
format!("https://github.com/{}/commit/{}", github_repo, commit)
}
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ mod draw;
mod edits;
mod env;
mod features;
mod format;
mod git_config;
mod git_config_entry;
mod options;
mod paint;
mod parse;
Expand All @@ -37,7 +39,7 @@ use crate::bat::output::{OutputType, PagingMode};
use crate::delta::delta;
use crate::options::theme::is_light_syntax_theme;

mod errors {
pub mod errors {
error_chain! {
foreign_links {
Io(::std::io::Error);
Expand Down
11 changes: 8 additions & 3 deletions src/options/set.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::process;
use std::str::FromStr;

use console::Term;
use structopt::clap;
Expand All @@ -11,6 +12,7 @@ use crate::config;
use crate::env;
use crate::features;
use crate::git_config;
use crate::git_config_entry::{self, GitConfigEntry};
use crate::options::option_value::{OptionValue, ProvenancedOptionValue};
use crate::options::theme;
use crate::style::Style;
Expand Down Expand Up @@ -125,6 +127,7 @@ pub fn set_options(
file_style,
hunk_header_decoration_style,
hunk_header_style,
hyperlinks,
keep_plus_minus_markers,
max_line_distance,
// Hack: minus-style must come before minus-*emph-style because the latter default
Expand Down Expand Up @@ -501,7 +504,7 @@ fn set_git_config_entries(opt: &mut cli::Opt, git_config: &mut git_config::GitCo
if let Some(style_string) = git_config.get::<String>(key) {
opt.git_config_entries.insert(
key.to_string(),
cli::GitConfigEntry::Style(Style::from_str(
GitConfigEntry::Style(Style::from_str(
&style_string,
None,
None,
Expand All @@ -515,8 +518,10 @@ fn set_git_config_entries(opt: &mut cli::Opt, git_config: &mut git_config::GitCo
// Strings
for key in &["remote.origin.url"] {
if let Some(string) = git_config.get::<String>(key) {
opt.git_config_entries
.insert(key.to_string(), cli::GitConfigEntry::String(string));
if let Ok(repo) = git_config_entry::GitRemoteRepo::from_str(&string) {
opt.git_config_entries
.insert(key.to_string(), GitConfigEntry::GitRemote(repo));
}
}
}
}
Expand Down
31 changes: 22 additions & 9 deletions src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use lazy_static::lazy_static;
use regex::Regex;
use std::borrow::Cow;
use std::path::Path;

use crate::config::Config;
use crate::format;

// https://git-scm.com/docs/git-config#Documentation/git-config.txt-diffmnemonicPrefix
const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"];
Expand Down Expand Up @@ -67,23 +69,34 @@ pub fn get_file_change_description_from_file_paths(
"".to_string()
}
};
let format_file = |file| {
if config.hyperlinks {
Cow::from(format::format_osc8_file_hyperlink(file))
} else {
Cow::from(file)
}
};
match (minus_file, plus_file) {
(minus_file, plus_file) if minus_file == plus_file => format!(
"{}{}",
format_label(&config.file_modified_label),
minus_file
format_file(minus_file)
),
(minus_file, "/dev/null") => format!(
"{}{}",
format_label(&config.file_removed_label),
format_file(minus_file)
),
("/dev/null", plus_file) => format!(
"{}{}",
format_label(&config.file_added_label),
format_file(plus_file)
),
(minus_file, "/dev/null") => {
format!("{}{}", format_label(&config.file_removed_label), minus_file)
}
("/dev/null", plus_file) => {
format!("{}{}", format_label(&config.file_added_label), plus_file)
}
(minus_file, plus_file) => format!(
"{}{} ⟶ {}",
format_label(&config.file_renamed_label),
minus_file,
plus_file
format_file(minus_file),
format_file(plus_file)
),
}
}
Expand Down

0 comments on commit a6440ef

Please sign in to comment.