Merge pull request #102 from eth-p/master

Added text wrapping. (Fixes #54)
This commit is contained in:
David Peter 2018-05-16 08:41:49 +02:00 committed by GitHub
commit 2eee68599d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 497 additions and 247 deletions

View File

@ -4,7 +4,7 @@ use console::Term;
use errors::*;
use std::collections::HashSet;
use std::env;
use style::{OutputComponent, OutputComponents};
use style::{OutputComponent, OutputComponents, OutputWrap};
#[cfg(windows)]
use ansi_term;
@ -86,6 +86,14 @@ impl App {
.default_value("auto")
.help("When to use the pager"),
)
.arg(
Arg::with_name("wrap")
.long("wrap")
.takes_value(true)
.possible_values(&["character", "never"])
.default_value("character")
.help("When to wrap text"),
)
.arg(
Arg::with_name("list-languages")
.long("list-languages")
@ -141,6 +149,16 @@ impl App {
true_color: is_truecolor_terminal(),
output_components: self.output_components()?,
language: self.matches.value_of("language"),
output_wrap: if !self.interactive_output {
// We don't have the tty width when piping to another program.
// There's no point in wrapping when this is the case.
OutputWrap::None
} else {
match self.matches.value_of("wrap") {
Some("character") => OutputWrap::Character,
Some("never") | _ => OutputWrap::None,
}
},
colored_output: match self.matches.value_of("color") {
Some("always") => true,
Some("never") => false,
@ -197,6 +215,7 @@ impl App {
pub struct Config<'a> {
pub true_color: bool,
pub output_wrap: OutputWrap,
pub output_components: OutputComponents,
pub language: Option<&'a str>,
pub colored_output: bool,

154
src/decorations.rs Normal file
View File

@ -0,0 +1,154 @@
use ansi_term::Style;
use diff::LineChange;
use printer::Printer;
use Colors;
#[derive(Clone)]
pub struct DecorationText {
pub width: usize,
pub text: String,
}
pub trait Decoration {
fn generate(&self, line_number: usize, continuation: bool, printer: &Printer)
-> DecorationText;
fn width(&self) -> usize;
}
// Line number decoration.
pub struct LineNumberDecoration {
color: Style,
cached_wrap: DecorationText,
cached_wrap_invalid_at: usize,
}
impl LineNumberDecoration {
pub fn new(colors: &Colors) -> Self {
LineNumberDecoration {
color: colors.line_number,
cached_wrap_invalid_at: 10000,
cached_wrap: DecorationText {
text: colors.line_number.paint(" ".repeat(4)).to_string(),
width: 4,
},
}
}
}
impl Decoration for LineNumberDecoration {
fn generate(
&self,
line_number: usize,
continuation: bool,
_printer: &Printer,
) -> DecorationText {
if continuation {
if line_number > self.cached_wrap_invalid_at {
let new_width = self.cached_wrap.width + 1;
return DecorationText {
text: self.color.paint(" ".repeat(new_width)).to_string(),
width: new_width,
};
}
self.cached_wrap.clone()
} else {
let plain: String = format!("{:4}", line_number);
DecorationText {
width: plain.len(),
text: self.color.paint(plain).to_string(),
}
}
}
fn width(&self) -> usize {
4
}
}
// Line changes decoration.
pub struct LineChangesDecoration {
cached_none: DecorationText,
cached_added: DecorationText,
cached_removed_above: DecorationText,
cached_removed_below: DecorationText,
cached_modified: DecorationText,
}
impl LineChangesDecoration {
#[inline]
fn generate_cached(style: Style, text: &str) -> DecorationText {
DecorationText {
text: style.paint(text).to_string(),
width: text.chars().count(),
}
}
pub fn new(colors: &Colors) -> Self {
LineChangesDecoration {
cached_none: Self::generate_cached(Style::default(), " "),
cached_added: Self::generate_cached(colors.git_added, "+"),
cached_removed_above: Self::generate_cached(colors.git_removed, ""),
cached_removed_below: Self::generate_cached(colors.git_removed, "_"),
cached_modified: Self::generate_cached(colors.git_modified, "~"),
}
}
}
impl Decoration for LineChangesDecoration {
fn generate(
&self,
line_number: usize,
continuation: bool,
printer: &Printer,
) -> DecorationText {
if !continuation {
if let Some(ref changes) = printer.line_changes {
return match changes.get(&(line_number as u32)) {
Some(&LineChange::Added) => self.cached_added.clone(),
Some(&LineChange::RemovedAbove) => self.cached_removed_above.clone(),
Some(&LineChange::RemovedBelow) => self.cached_removed_below.clone(),
Some(&LineChange::Modified) => self.cached_modified.clone(),
_ => self.cached_none.clone(),
};
}
}
self.cached_none.clone()
}
fn width(&self) -> usize {
self.cached_none.width
}
}
// Grid border decoration.
pub struct GridBorderDecoration {
cached: DecorationText,
}
impl GridBorderDecoration {
pub fn new(colors: &Colors) -> Self {
GridBorderDecoration {
cached: DecorationText {
text: colors.grid.paint("").to_string(),
width: 1,
},
}
}
}
impl Decoration for GridBorderDecoration {
fn generate(
&self,
_line_number: usize,
_continuation: bool,
_printer: &Printer,
) -> DecorationText {
self.cached.clone()
}
fn width(&self) -> usize {
self.cached.width
}
}

View File

@ -19,6 +19,7 @@ extern crate syntect;
mod app;
mod assets;
mod decorations;
mod diff;
mod printer;
mod style;

View File

@ -1,18 +1,21 @@
use ansi_term::Style;
use app::Config;
use diff::{LineChange, LineChanges};
use decorations::{Decoration, GridBorderDecoration, LineChangesDecoration, LineNumberDecoration};
use diff::LineChanges;
use errors::*;
use std::boxed::Box;
use std::io::Write;
use std::vec::Vec;
use style::OutputWrap;
use syntect::highlighting;
use terminal::as_terminal_escaped;
use Colors;
const PANEL_WIDTH: usize = 7;
pub struct Printer<'a> {
handle: &'a mut Write,
colors: Colors,
config: &'a Config<'a>,
decorations: Vec<Box<Decoration>>,
panel_width: usize,
pub line_changes: Option<LineChanges>,
}
@ -24,10 +27,43 @@ impl<'a> Printer<'a> {
Colors::plain()
};
// Create decorations.
let mut decorations: Vec<Box<Decoration>> = Vec::new();
if config.output_components.numbers() {
decorations.push(Box::new(LineNumberDecoration::new(&colors)));
}
if config.output_components.changes() {
decorations.push(Box::new(LineChangesDecoration::new(&colors)));
}
let mut panel_width: usize =
decorations.len() + decorations.iter().fold(0, |a, x| a + x.width());
// The grid border decoration isn't added until after the panel_width calculation, since the
// print_horizontal_line, print_header, and print_footer functions all assume the panel
// width is without the grid border.
if config.output_components.grid() && decorations.len() > 0 {
decorations.push(Box::new(GridBorderDecoration::new(&colors)));
}
// Disable the panel if the terminal is too small (i.e. can't fit 5 characters with the
// panel showing).
if config.term_width
< (decorations.len() + decorations.iter().fold(0, |a, x| a + x.width())) + 5
{
decorations.clear();
panel_width = 0;
}
// Create printer.
Printer {
panel_width,
handle,
colors,
config,
decorations,
line_changes: None,
}
}
@ -43,8 +79,10 @@ impl<'a> Printer<'a> {
write!(
self.handle,
"{}{} ",
" ".repeat(PANEL_WIDTH),
self.colors.grid.paint(""),
" ".repeat(self.panel_width),
self.colors
.grid
.paint(if self.panel_width > 0 { "" } else { "" }),
)?;
}
@ -75,85 +113,123 @@ impl<'a> Printer<'a> {
line_number: usize,
regions: &[(highlighting::Style, &str)],
) -> Result<()> {
let decorations = vec![
self.print_line_number(line_number),
self.print_git_marker(line_number),
self.print_line_border(),
Some(as_terminal_escaped(
&regions,
self.config.true_color,
self.config.colored_output,
)),
];
let mut cursor: usize = 0;
let mut cursor_max: usize = self.config.term_width;
let mut panel_wrap: Option<String> = None;
let grid_requested = self.config.output_components.grid();
write!(
self.handle,
"{}",
decorations
.into_iter()
.filter_map(|dec| if grid_requested {
Some(dec.unwrap_or_else(|| " ".to_owned()))
} else {
dec
})
.collect::<Vec<_>>()
.join(" ")
)?;
// Line decorations.
if self.panel_width > 0 {
let decorations = self
.decorations
.iter()
.map(|ref d| d.generate(line_number, false, self))
.collect::<Vec<_>>();
for deco in decorations {
write!(self.handle, "{} ", deco.text)?;
cursor_max -= deco.width + 1;
}
}
// Line contents.
if self.config.output_wrap == OutputWrap::None {
let true_color = self.config.true_color;
let colored_output = self.config.colored_output;
write!(
self.handle,
"{}",
regions
.iter()
.map(|&(style, text)| as_terminal_escaped(
style,
text,
true_color,
colored_output,
))
.collect::<Vec<_>>()
.join("")
)?;
} else {
for &(style, text) in regions.iter() {
let text = text.trim_right_matches(|c| c == '\r' || c == '\n');
let mut chars = text.chars();
let mut remaining = text.chars().count();
while remaining > 0 {
let available = cursor_max - cursor;
// It fits.
if remaining <= available {
let text = chars.by_ref().take(remaining).collect::<String>();
cursor += remaining;
write!(
self.handle,
"{}",
as_terminal_escaped(
style,
&*text,
self.config.true_color,
self.config.colored_output,
)
)?;
break;
}
// Generate wrap padding if not already generated.
if panel_wrap.is_none() {
panel_wrap = if self.panel_width > 0 {
Some(format!(
"{} ",
self.decorations
.iter()
.map(|ref d| d.generate(line_number, true, self).text)
.collect::<Vec<String>>()
.join(" ")
))
} else {
Some("".to_string())
}
}
// It wraps.
let text = chars.by_ref().take(available).collect::<String>();
cursor = 0;
remaining -= available;
write!(
self.handle,
"{}\n{}",
as_terminal_escaped(
style,
&*text,
self.config.true_color,
self.config.colored_output,
),
panel_wrap.clone().unwrap()
)?;
}
}
write!(self.handle, "\n")?;
}
Ok(())
}
fn print_line_number(&self, line_number: usize) -> Option<String> {
if self.config.output_components.numbers() {
Some(
self.colors
.line_number
.paint(format!("{:4}", line_number))
.to_string(),
)
} else if self.config.output_components.grid() {
Some(" ".to_owned())
} else {
None
}
}
fn print_git_marker(&self, line_number: usize) -> Option<String> {
if self.config.output_components.changes() {
Some(
if let Some(ref changes) = self.line_changes {
match changes.get(&(line_number as u32)) {
Some(&LineChange::Added) => self.colors.git_added.paint("+"),
Some(&LineChange::RemovedAbove) => self.colors.git_removed.paint(""),
Some(&LineChange::RemovedBelow) => self.colors.git_removed.paint("_"),
Some(&LineChange::Modified) => self.colors.git_modified.paint("~"),
_ => Style::default().paint(" "),
}
} else {
Style::default().paint(" ")
}.to_string(),
)
} else if self.config.output_components.grid() {
Some(" ".to_owned())
} else {
None
}
}
fn print_line_border(&self) -> Option<String> {
if self.config.output_components.grid() {
Some(self.colors.grid.paint("").to_string())
} else {
None
}
}
fn print_horizontal_line(&mut self, grid_char: char) -> Result<()> {
let hline = "".repeat(self.config.term_width - (PANEL_WIDTH + 1));
let hline = format!("{}{}{}", "".repeat(PANEL_WIDTH), grid_char, hline);
writeln!(self.handle, "{}", self.colors.grid.paint(hline))?;
if self.panel_width == 0 {
writeln!(
self.handle,
"{}",
self.colors.grid.paint("".repeat(self.config.term_width))
)?;
} else {
let hline = "".repeat(self.config.term_width - (self.panel_width + 1));
let hline = format!("{}{}{}", "".repeat(self.panel_width), grid_char, hline);
writeln!(self.handle, "{}", self.colors.grid.paint(hline))?;
}
Ok(())
}

View File

@ -13,6 +13,12 @@ pub enum OutputComponent {
Plain,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub enum OutputWrap {
Character,
None,
}
impl OutputComponent {
pub fn components(&self, interactive_terminal: bool) -> &'static [OutputComponent] {
match *self {

View File

@ -1,5 +1,3 @@
use std::fmt::Write;
use ansi_term::Colour::{Fixed, RGB};
use ansi_term::Style;
use syntect::highlighting::{self, FontStyle};
@ -27,37 +25,33 @@ fn rgb2ansi(r: u8, g: u8, b: u8) -> u8 {
}
pub fn as_terminal_escaped(
v: &[(highlighting::Style, &str)],
style: highlighting::Style,
text: &str,
true_color: bool,
colored: bool,
) -> String {
let mut s: String = String::new();
for &(ref style, text) in v.iter() {
let style = if !colored {
Style::default()
let style = if !colored {
Style::default()
} else {
let color = if true_color {
RGB(style.foreground.r, style.foreground.g, style.foreground.b)
} else {
let color = if true_color {
RGB(style.foreground.r, style.foreground.g, style.foreground.b)
} else {
let ansi = rgb2ansi(style.foreground.r, style.foreground.g, style.foreground.b);
Fixed(ansi)
};
if style.font_style.contains(FontStyle::BOLD) {
color.bold()
} else if style.font_style.contains(FontStyle::UNDERLINE) {
color.underline()
} else if style.font_style.contains(FontStyle::ITALIC) {
color.italic()
} else {
color.normal()
}
let ansi = rgb2ansi(style.foreground.r, style.foreground.g, style.foreground.b);
Fixed(ansi)
};
write!(s, "{}", style.paint(text)).unwrap();
}
if style.font_style.contains(FontStyle::BOLD) {
color.bold()
} else if style.font_style.contains(FontStyle::UNDERLINE) {
color.underline()
} else if style.font_style.contains(FontStyle::ITALIC) {
color.italic()
} else {
color.normal()
}
};
s
style.paint(text).to_string()
}
#[test]

View File

@ -1,25 +1,25 @@
───────────────────────────────────────────────────────────────────────────────
│ File: sample.rs
───────────────────────────────────────────────────────────────────────────────
│ struct Rectangle {
│ width: u32,
│ height: u32,
│ }
_ │ fn main() {
│ let rect1 = Rectangle { width: 30, height: 50 };
│ println!(
~ │ "The perimeter of the rectangle is {} pixels.",
~ │ perimeter(&rect1)
│ );
│ }
│ fn area(rectangle: &Rectangle) -> u32 {
│ rectangle.width * rectangle.height
│ }
+ │
+ │ fn perimeter(rectangle: &Rectangle) -> u32 {
+ │ (rectangle.width + rectangle.height) * 2
+ │ }
───────────────────────────────────────────────────────────────────────────────
───────────────────────────────────────────────────────────────────────────────
│ File: sample.rs
───────────────────────────────────────────────────────────────────────────────
│ struct Rectangle {
│ width: u32,
│ height: u32,
│ }
_ │ fn main() {
│ let rect1 = Rectangle { width: 30, height: 50 };
│ println!(
~ │ "The perimeter of the rectangle is {} pixels.",
~ │ perimeter(&rect1)
│ );
│ }
│ fn area(rectangle: &Rectangle) -> u32 {
│ rectangle.width * rectangle.height
│ }
+ │
+ │ fn perimeter(rectangle: &Rectangle) -> u32 {
+ │ (rectangle.width + rectangle.height) * 2
+ │ }
───────────────────────────────────────────────────────────────────────────────

View File

@ -1,22 +1,22 @@
│ struct Rectangle {
│ width: u32,
│ height: u32,
│ }
_ │ fn main() {
│ let rect1 = Rectangle { width: 30, height: 50 };
│ println!(
~ │ "The perimeter of the rectangle is {} pixels.",
~ │ perimeter(&rect1)
│ );
│ }
│ fn area(rectangle: &Rectangle) -> u32 {
│ rectangle.width * rectangle.height
│ }
+ │
+ │ fn perimeter(rectangle: &Rectangle) -> u32 {
+ │ (rectangle.width + rectangle.height) * 2
+ │ }
───────────────────────────────────────────────────────────────────────────────
│ struct Rectangle {
│ width: u32,
│ height: u32,
│ }
_ │ fn main() {
│ let rect1 = Rectangle { width: 30, height: 50 };
│ println!(
~ │ "The perimeter of the rectangle is {} pixels.",
~ │ perimeter(&rect1)
│ );
│ }
│ fn area(rectangle: &Rectangle) -> u32 {
│ rectangle.width * rectangle.height
│ }
+ │
+ │ fn perimeter(rectangle: &Rectangle) -> u32 {
+ │ (rectangle.width + rectangle.height) * 2
+ │ }
───────────────────────────────────────────────────────────────────────────────

View File

@ -1,25 +1,25 @@
───────────────────────────────────────────────────────────────────────────────
│ File: sample.rs
───────────────────────────────────────────────────────────────────────────────
1 │ struct Rectangle {
2 │ width: u32,
3 │ height: u32,
4 │ }
5
6 │ fn main() {
7 │ let rect1 = Rectangle { width: 30, height: 50 };
8
9 │ println!(
10 │ "The perimeter of the rectangle is {} pixels.",
11 │ perimeter(&rect1)
12 │ );
13 │ }
14
15 │ fn area(rectangle: &Rectangle) -> u32 {
16 │ rectangle.width * rectangle.height
17 │ }
18
19 │ fn perimeter(rectangle: &Rectangle) -> u32 {
20 │ (rectangle.width + rectangle.height) * 2
21 │ }
───────────────────────────────────────────────────────────────────────────────
───────────────────────────────────────────────────────────────────────────────
│ File: sample.rs
───────────────────────────────────────────────────────────────────────────────
1 │ struct Rectangle {
2 │ width: u32,
3 │ height: u32,
4 │ }
5 │
6 │ fn main() {
7 │ let rect1 = Rectangle { width: 30, height: 50 };
8 │
9 │ println!(
10 │ "The perimeter of the rectangle is {} pixels.",
11 │ perimeter(&rect1)
12 │ );
13 │ }
14 │
15 │ fn area(rectangle: &Rectangle) -> u32 {
16 │ rectangle.width * rectangle.height
17 │ }
18 │
19 │ fn perimeter(rectangle: &Rectangle) -> u32 {
20 │ (rectangle.width + rectangle.height) * 2
21 │ }
───────────────────────────────────────────────────────────────────────────────

View File

@ -1,25 +1,25 @@
───────────────────────────────────────────────────────────────────────────────
File: sample.rs
───────────────────────────────────────────────────────────────────────────────
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The perimeter of the rectangle is {} pixels.",
perimeter(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn perimeter(rectangle: &Rectangle) -> u32 {
(rectangle.width + rectangle.height) * 2
}
───────────────────────────────────────────────────────────────────────────────
───────────────────────────────────────────────────────────────────────────────
File: sample.rs
───────────────────────────────────────────────────────────────────────────────
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The perimeter of the rectangle is {} pixels.",
perimeter(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn perimeter(rectangle: &Rectangle) -> u32 {
(rectangle.width + rectangle.height) * 2
}
───────────────────────────────────────────────────────────────────────────────

View File

@ -1,22 +1,22 @@
1 │ struct Rectangle {
2 │ width: u32,
3 │ height: u32,
4 │ }
5
6 │ fn main() {
7 │ let rect1 = Rectangle { width: 30, height: 50 };
8
9 │ println!(
10 │ "The perimeter of the rectangle is {} pixels.",
11 │ perimeter(&rect1)
12 │ );
13 │ }
14
15 │ fn area(rectangle: &Rectangle) -> u32 {
16 │ rectangle.width * rectangle.height
17 │ }
18
19 │ fn perimeter(rectangle: &Rectangle) -> u32 {
20 │ (rectangle.width + rectangle.height) * 2
21 │ }
───────────────────────────────────────────────────────────────────────────────
1 │ struct Rectangle {
2 │ width: u32,
3 │ height: u32,
4 │ }
5 │
6 │ fn main() {
7 │ let rect1 = Rectangle { width: 30, height: 50 };
8 │
9 │ println!(
10 │ "The perimeter of the rectangle is {} pixels.",
11 │ perimeter(&rect1)
12 │ );
13 │ }
14 │
15 │ fn area(rectangle: &Rectangle) -> u32 {
16 │ rectangle.width * rectangle.height
17 │ }
18 │
19 │ fn perimeter(rectangle: &Rectangle) -> u32 {
20 │ (rectangle.width + rectangle.height) * 2
21 │ }
───────────────────────────────────────────────────────────────────────────────

View File

@ -1,22 +1,22 @@
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The perimeter of the rectangle is {} pixels.",
perimeter(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn perimeter(rectangle: &Rectangle) -> u32 {
(rectangle.width + rectangle.height) * 2
}
───────────────────────────────────────────────────────────────────────────────
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The perimeter of the rectangle is {} pixels.",
perimeter(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
fn perimeter(rectangle: &Rectangle) -> u32 {
(rectangle.width + rectangle.height) * 2
}
───────────────────────────────────────────────────────────────────────────────