Skip to content

Commit

Permalink
Support color ANSI escape sequences in /etc/issue.
Browse files Browse the repository at this point in the history
  • Loading branch information
apognu committed Aug 6, 2024
1 parent f0ef892 commit 75c2706
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 284 deletions.
542 changes: 295 additions & 247 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("\\\\", "\\"),
),

Expand Down
2 changes: 0 additions & 2 deletions src/integration/common/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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();
}
Expand Down
18 changes: 13 additions & 5 deletions src/integration/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod backend;
mod output;

use std::{
panic,
Expand All @@ -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<()>>,
Expand All @@ -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();

Expand Down Expand Up @@ -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))
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/integration/common/output.rs
Original file line number Diff line number Diff line change
@@ -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>>());
}
}
}
35 changes: 35 additions & 0 deletions src/integration/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
14 changes: 6 additions & 8 deletions src/ui/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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]);
}
Expand Down Expand Up @@ -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));
}
Expand Down
88 changes: 69 additions & 19 deletions src/ui/util.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -96,33 +101,44 @@ 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)
}
}

#[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},
Expand Down Expand Up @@ -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_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!(matches!(text.as_deref(), Some("Hello\nWorld")));
assert_eq!(width, 3);
assert_eq!(text, Some(expected));
assert_eq!(height, 3);
}
}

0 comments on commit 75c2706

Please sign in to comment.