Support color ANSI escape sequences in /etc/issue.

This commit is contained in:
Antoine POPINEAU 2024-08-06 18:16:05 +02:00 committed by Antoine POPINEAU
parent 51e7a60b7a
commit 79e3d0ed26
10 changed files with 451 additions and 286 deletions

546
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@ default = []
nsswrapper = []
[dependencies]
ansi-to-tui = "4.1.0"
chrono = { version = "^0.4", features = ["unstable-locales"] }
crossterm = { version = "^0.27", features = ["event-stream"] }
futures = "0.3"
@ -24,11 +25,11 @@ lazy_static = "^1.4"
nix = { version = "^0.28", features = ["feature"] }
tui = { package = "ratatui", version = "^0.26", default-features = false, features = [
"crossterm",
"unstable"
] }
rust-embed = "^8.0"
rust-ini = "^0.21"
smart-default = "^0.7"
textwrap = "^0.16"
tokio = { version = "^1.2", default-features = false, features = [
"macros",
"rt-multi-thread",

View File

@ -63,6 +63,9 @@ pub fn get_issue() -> Option<String> {
.replace("\\v", uts.version().to_str().unwrap_or(""))
.replace("\\n", uts.nodename().to_str().unwrap_or(""))
.replace("\\m", uts.machine().to_str().unwrap_or(""))
.replace("\\x1b", "\x1b")
.replace("\\033", "\x1b")
.replace("\\e", "\x1b")
.replace("\\\\", "\\"),
),

View File

@ -62,7 +62,6 @@ pub fn output(buffer: &Arc<Mutex<Buffer>>) -> String {
for cells in buffer.content.chunks(buffer.area.width as usize) {
let mut overwritten = vec![];
let mut skip: usize = 0;
view.push('"');
for (x, c) in cells.iter().enumerate() {
if skip == 0 {
view.push_str(c.symbol());
@ -71,7 +70,6 @@ pub fn output(buffer: &Arc<Mutex<Buffer>>) -> String {
}
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
}
view.push('"');
if !overwritten.is_empty() {
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
}

View File

@ -1,4 +1,5 @@
mod backend;
mod output;
use std::{
panic,
@ -24,9 +25,12 @@ use crate::{
Greeter,
};
pub use self::backend::{output, TestBackend};
pub(super) use self::{
backend::{output, TestBackend},
output::*,
};
pub struct IntegrationRunner(Arc<RwLock<_IntegrationRunner>>);
pub(super) struct IntegrationRunner(Arc<RwLock<_IntegrationRunner>>);
struct _IntegrationRunner {
server: Option<JoinHandle<()>>,
@ -45,9 +49,13 @@ impl Clone for IntegrationRunner {
impl IntegrationRunner {
pub async fn new(opts: SessionOptions, builder: Option<fn(&mut Greeter)>) -> IntegrationRunner {
IntegrationRunner::new_with_size(opts, builder, (200, 40)).await
}
pub async fn new_with_size(opts: SessionOptions, builder: Option<fn(&mut Greeter)>, size: (u16, u16)) -> IntegrationRunner {
let socket = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
let (backend, buffer, tick) = TestBackend::new(200, 200);
let (backend, buffer, tick) = TestBackend::new(size.0, size.1);
let events = Events::new().await;
let sender = events.sender();
@ -159,8 +167,8 @@ impl IntegrationRunner {
self.0.write().await.tick.recv().await;
}
pub async fn output(&self) -> String {
output(&self.0.read().await.buffer)
pub async fn output(&self) -> Output {
Output(output(&self.0.read().await.buffer))
}
}

View File

@ -0,0 +1,26 @@
use std::ops::Deref;
pub(in crate::integration) struct Output(pub String);
impl Deref for Output {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(dead_code)]
impl Output {
pub fn debug_print(&self) {
for line in self.lines() {
println!("{}", line);
}
}
pub fn debug_inspect(&self) {
for line in self.lines() {
println!("{:?}", line.as_bytes().iter().map(|c| *c as char).collect::<Vec<char>>());
}
}
}

View File

@ -34,6 +34,41 @@ async fn show_greet() {
runner.join_until_end(events).await;
}
#[tokio::test]
async fn show_wrapped_greet() {
let opts = SessionOptions {
username: "apognu".to_string(),
password: "password".to_string(),
mfa: false,
};
let mut runner = IntegrationRunner::new_with_size(
opts,
Some(|greeter| {
greeter.greeting = Some("Lorem \x1b[31mipsum dolor sit amet".to_string());
}),
(20, 20),
)
.await;
let events = tokio::task::spawn({
let mut runner = runner.clone();
async move {
runner.wait_for_render().await;
let output = runner.output().await;
assert!(output.contains("┌ Authenticate into┐"));
assert!(output.contains("│ Lorem ipsum │"));
assert!(output.contains("│ dolor sit amet │"));
assert!(output.contains("└──────────────────┘"));
}
});
runner.join_until_end(events).await;
}
const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
// TODO

View File

@ -323,8 +323,6 @@ mod test {
let mut greeter = Greeter::default();
greeter.xsession_wrapper = Some("startx /usr/bin/env".into());
println!("{:?}", greeter.xsession_wrapper);
let session = Session {
slug: Some("thede".to_string()),
name: "Session1".into(),

View File

@ -3,14 +3,14 @@ use std::error::Error;
use rand::{prelude::StdRng, Rng, SeedableRng};
use tui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
text::{Span, Text},
text::Span,
widgets::{Block, BorderType, Borders, Paragraph},
};
use crate::{
info::get_hostname,
ui::{prompt_value, util::*, Frame},
Greeter, Mode, SecretDisplay, GreetAlign
GreetAlign, Greeter, Mode, SecretDisplay,
};
use super::common::style::Themed;
@ -30,7 +30,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
let greeting_alignment = match greeter.greet_align() {
GreetAlign::Center => Alignment::Center,
GreetAlign::Left => Alignment::Left,
GreetAlign::Right => Alignment::Right
GreetAlign::Right => Alignment::Right,
};
let container = Rect::new(x, y, width, height);
@ -62,9 +62,8 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
let chunks = Layout::default().direction(Direction::Vertical).constraints(constraints.as_ref()).split(frame);
let cursor = chunks[USERNAME_INDEX];
if let Some(greeting) = &greeting {
let greeting_text = greeting.trim_end();
let greeting_label = Paragraph::new(greeting_text).alignment(greeting_alignment).style(theme.of(&[Themed::Greet]));
if let Some(greeting) = greeting {
let greeting_label = greeting.alignment(greeting_alignment).style(theme.of(&[Themed::Greet]));
f.render_widget(greeting_label, chunks[GREETING_INDEX]);
}
@ -137,8 +136,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
}
if let Some(message) = message {
let message_text = Text::from(message);
let message = Paragraph::new(message_text).alignment(Alignment::Center);
let message = message.alignment(Alignment::Center);
f.render_widget(message, Rect::new(x, y + height, width, message_height));
}

View File

@ -1,4 +1,9 @@
use tui::prelude::Rect;
use ansi_to_tui::IntoText;
use tui::{
prelude::Rect,
text::Text,
widgets::{Paragraph, Wrap},
};
use crate::{Greeter, Mode};
@ -96,25 +101,31 @@ pub fn get_cursor_offset(greeter: &mut Greeter, length: usize) -> i16 {
offset
}
pub fn get_greeting_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Option<String>, u16) {
pub fn get_greeting_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Option<Paragraph>, u16) {
if let Some(greeting) = &greeter.greeting {
let width = greeter.width();
let wrapped = textwrap::fill(greeting, (width - (2 * padding)) as usize);
let height = wrapped.trim_end().matches('\n').count();
(Some(wrapped), height as u16 + 2)
let text = match greeting.clone().trim().into_text() {
Ok(text) => text,
Err(_) => Text::raw(greeting),
};
let paragraph = Paragraph::new(text.clone()).wrap(Wrap { trim: true });
let height = paragraph.line_count(width - (2 * padding)) + 1;
(Some(paragraph), height as u16)
} else {
(None, fallback)
}
}
pub fn get_message_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Option<String>, u16) {
pub fn get_message_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Option<Paragraph>, u16) {
if let Some(message) = &greeter.message {
let width = greeter.width();
let wrapped = textwrap::fill(message.trim_end(), width as usize - 4);
let height = wrapped.trim_end().matches('\n').count();
let paragraph = Paragraph::new(message.trim_end()).wrap(Wrap { trim: true });
let height = paragraph.line_count(width - 4);
(Some(wrapped), height as u16 + padding)
(Some(paragraph), height as u16 + padding)
} else {
(None, fallback)
}
@ -122,7 +133,12 @@ pub fn get_message_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Op
#[cfg(test)]
mod test {
use tui::prelude::Rect;
use tui::{
prelude::Rect,
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Paragraph, Wrap},
};
use crate::{
ui::util::{get_greeting_height, get_height},
@ -243,24 +259,58 @@ mod test {
#[test]
fn greeting_height_one_line() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "10", "--container-padding", "1"]).ok();
greeter.greeting = Some("Hello".into());
greeter.config = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok();
greeter.greeting = Some("Hello World".into());
let (text, width) = get_greeting_height(&greeter, 1, 0);
let (_, height) = get_greeting_height(&greeter, 1, 0);
assert!(matches!(text.as_deref(), Some("Hello")));
assert_eq!(width, 2);
assert_eq!(height, 2);
}
#[test]
fn greeting_height_two_lines() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "10", "--container-padding", "1"]).ok();
greeter.config = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok();
greeter.greeting = Some("Hello World".into());
let (text, width) = get_greeting_height(&greeter, 1, 0);
let (_, height) = get_greeting_height(&greeter, 1, 0);
assert!(matches!(text.as_deref(), Some("Hello\nWorld")));
assert_eq!(width, 3);
assert_eq!(height, 3);
}
#[test]
fn ansi_greeting_height_one_line() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "15", "--container-padding", "1"]).ok();
greeter.greeting = Some("\x1b[31mHello\x1b[0m World".into());
let (text, height) = get_greeting_height(&greeter, 1, 0);
let expected = Paragraph::new(Text::from(vec![Line::from(vec![
Span::styled("Hello", Style::default().fg(Color::Red)),
Span::styled(" World", Style::reset()),
])]))
.wrap(Wrap { trim: true });
assert_eq!(text, Some(expected));
assert_eq!(height, 2);
}
#[test]
fn ansi_greeting_height_two_lines() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "8", "--container-padding", "1"]).ok();
greeter.greeting = Some("\x1b[31mHello\x1b[0m World".into());
let (text, height) = get_greeting_height(&greeter, 1, 0);
let expected = Paragraph::new(Text::from(vec![Line::from(vec![
Span::styled("Hello", Style::default().fg(Color::Red)),
Span::styled(" World", Style::reset()),
])]))
.wrap(Wrap { trim: true });
assert_eq!(text, Some(expected));
assert_eq!(height, 3);
}
}