mirror of
https://github.com/alacritty/alacritty.git
synced 2024-11-18 13:55:23 -05:00
Add support for indexed colors
ANSI escape sequences like `\x1b[48;5;10m` were not supported until now. Specifically, the second attribute, 5, says that the following attribute is a color index. The ref tests were updated since `enum Color` variants changed.
This commit is contained in:
parent
3151ef8625
commit
23e36f1925
15 changed files with 184 additions and 122 deletions
119
src/ansi.rs
119
src/ansi.rs
|
@ -310,7 +310,7 @@ pub enum TabulationClearMode {
|
|||
/// The order here matters since the enum should be castable to a `usize` for
|
||||
/// indexing a color list.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum Color {
|
||||
pub enum NamedColor {
|
||||
/// Black
|
||||
Black = 0,
|
||||
/// Red
|
||||
|
@ -344,11 +344,18 @@ pub enum Color {
|
|||
/// Bright white
|
||||
BrightWhite,
|
||||
/// The foreground color
|
||||
Foreground,
|
||||
Foreground = 256,
|
||||
/// The background color
|
||||
Background,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Color {
|
||||
Named(NamedColor),
|
||||
Spec(Rgb),
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
/// Terminal character attributes
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Attr {
|
||||
|
@ -388,12 +395,8 @@ pub enum Attr {
|
|||
CancelStrike,
|
||||
/// Set indexed foreground color
|
||||
Foreground(Color),
|
||||
/// Set specific foreground color
|
||||
ForegroundSpec(Rgb),
|
||||
/// Set indexed background color
|
||||
Background(Color),
|
||||
/// Set specific background color
|
||||
BackgroundSpec(Rgb),
|
||||
}
|
||||
|
||||
impl<'a, H: Handler + TermInfo + 'a> vte::Perform for Performer<'a, H> {
|
||||
|
@ -575,54 +578,54 @@ impl<'a, H: Handler + TermInfo + 'a> vte::Perform for Performer<'a, H> {
|
|||
27 => Attr::CancelReverse,
|
||||
28 => Attr::CancelHidden,
|
||||
29 => Attr::CancelStrike,
|
||||
30 => Attr::Foreground(Color::Black),
|
||||
31 => Attr::Foreground(Color::Red),
|
||||
32 => Attr::Foreground(Color::Green),
|
||||
33 => Attr::Foreground(Color::Yellow),
|
||||
34 => Attr::Foreground(Color::Blue),
|
||||
35 => Attr::Foreground(Color::Magenta),
|
||||
36 => Attr::Foreground(Color::Cyan),
|
||||
37 => Attr::Foreground(Color::White),
|
||||
30 => Attr::Foreground(Color::Named(NamedColor::Black)),
|
||||
31 => Attr::Foreground(Color::Named(NamedColor::Red)),
|
||||
32 => Attr::Foreground(Color::Named(NamedColor::Green)),
|
||||
33 => Attr::Foreground(Color::Named(NamedColor::Yellow)),
|
||||
34 => Attr::Foreground(Color::Named(NamedColor::Blue)),
|
||||
35 => Attr::Foreground(Color::Named(NamedColor::Magenta)),
|
||||
36 => Attr::Foreground(Color::Named(NamedColor::Cyan)),
|
||||
37 => Attr::Foreground(Color::Named(NamedColor::White)),
|
||||
38 => {
|
||||
if let Some(spec) = parse_color(&args[i..], &mut i) {
|
||||
Attr::ForegroundSpec(spec)
|
||||
if let Some(color) = parse_color(&args[i..], &mut i) {
|
||||
Attr::Foreground(color)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
39 => Attr::Foreground(Color::Foreground),
|
||||
40 => Attr::Background(Color::Black),
|
||||
41 => Attr::Background(Color::Red),
|
||||
42 => Attr::Background(Color::Green),
|
||||
43 => Attr::Background(Color::Yellow),
|
||||
44 => Attr::Background(Color::Blue),
|
||||
45 => Attr::Background(Color::Magenta),
|
||||
46 => Attr::Background(Color::Cyan),
|
||||
47 => Attr::Background(Color::White),
|
||||
39 => Attr::Foreground(Color::Named(NamedColor::Foreground)),
|
||||
40 => Attr::Background(Color::Named(NamedColor::Black)),
|
||||
41 => Attr::Background(Color::Named(NamedColor::Red)),
|
||||
42 => Attr::Background(Color::Named(NamedColor::Green)),
|
||||
43 => Attr::Background(Color::Named(NamedColor::Yellow)),
|
||||
44 => Attr::Background(Color::Named(NamedColor::Blue)),
|
||||
45 => Attr::Background(Color::Named(NamedColor::Magenta)),
|
||||
46 => Attr::Background(Color::Named(NamedColor::Cyan)),
|
||||
47 => Attr::Background(Color::Named(NamedColor::White)),
|
||||
48 => {
|
||||
if let Some(spec) = parse_color(&args[i..], &mut i) {
|
||||
Attr::BackgroundSpec(spec)
|
||||
if let Some(color) = parse_color(&args[i..], &mut i) {
|
||||
Attr::Background(color)
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
49 => Attr::Background(Color::Background),
|
||||
90 => Attr::Foreground(Color::BrightBlack),
|
||||
91 => Attr::Foreground(Color::BrightRed),
|
||||
92 => Attr::Foreground(Color::BrightGreen),
|
||||
93 => Attr::Foreground(Color::BrightYellow),
|
||||
94 => Attr::Foreground(Color::BrightBlue),
|
||||
95 => Attr::Foreground(Color::BrightMagenta),
|
||||
96 => Attr::Foreground(Color::BrightCyan),
|
||||
97 => Attr::Foreground(Color::BrightWhite),
|
||||
100 => Attr::Foreground(Color::BrightBlack),
|
||||
101 => Attr::Foreground(Color::BrightRed),
|
||||
102 => Attr::Foreground(Color::BrightGreen),
|
||||
103 => Attr::Foreground(Color::BrightYellow),
|
||||
104 => Attr::Foreground(Color::BrightBlue),
|
||||
105 => Attr::Foreground(Color::BrightMagenta),
|
||||
106 => Attr::Foreground(Color::BrightCyan),
|
||||
107 => Attr::Foreground(Color::BrightWhite),
|
||||
49 => Attr::Background(Color::Named(NamedColor::Background)),
|
||||
90 => Attr::Foreground(Color::Named(NamedColor::BrightBlack)),
|
||||
91 => Attr::Foreground(Color::Named(NamedColor::BrightRed)),
|
||||
92 => Attr::Foreground(Color::Named(NamedColor::BrightGreen)),
|
||||
93 => Attr::Foreground(Color::Named(NamedColor::BrightYellow)),
|
||||
94 => Attr::Foreground(Color::Named(NamedColor::BrightBlue)),
|
||||
95 => Attr::Foreground(Color::Named(NamedColor::BrightMagenta)),
|
||||
96 => Attr::Foreground(Color::Named(NamedColor::BrightCyan)),
|
||||
97 => Attr::Foreground(Color::Named(NamedColor::BrightWhite)),
|
||||
100 => Attr::Foreground(Color::Named(NamedColor::BrightBlack)),
|
||||
101 => Attr::Foreground(Color::Named(NamedColor::BrightRed)),
|
||||
102 => Attr::Foreground(Color::Named(NamedColor::BrightGreen)),
|
||||
103 => Attr::Foreground(Color::Named(NamedColor::BrightYellow)),
|
||||
104 => Attr::Foreground(Color::Named(NamedColor::BrightBlue)),
|
||||
105 => Attr::Foreground(Color::Named(NamedColor::BrightMagenta)),
|
||||
106 => Attr::Foreground(Color::Named(NamedColor::BrightCyan)),
|
||||
107 => Attr::Foreground(Color::Named(NamedColor::BrightWhite)),
|
||||
_ => unhandled!(),
|
||||
};
|
||||
|
||||
|
@ -674,7 +677,7 @@ impl<'a, H: Handler + TermInfo + 'a> vte::Perform for Performer<'a, H> {
|
|||
|
||||
|
||||
/// Parse a color specifier from list of attributes
|
||||
fn parse_color(attrs: &[i64], i: &mut usize) -> Option<Rgb> {
|
||||
fn parse_color(attrs: &[i64], i: &mut usize) -> Option<Color> {
|
||||
if attrs.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
@ -699,11 +702,29 @@ fn parse_color(attrs: &[i64], i: &mut usize) -> Option<Rgb> {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(Rgb {
|
||||
Some(Color::Spec(Rgb {
|
||||
r: r as u8,
|
||||
g: g as u8,
|
||||
b: b as u8
|
||||
})
|
||||
}))
|
||||
},
|
||||
5 => {
|
||||
if attrs.len() < 3 {
|
||||
err_println!("Expected color index; got {:?}", attrs);
|
||||
None
|
||||
} else {
|
||||
*i = *i + 2;
|
||||
let idx = attrs[*i];
|
||||
match idx {
|
||||
0 ... 255 => {
|
||||
Some(Color::Indexed(idx as u8))
|
||||
},
|
||||
_ => {
|
||||
err_println!("Invalid color index: {}", idx);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
err_println!("Unexpected color attr: {}", attrs[*i+1]);
|
||||
|
@ -863,7 +884,7 @@ pub mod C1 {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use index::{Line, Column};
|
||||
use super::{Processor, Handler, Attr, TermInfo};
|
||||
use super::{Processor, Handler, Attr, TermInfo, Color};
|
||||
use ::Rgb;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -923,7 +944,7 @@ mod tests {
|
|||
b: 255
|
||||
};
|
||||
|
||||
assert_eq!(handler.attr, Some(Attr::ForegroundSpec(spec)));
|
||||
assert_eq!(handler.attr, Some(Attr::Foreground(Color::Spec(spec))));
|
||||
}
|
||||
|
||||
/// No exactly a test; useful for debugging
|
||||
|
|
|
@ -665,34 +665,59 @@ impl Config {
|
|||
///
|
||||
/// The ordering returned here is expected by the terminal. Colors are simply indexed in this
|
||||
/// array for performance.
|
||||
pub fn color_list(&self) -> [Rgb; 18] {
|
||||
let colors = &self.colors;
|
||||
pub fn color_list(&self) -> Vec<Rgb> {
|
||||
let mut colors = Vec::with_capacity(258);
|
||||
|
||||
[
|
||||
// Normals
|
||||
colors.normal.black,
|
||||
colors.normal.red,
|
||||
colors.normal.green,
|
||||
colors.normal.yellow,
|
||||
colors.normal.blue,
|
||||
colors.normal.magenta,
|
||||
colors.normal.cyan,
|
||||
colors.normal.white,
|
||||
// Normals
|
||||
colors.push(self.colors.normal.black);
|
||||
colors.push(self.colors.normal.red);
|
||||
colors.push(self.colors.normal.green);
|
||||
colors.push(self.colors.normal.yellow);
|
||||
colors.push(self.colors.normal.blue);
|
||||
colors.push(self.colors.normal.magenta);
|
||||
colors.push(self.colors.normal.cyan);
|
||||
colors.push(self.colors.normal.white);
|
||||
|
||||
// Brights
|
||||
colors.bright.black,
|
||||
colors.bright.red,
|
||||
colors.bright.green,
|
||||
colors.bright.yellow,
|
||||
colors.bright.blue,
|
||||
colors.bright.magenta,
|
||||
colors.bright.cyan,
|
||||
colors.bright.white,
|
||||
// Brights
|
||||
colors.push(self.colors.bright.black);
|
||||
colors.push(self.colors.bright.red);
|
||||
colors.push(self.colors.bright.green);
|
||||
colors.push(self.colors.bright.yellow);
|
||||
colors.push(self.colors.bright.blue);
|
||||
colors.push(self.colors.bright.magenta);
|
||||
colors.push(self.colors.bright.cyan);
|
||||
colors.push(self.colors.bright.white);
|
||||
|
||||
// Foreground and background
|
||||
colors.primary.foreground,
|
||||
colors.primary.background,
|
||||
]
|
||||
// Build colors
|
||||
for r in 0..6 {
|
||||
for g in 0..6 {
|
||||
for b in 0..6 {
|
||||
colors.push(Rgb {
|
||||
r: if r == 0 { 0 } else { r * 40 + 55 },
|
||||
b: if b == 0 { 0 } else { b * 40 + 55 },
|
||||
g: if g == 0 { 0 } else { g * 40 + 55 },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build grays
|
||||
for i in 0..24 {
|
||||
let value = i * 10 + 8;
|
||||
colors.push(Rgb {
|
||||
r: value,
|
||||
g: value,
|
||||
b: value
|
||||
});
|
||||
}
|
||||
|
||||
debug_assert!(colors.len() == 256);
|
||||
|
||||
// Foreground and background
|
||||
colors.push(self.colors.primary.foreground);
|
||||
colors.push(self.colors.primary.background);
|
||||
|
||||
colors
|
||||
}
|
||||
|
||||
pub fn key_bindings(&self) -> &[KeyBinding] {
|
||||
|
|
|
@ -371,7 +371,7 @@ impl Display {
|
|||
// Draw render timer
|
||||
if self.render_timer {
|
||||
let timing = format!("{:.3} usec", self.meter.average());
|
||||
let color = alacritty::term::cell::Color::Rgb(Rgb { r: 0xd5, g: 0x4e, b: 0x53 });
|
||||
let color = alacritty::ansi::Color::Spec(Rgb { r: 0xd5, g: 0x4e, b: 0x53 });
|
||||
self.renderer.with_api(terminal.size_info(), |mut api| {
|
||||
api.render_string(&timing[..], glyph_cache, &color);
|
||||
});
|
||||
|
|
|
@ -26,6 +26,8 @@ use gl;
|
|||
use notify::{Watcher as WatcherApi, RecommendedWatcher as Watcher, op};
|
||||
use index::{Line, Column};
|
||||
|
||||
use ansi::{Color, NamedColor};
|
||||
|
||||
use config::Config;
|
||||
use term::{self, cell, IndexedCell, Cell};
|
||||
|
||||
|
@ -241,7 +243,7 @@ pub struct QuadRenderer {
|
|||
atlas: Vec<Atlas>,
|
||||
active_tex: GLuint,
|
||||
batch: Batch,
|
||||
colors: [Rgb; 18],
|
||||
colors: Vec<Rgb>,
|
||||
draw_bold_text_with_bright_colors: bool,
|
||||
rx: mpsc::Receiver<Msg>,
|
||||
}
|
||||
|
@ -252,7 +254,7 @@ pub struct RenderApi<'a> {
|
|||
batch: &'a mut Batch,
|
||||
atlas: &'a mut Vec<Atlas>,
|
||||
program: &'a mut ShaderProgram,
|
||||
colors: &'a [Rgb; 18],
|
||||
colors: &'a [Rgb],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -271,7 +273,7 @@ pub struct PackedVertex {
|
|||
pub struct Batch {
|
||||
tex: GLuint,
|
||||
instances: Vec<InstanceData>,
|
||||
colors: [Rgb; 18],
|
||||
colors: Vec<Rgb>,
|
||||
draw_bold_text_with_bright_colors: bool,
|
||||
}
|
||||
|
||||
|
@ -292,22 +294,35 @@ impl Batch {
|
|||
}
|
||||
|
||||
let fg = match cell.fg {
|
||||
::term::cell::Color::Rgb(rgb) => rgb,
|
||||
::term::cell::Color::Ansi(ansi) => {
|
||||
Color::Spec(rgb) => rgb,
|
||||
Color::Named(ansi) => {
|
||||
if self.draw_bold_text_with_bright_colors
|
||||
&& cell.bold()
|
||||
&& ansi < ::ansi::Color::BrightBlack
|
||||
&& ansi < NamedColor::BrightBlack
|
||||
{
|
||||
self.colors[ansi as usize + 8]
|
||||
} else {
|
||||
self.colors[ansi as usize]
|
||||
}
|
||||
},
|
||||
Color::Indexed(idx) => {
|
||||
let idx = if self.draw_bold_text_with_bright_colors
|
||||
&& cell.bold()
|
||||
&& idx < 8
|
||||
{
|
||||
idx + 8
|
||||
} else {
|
||||
idx
|
||||
};
|
||||
|
||||
self.colors[idx as usize]
|
||||
}
|
||||
};
|
||||
|
||||
let bg = match cell.bg {
|
||||
::term::cell::Color::Rgb(rgb) => rgb,
|
||||
::term::cell::Color::Ansi(ansi) => self.colors[ansi as usize],
|
||||
Color::Spec(rgb) => rgb,
|
||||
Color::Named(ansi) => self.colors[ansi as usize],
|
||||
Color::Indexed(idx) => self.colors[idx as usize],
|
||||
};
|
||||
|
||||
self.instances.push(InstanceData {
|
||||
|
@ -620,7 +635,7 @@ impl QuadRenderer {
|
|||
|
||||
impl<'a> RenderApi<'a> {
|
||||
pub fn clear(&self) {
|
||||
let color = self.colors[::ansi::Color::Background as usize];
|
||||
let color = self.colors[NamedColor::Background as usize];
|
||||
unsafe {
|
||||
gl::ClearColor(
|
||||
color.r as f32 / 255.0,
|
||||
|
@ -666,7 +681,7 @@ impl<'a> RenderApi<'a> {
|
|||
&mut self,
|
||||
string: &str,
|
||||
glyph_cache: &mut GlyphCache,
|
||||
color: &::term::cell::Color,
|
||||
color: &Color,
|
||||
) {
|
||||
let line = Line(23);
|
||||
let col = Column(0);
|
||||
|
@ -679,7 +694,7 @@ impl<'a> RenderApi<'a> {
|
|||
inner: Cell {
|
||||
c: c,
|
||||
bg: *color,
|
||||
fg: cell::Color::Rgb(Rgb { r: 0, g: 0, b: 0}),
|
||||
fg: Color::Spec(Rgb { r: 0, g: 0, b: 0}),
|
||||
flags: cell::Flags::empty(),
|
||||
}
|
||||
})
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
|
||||
use std::mem;
|
||||
|
||||
use ansi;
|
||||
use Rgb;
|
||||
use ansi::{NamedColor, Color};
|
||||
|
||||
bitflags! {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -28,12 +27,6 @@ bitflags! {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Color {
|
||||
Rgb(Rgb),
|
||||
Ansi(ansi::Color),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
pub struct Cell {
|
||||
pub c: char,
|
||||
|
@ -59,7 +52,7 @@ impl Cell {
|
|||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.c == ' ' &&
|
||||
self.bg == Color::Ansi(ansi::Color::Background) &&
|
||||
self.bg == Color::Named(NamedColor::Background) &&
|
||||
!self.flags.contains(INVERSE)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ use std::cmp;
|
|||
use ansi::{self, Attr, Handler};
|
||||
use grid::{Grid, ClearRegion};
|
||||
use index::{Cursor, Column, Line};
|
||||
use ansi::Color;
|
||||
use ansi::{Color, NamedColor};
|
||||
|
||||
pub mod cell;
|
||||
pub use self::cell::Cell;
|
||||
|
@ -250,8 +250,8 @@ impl Term {
|
|||
pub fn new(size: SizeInfo) -> Term {
|
||||
let template = Cell::new(
|
||||
' ',
|
||||
cell::Color::Ansi(Color::Foreground),
|
||||
cell::Color::Ansi(Color::Background)
|
||||
Color::Named(NamedColor::Foreground),
|
||||
Color::Named(NamedColor::Background)
|
||||
);
|
||||
|
||||
let num_cols = size.cols();
|
||||
|
@ -800,21 +800,11 @@ impl ansi::Handler for Term {
|
|||
fn terminal_attribute(&mut self, attr: Attr) {
|
||||
debug_println!("Set Attribute: {:?}", attr);
|
||||
match attr {
|
||||
Attr::Foreground(named_color) => {
|
||||
self.template_cell.fg = cell::Color::Ansi(named_color);
|
||||
},
|
||||
Attr::Background(named_color) => {
|
||||
self.template_cell.bg = cell::Color::Ansi(named_color);
|
||||
},
|
||||
Attr::ForegroundSpec(rgb) => {
|
||||
self.template_cell.fg = cell::Color::Rgb(rgb);
|
||||
},
|
||||
Attr::BackgroundSpec(rgb) => {
|
||||
self.template_cell.bg = cell::Color::Rgb(rgb);
|
||||
},
|
||||
Attr::Foreground(color) => self.template_cell.fg = color,
|
||||
Attr::Background(color) => self.template_cell.bg = color,
|
||||
Attr::Reset => {
|
||||
self.template_cell.fg = cell::Color::Ansi(Color::Foreground);
|
||||
self.template_cell.bg = cell::Color::Ansi(Color::Background);
|
||||
self.template_cell.fg = Color::Named(NamedColor::Foreground);
|
||||
self.template_cell.bg = Color::Named(NamedColor::Background);
|
||||
self.template_cell.flags = cell::Flags::empty();
|
||||
},
|
||||
Attr::Reverse => self.template_cell.flags.insert(cell::INVERSE),
|
||||
|
@ -888,10 +878,10 @@ mod tests {
|
|||
|
||||
use super::limit;
|
||||
|
||||
use ansi::{Color};
|
||||
use ansi::{Color, NamedColor};
|
||||
use grid::Grid;
|
||||
use index::{Line, Column};
|
||||
use term::{cell, Cell};
|
||||
use term::{Cell};
|
||||
|
||||
/// Check that the grid can be serialized back and forth losslessly
|
||||
///
|
||||
|
@ -901,8 +891,8 @@ mod tests {
|
|||
fn grid_serde() {
|
||||
let template = Cell::new(
|
||||
' ',
|
||||
cell::Color::Ansi(Color::Foreground),
|
||||
cell::Color::Ansi(Color::Background)
|
||||
Color::Named(NamedColor::Foreground),
|
||||
Color::Named(NamedColor::Background)
|
||||
);
|
||||
|
||||
let grid = Grid::new(Line(24), Column(80), &template);
|
||||
|
|
|
@ -79,6 +79,7 @@ mod reference {
|
|||
vim_simple_edit,
|
||||
tmux_htop,
|
||||
tmux_git_log,
|
||||
vim_large_window_scroll
|
||||
vim_large_window_scroll,
|
||||
indexed_256_colors
|
||||
}
|
||||
}
|
||||
|
|
15
tests/ref/indexed_256_colors/alacritty.recording
Normal file
15
tests/ref/indexed_256_colors/alacritty.recording
Normal file
File diff suppressed because one or more lines are too long
1
tests/ref/indexed_256_colors/grid.json
Normal file
1
tests/ref/indexed_256_colors/grid.json
Normal file
File diff suppressed because one or more lines are too long
1
tests/ref/indexed_256_colors/size.json
Normal file
1
tests/ref/indexed_256_colors/size.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"width":1124.0,"height":628.0,"cell_width":14.0,"cell_height":26.0}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue