mirror of
https://github.com/apognu/tuigreet.git
synced 2024-10-26 08:34:57 +03:00
Merge 083b9ba6f1
into 51e7a60b7a
This commit is contained in:
commit
0f302193b5
590
Cargo.lock
generated
590
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ default = []
|
||||
nsswrapper = []
|
||||
|
||||
[dependencies]
|
||||
ansi-to-tui = "5.0.0-rc.1"
|
||||
chrono = { version = "^0.4", features = ["unstable-locales"] }
|
||||
crossterm = { version = "^0.27", features = ["event-stream"] }
|
||||
futures = "0.3"
|
||||
@ -22,13 +23,13 @@ i18n-embed = { version = "^0.14", features = [
|
||||
i18n-embed-fl = "^0.8"
|
||||
lazy_static = "^1.4"
|
||||
nix = { version = "^0.28", features = ["feature"] }
|
||||
tui = { package = "ratatui", version = "^0.26", default-features = false, features = [
|
||||
tui = { package = "ratatui", version = "^0.27", 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",
|
||||
@ -44,6 +45,7 @@ rand = "0.8.5"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.18"
|
||||
tracing = "0.1.40"
|
||||
utmp-rs = "0.3.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
53
src/info.rs
53
src/info.rs
@ -7,9 +7,11 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use ini::Ini;
|
||||
use lazy_static::lazy_static;
|
||||
use nix::sys::utsname;
|
||||
use utmp_rs::{UtmpEntry, UtmpParser};
|
||||
use uzers::os::unix::UserExt;
|
||||
|
||||
use crate::{
|
||||
@ -49,25 +51,50 @@ pub fn get_hostname() -> String {
|
||||
}
|
||||
|
||||
pub fn get_issue() -> Option<String> {
|
||||
let vtnr: usize = env::var("XDG_VTNR").unwrap_or_else(|_| "0".to_string()).parse().expect("unable to parse VTNR");
|
||||
let (date, time) = {
|
||||
let now = Local::now();
|
||||
|
||||
(now.format("%a %b %_d %Y").to_string(), now.format("%H:%M:%S").to_string())
|
||||
};
|
||||
|
||||
let user_count = match UtmpParser::from_path("/var/run/utmp")
|
||||
.map(|utmp| {
|
||||
utmp.into_iter().fold(0, |acc, entry| match entry {
|
||||
Ok(UtmpEntry::UserProcess { .. }) => acc + 1,
|
||||
Ok(UtmpEntry::LoginProcess { .. }) => acc + 1,
|
||||
_ => acc,
|
||||
})
|
||||
})
|
||||
.unwrap_or(0)
|
||||
{
|
||||
n if n < 2 => format!("{n} user"),
|
||||
n => format!("{n} users"),
|
||||
};
|
||||
|
||||
let vtnr: usize = env::var("XDG_VTNR").unwrap_or_else(|_| "0".to_string()).parse().unwrap_or(0);
|
||||
let uts = utsname::uname();
|
||||
|
||||
if let Ok(issue) = fs::read_to_string("/etc/issue") {
|
||||
let issue = issue.replace("\\S", "Linux").replace("\\l", &format!("tty{vtnr}"));
|
||||
let issue = issue
|
||||
.replace("\\S", "Linux")
|
||||
.replace("\\l", &format!("tty{vtnr}"))
|
||||
.replace("\\d", &date)
|
||||
.replace("\\t", &time)
|
||||
.replace("\\U", &user_count);
|
||||
|
||||
return match uts {
|
||||
Ok(uts) => Some(
|
||||
issue
|
||||
.replace("\\s", uts.sysname().to_str().unwrap_or(""))
|
||||
.replace("\\r", uts.release().to_str().unwrap_or(""))
|
||||
.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("\\\\", "\\"),
|
||||
),
|
||||
let issue = match uts {
|
||||
Ok(uts) => issue
|
||||
.replace("\\s", uts.sysname().to_str().unwrap_or(""))
|
||||
.replace("\\r", uts.release().to_str().unwrap_or(""))
|
||||
.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("\\o", uts.domainname().to_str().unwrap_or("")),
|
||||
|
||||
_ => Some(issue),
|
||||
_ => issue,
|
||||
};
|
||||
|
||||
return Some(issue.replace("\\x1b", "\x1b").replace("\\033", "\x1b").replace("\\e", "\x1b").replace(r"\\", r"\"));
|
||||
}
|
||||
|
||||
None
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
26
src/integration/common/output.rs
Normal file
26
src/integration/common/output.rs
Normal 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>>());
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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: false });
|
||||
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: false });
|
||||
|
||||
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: false });
|
||||
|
||||
assert_eq!(text, Some(expected));
|
||||
assert_eq!(height, 3);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user