Support color ANSI escape sequences in /etc/issue.

This commit is contained in:
Antoine POPINEAU 2024-08-06 18:16:05 +02:00
parent f0ef8921b0
commit 75c2706f0b
No known key found for this signature in database
GPG Key ID: E8379674E92D25D2
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);
}
}