Compare commits

...

15 Commits

Author SHA1 Message Date
Antoine POPINEAU
e8c6b57dda
Merge 4b1e90fff1 into 86bf7bbc15 2024-06-03 22:45:55 +03:00
Antoine POPINEAU
86bf7bbc15 Bump tuigreet version. 2024-05-30 23:51:39 +02:00
Antoine POPINEAU
976534d90d Fixed inconsistent display of session name on remember global session (#139). 2024-05-30 23:51:39 +02:00
Antoine POPINEAU
0baa018679 Select first session found by default (#141). 2024-05-30 23:51:39 +02:00
Antoine POPINEAU
e5c8c996d5 Never unpack greetd error and display them on screen or in logs. 2024-05-30 23:51:39 +02:00
Antoine POPINEAU
8a3643f324 Short-circuit authentication if no command was configured (#140). 2024-05-30 23:51:39 +02:00
dthelegend
4ac12261cc Fix greet-align formatting for manpage 2024-05-13 18:09:09 +02:00
dthelegend
598a997d45 Add greet-align to the manpage 2024-05-13 18:09:09 +02:00
dthelegend
18bc57c379 Fix bug where greet align could not match;
Greet align matches to exact case now, not coaxing the string into upper/lower case
2024-05-13 18:09:09 +02:00
dthelegend
ca5762d56a Rename text-align option to greet-align and limit to greeting only 2024-05-13 18:09:09 +02:00
dthelegend
6239bf0f31 Add Text Align option 2024-05-13 18:09:09 +02:00
Antoine POPINEAU
f67e43ffa0
Added instructions to run the test suite (#138). 2024-05-08 22:27:40 +02:00
Antoine POPINEAU
96f7d28377
Prepare 0.9.0. 2024-05-06 09:15:14 +02:00
Antoine POPINEAU
dd56f3fb7a
Removed a few useless clone(). 2024-05-02 14:22:37 +02:00
Antoine POPINEAU
59c4fa4cfe Clear screen and reset cursor on power command. 2024-05-01 12:47:24 +02:00
18 changed files with 211 additions and 96 deletions

2
Cargo.lock generated
View File

@ -1690,7 +1690,7 @@ checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
[[package]]
name = "tuigreet"
version = "0.9.0"
version = "0.9.1"
dependencies = [
"chrono",
"crossterm",

View File

@ -1,7 +1,7 @@
[package]
name = "tuigreet"
version = "0.9.0"
authors = ["Antoine POPINEAU <antoine.popineau@appscho.com>"]
version = "0.9.1"
authors = ["Antoine POPINEAU <antoine@popineau.eu>"]
edition = "2018"
build = "build.rs"

View File

@ -138,6 +138,21 @@ Please refer to the snippet below for the minimal `tuigreet` configuration:
Pre-built binaries of `tuigreet` for several architectures can be found in the [releases](https://github.com/apognu/tuigreet/releases) section of this repository. The [tip prerelease](https://github.com/apognu/tuigreet/releases/tag/tip) is continuously built and kept in sync with the `master` branch.
## Running the tests
Tests from the default features should run without any special consideration by running `cargo test`.
If you intend to run the whole test suite, you will need to perform some setup. One of our features uses NSS to list and filter existing users on the system, and in order not to rely on actual users being created on the host, we use [libnss_wrapper](https://cwrap.org/nss_wrapper.html) to mock responses from NSS. Without this, the tests would use the real user list from your system and probably fail because it cannot find the one it looks for.
After installing `libnss_wrapper` on your system (or compiling it to get the `.so`), you can run those specific tests as such:
```
$ export NSS_WRAPPER_PASSWD=contrib/fixtures/passwd
$ export NSS_WRAPPER_GROUP=contrib/fixtures/group
$ LD_PRELOAD=/path/to/libnss_wrapper.so cargo test --features nsswrapper nsswrapper_ # To run those tests specifically
$ LD_PRELOAD=/path/to/libnss_wrapper.so cargo test --all-features # To run the whole test suite
```
## Configuration
Edit `/etc/greetd/config.toml` and set the `command` setting to use `tuigreet`:

View File

@ -21,6 +21,7 @@ new_command = New command:
shutdown = Shut down
reboot = Reboot
command_missing = No command configured
command_exited = Command exited with
command_failed = Command failed

View File

@ -21,6 +21,7 @@ command = Nouvelle commande :
shutdown = Éteindre
reboot = Redémarrer
command_missing = Aucune commande configurée
command_exited = La commande a retourné
command_failed = Échec de la commande

View File

@ -105,6 +105,10 @@ tuigreet - A graphical console greeter for greetd
*--prompt-padding ROWS*
Add spacing between form fields.
*--greet-align [left|center|right]*
Alignment of the greeting text in the main prompt container
(default: 'center').
*--power-shutdown CMD [ARGS]...*
Customize the command run when instructed to shut down the machine. This must
be a non-interactive command (sudo cannot prompt for a password, for example).

View File

@ -23,9 +23,7 @@ use zeroize::Zeroize;
use crate::{
event::Event,
info::{
get_issue, get_last_session, get_last_session_path, get_last_user_name, get_last_user_session, get_last_user_session_path, get_last_user_username, get_min_max_uids, get_sessions, get_users,
},
info::{get_issue, get_last_command, get_last_session_path, get_last_user_command, get_last_user_name, get_last_user_session, get_last_user_username, get_min_max_uids, get_sessions, get_users},
power::PowerOption,
ui::{
common::{masked::MaskedString, menu::Menu, style::Theme},
@ -91,6 +89,15 @@ impl SecretDisplay {
}
}
// This enum models text alignment options
#[derive(SmartDefault, Debug, Clone)]
pub enum GreetAlign {
#[default]
Center,
Left,
Right,
}
#[derive(SmartDefault)]
pub struct Greeter {
pub debug: bool,
@ -220,28 +227,39 @@ impl Greeter {
greeter.connect().await;
}
let sessions = get_sessions(&greeter).unwrap_or_default();
if let SessionSource::None = greeter.session_source {
if !sessions.is_empty() {
greeter.session_source = SessionSource::Session(0);
}
}
greeter.sessions = Menu {
title: fl!("title_session"),
options: get_sessions(&greeter).unwrap_or_default(),
options: sessions,
selected: 0,
};
// If we should remember the last logged-in user.
if greeter.remember {
if let Some(username) = get_last_user_username() {
greeter.username = MaskedString::from(username.clone(), get_last_user_name());
greeter.username = MaskedString::from(username, get_last_user_name());
// If, on top of that, we should remember their last session.
if greeter.remember_user_session {
if let Ok(ref session_path) = get_last_user_session_path(&username) {
// Set the selected menu option and the session source.
greeter.sessions.selected = greeter.sessions.options.iter().position(|Session { path, .. }| path.as_deref() == Some(session_path)).unwrap_or(0);
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
// See if we have the last free-form command from the user.
if let Ok(command) = get_last_user_command(greeter.username.get()) {
greeter.session_source = SessionSource::Command(command);
}
// See if we have the last free-form command from the user.
if let Ok(command) = get_last_user_session(&username) {
greeter.session_source = SessionSource::Command(command);
// If a session was saved, use it and its name.
if let Ok(ref session_path) = get_last_user_session(greeter.username.get()) {
// Set the selected menu option and the session source.
if let Some(index) = greeter.sessions.options.iter().position(|Session { path, .. }| path.as_deref() == Some(session_path)) {
greeter.sessions.selected = index;
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
}
}
}
}
@ -249,13 +267,15 @@ impl Greeter {
// Same thing, but not user specific.
if greeter.remember_session {
if let Ok(ref session_path) = get_last_session_path() {
greeter.sessions.selected = greeter.sessions.options.iter().position(|Session { path, .. }| path.as_deref() == Some(session_path)).unwrap_or(0);
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
if let Ok(command) = get_last_command() {
greeter.session_source = SessionSource::Command(command.trim().to_string());
}
if let Ok(command) = get_last_session() {
greeter.session_source = SessionSource::Command(command.trim().to_string());
if let Ok(ref session_path) = get_last_session_path() {
if let Some(index) = greeter.sessions.options.iter().position(|Session { path, .. }| path.as_deref() == Some(session_path)) {
greeter.sessions.selected = index;
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
}
}
}
@ -295,7 +315,7 @@ impl Greeter {
self.connect().await;
}
// Connect to `greetd` and return a strea we can safely write to.
// Connect to `greetd` and return a stream we can safely write to.
pub async fn connect(&mut self) {
match UnixStream::connect(&self.socket).await {
Ok(stream) => self.stream = Some(Arc::new(RwLock::new(stream))),
@ -365,6 +385,18 @@ impl Greeter {
1
}
pub fn greet_align(&self) -> GreetAlign {
if let Some(value) = self.option("greet-align") {
match value.as_str() {
"left" => GreetAlign::Left,
"right" => GreetAlign::Right,
_ => GreetAlign::Center,
}
} else {
GreetAlign::default()
}
}
// Sets the locale that will be used for this invocation from environment.
fn set_locale(&mut self) {
let locale = DesktopLanguageRequester::requested_languages()
@ -409,6 +441,12 @@ impl Greeter {
opts.optopt("", "window-padding", "padding inside the terminal area (default: 0)", "PADDING");
opts.optopt("", "container-padding", "padding inside the main prompt container (default: 1)", "PADDING");
opts.optopt("", "prompt-padding", "padding between prompt rows (default: 1)", "PADDING");
opts.optopt(
"",
"greet-align",
"alignment of the greeting text in the main prompt container (default: 'center')",
"[left|center|right]",
);
opts.optopt("", "power-shutdown", "command to run to shut down the system", "'CMD [ARGS]...'");
opts.optopt("", "power-reboot", "command to run to reboot the system", "'CMD [ARGS]...'");

View File

@ -23,8 +23,8 @@ use crate::{
const LAST_USER_USERNAME: &str = "/var/cache/tuigreet/lastuser";
const LAST_USER_NAME: &str = "/var/cache/tuigreet/lastuser-name";
const LAST_SESSION: &str = "/var/cache/tuigreet/lastsession";
const LAST_SESSION_PATH: &str = "/var/cache/tuigreet/lastsession-path";
const LAST_COMMAND: &str = "/var/cache/tuigreet/lastsession";
const LAST_SESSION: &str = "/var/cache/tuigreet/lastsession-path";
const DEFAULT_MIN_UID: u16 = 1000;
const DEFAULT_MAX_UID: u16 = 60000;
@ -104,7 +104,7 @@ pub fn get_last_user_name() -> Option<String> {
}
pub fn write_last_username(username: &MaskedString) {
let _ = fs::write(LAST_USER_USERNAME, username.value.clone());
let _ = fs::write(LAST_USER_USERNAME, &username.value);
if let Some(ref name) = username.mask {
let _ = fs::write(LAST_USER_NAME, name);
@ -114,55 +114,59 @@ pub fn write_last_username(username: &MaskedString) {
}
pub fn get_last_session_path() -> Result<PathBuf, io::Error> {
Ok(PathBuf::from(fs::read_to_string(LAST_SESSION_PATH)?.trim()))
Ok(PathBuf::from(fs::read_to_string(LAST_SESSION)?.trim()))
}
pub fn get_last_session() -> Result<String, io::Error> {
Ok(fs::read_to_string(LAST_SESSION)?.trim().to_string())
pub fn get_last_command() -> Result<String, io::Error> {
Ok(fs::read_to_string(LAST_COMMAND)?.trim().to_string())
}
pub fn write_last_session_path<P>(session: &P)
where
P: AsRef<Path>,
{
let _ = fs::write(LAST_SESSION_PATH, session.as_ref().to_string_lossy().as_bytes());
let _ = fs::write(LAST_SESSION, session.as_ref().to_string_lossy().as_bytes());
}
pub fn write_last_session(session: &str) {
let _ = fs::write(LAST_SESSION, session);
pub fn write_last_command(session: &str) {
let _ = fs::write(LAST_COMMAND, session);
}
pub fn get_last_user_session_path(username: &str) -> Result<PathBuf, io::Error> {
Ok(PathBuf::from(fs::read_to_string(format!("{LAST_SESSION_PATH}-{username}"))?.trim()))
pub fn get_last_user_session(username: &str) -> Result<PathBuf, io::Error> {
Ok(PathBuf::from(fs::read_to_string(format!("{LAST_SESSION}-{username}"))?.trim()))
}
pub fn get_last_user_session(username: &str) -> Result<String, io::Error> {
Ok(fs::read_to_string(format!("{LAST_SESSION}-{username}"))?.trim().to_string())
pub fn get_last_user_command(username: &str) -> Result<String, io::Error> {
Ok(fs::read_to_string(format!("{LAST_COMMAND}-{username}"))?.trim().to_string())
}
pub fn write_last_user_session_path<P>(username: &str, session: P)
pub fn write_last_user_session<P>(username: &str, session: P)
where
P: AsRef<Path>,
{
let _ = fs::write(format!("{LAST_SESSION_PATH}-{username}"), session.as_ref().to_string_lossy().as_bytes());
let _ = fs::write(format!("{LAST_SESSION}-{username}"), session.as_ref().to_string_lossy().as_bytes());
}
pub fn delete_last_session_path() {
let _ = fs::remove_file(LAST_SESSION_PATH);
pub fn delete_last_session() {
let _ = fs::remove_file(LAST_SESSION);
}
pub fn write_last_user_session(username: &str, session: &str) {
let _ = fs::write(format!("{LAST_SESSION}-{username}"), session);
}
pub fn delete_last_user_session_path(username: &str) {
let _ = fs::remove_file(format!("{LAST_SESSION_PATH}-{username}"));
pub fn write_last_user_command(username: &str, session: &str) {
let _ = fs::write(format!("{LAST_COMMAND}-{username}"), session);
}
pub fn delete_last_user_session(username: &str) {
let _ = fs::remove_file(format!("{LAST_SESSION}-{username}"));
}
pub fn delete_last_command() {
let _ = fs::remove_file(LAST_COMMAND);
}
pub fn delete_last_user_command(username: &str) {
let _ = fs::remove_file(format!("{LAST_COMMAND}-{username}"));
}
pub fn get_users(min_uid: u16, max_uid: u16) -> Vec<User> {
let users = unsafe { uzers::all_users() };

View File

@ -8,7 +8,7 @@ use tokio::sync::{
use crate::{
event::Event,
info::{delete_last_user_session, delete_last_user_session_path, write_last_user_session, write_last_user_session_path, write_last_username},
info::{delete_last_user_command, delete_last_user_session, write_last_user_command, write_last_user_session, write_last_username},
macros::SafeDebug,
ui::sessions::{Session, SessionSource, SessionType},
AuthStatus, Greeter, Mode,
@ -69,7 +69,11 @@ impl Ipc {
}
async fn parse_response(&mut self, greeter: &mut Greeter, response: Response) -> Result<(), Box<dyn Error>> {
tracing::info!("received greetd message: {:?}", response);
// Do not display actual message from greetd, which may contain entered information, sometimes passwords.
match response {
Response::Error { ref error_type, .. } => tracing::info!("received greetd error message: {error_type:?}"),
ref response => tracing::info!("received greetd message: {:?}", response),
}
match response {
Response::AuthMessage { auth_message_type, auth_message } => match auth_message_type {
@ -124,16 +128,16 @@ impl Ipc {
SessionSource::Command(ref command) => {
tracing::info!("caching last user command: {command}");
write_last_user_session(&greeter.username.value, command);
delete_last_user_session_path(&greeter.username.value);
write_last_user_command(&greeter.username.value, command);
delete_last_user_session(&greeter.username.value);
}
SessionSource::Session(index) => {
if let Some(Session { path: Some(session_path), .. }) = greeter.sessions.options.get(index) {
tracing::info!("caching last user session: {session_path:?}");
write_last_user_session_path(&greeter.username.value, session_path);
delete_last_user_session(&greeter.username.value);
write_last_user_session(&greeter.username.value, session_path);
delete_last_user_command(&greeter.username.value);
}
}
@ -148,36 +152,51 @@ impl Ipc {
} else {
tracing::info!("authentication successful, starting session");
let command = greeter.session_source.command(greeter).map(str::to_string);
match greeter.session_source.command(greeter).map(str::to_string) {
None => {
Ipc::cancel(greeter).await;
if let Some(command) = command {
greeter.done = true;
greeter.mode = Mode::Processing;
greeter.message = Some(fl!("command_missing"));
greeter.reset(false).await;
}
let session = Session::get_selected(greeter);
let (command, env) = wrap_session_command(greeter, session, &command);
Some(command) if command.is_empty() => {
Ipc::cancel(greeter).await;
#[cfg(not(debug_assertions))]
self.send(Request::StartSession { cmd: vec![command.to_string()], env }).await;
greeter.message = Some(fl!("command_missing"));
greeter.reset(false).await;
}
#[cfg(debug_assertions)]
{
let _ = command;
let _ = env;
Some(command) => {
greeter.done = true;
greeter.mode = Mode::Processing;
self
.send(Request::StartSession {
cmd: vec!["true".to_string()],
env: vec![],
})
.await;
let session = Session::get_selected(greeter);
let (command, env) = wrap_session_command(greeter, session, &command);
#[cfg(not(debug_assertions))]
self.send(Request::StartSession { cmd: vec![command.to_string()], env }).await;
#[cfg(debug_assertions)]
{
let _ = command;
let _ = env;
self
.send(Request::StartSession {
cmd: vec!["true".to_string()],
env: vec![],
})
.await;
}
}
}
}
}
Response::Error { error_type, description } => {
tracing::info!("received an error from greetd: {error_type:?} - {description}");
Response::Error { error_type, .. } => {
// Do not display actual message from greetd, which may contain entered information, sometimes passwords.
tracing::info!("received an error from greetd: {error_type:?}");
Ipc::cancel(greeter).await;
@ -193,7 +212,8 @@ impl Ipc {
}
ErrorType::Error => {
greeter.message = Some(description);
// Do not display actual message from greetd, which may contain entered information, sometimes passwords.
greeter.message = Some("An error was received from greetd".to_string());
greeter.reset(false).await;
}
}

View File

@ -5,7 +5,7 @@ use greetd_ipc::Request;
use tokio::sync::RwLock;
use crate::{
info::{delete_last_session_path, get_last_user_session, get_last_user_session_path, write_last_session, write_last_session_path},
info::{delete_last_command, delete_last_session, get_last_user_command, get_last_user_session, write_last_command, write_last_session_path},
ipc::Ipc,
power::power,
ui::{
@ -225,8 +225,8 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
greeter.session_source = SessionSource::Command(greeter.buffer.clone());
if greeter.remember_session {
write_last_session(&greeter.buffer);
delete_last_session_path();
write_last_command(&greeter.buffer);
delete_last_session();
}
greeter.buffer = greeter.previous_buffer.take().unwrap_or_default();
@ -248,13 +248,12 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
Mode::Sessions => {
let session = greeter.sessions.options.get(greeter.sessions.selected).cloned();
if let Some(Session { path, command, .. }) = session {
if let Some(Session { path, .. }) = session {
if greeter.remember_session {
if let Some(ref path) = path {
write_last_session_path(path);
delete_last_command();
}
write_last_session(&command);
}
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
@ -365,7 +364,7 @@ async fn validate_username(greeter: &mut Greeter, ipc: &Ipc) {
greeter.buffer = String::new();
if greeter.remember_user_session {
if let Ok(last_session) = get_last_user_session_path(&greeter.username.value) {
if let Ok(last_session) = get_last_user_session(&greeter.username.value) {
if let Some(last_session) = Session::from_path(greeter, last_session).cloned() {
tracing::info!("remembered user session is {}", last_session.name);
@ -374,7 +373,7 @@ async fn validate_username(greeter: &mut Greeter, ipc: &Ipc) {
}
}
if let Ok(command) = get_last_user_session(&greeter.username.value) {
if let Ok(command) = get_last_user_command(&greeter.username.value) {
tracing::info!("remembered user command is {}", command);
greeter.session_source = SessionSource::Command(command);

View File

@ -23,6 +23,7 @@ use crossterm::{
};
use event::Event;
use greetd_ipc::Request;
use power::PowerPostAction;
use tokio::sync::RwLock;
use tracing_appender::non_blocking::WorkerGuard;
use tui::{backend::CrosstermBackend, Terminal};
@ -112,12 +113,21 @@ where
}
Some(Event::PowerCommand(command)) => {
power::run(&greeter, command).await;
if let PowerPostAction::ClearScreen = power::run(&greeter, command).await {
execute!(io::stdout(), LeaveAlternateScreen)?;
terminal.set_cursor(1, 1)?;
terminal.clear()?;
disable_raw_mode()?;
break;
}
}
_ => {}
}
}
Ok(())
}
async fn exit(greeter: &mut Greeter, status: AuthStatus) {

View File

@ -60,7 +60,12 @@ pub async fn power(greeter: &mut Greeter, option: PowerOption) {
}
}
pub async fn run(greeter: &Arc<RwLock<Greeter>>, mut command: Command) {
pub enum PowerPostAction {
Noop,
ClearScreen,
}
pub async fn run(greeter: &Arc<RwLock<Greeter>>, mut command: Command) -> PowerPostAction {
tracing::info!("executing power command: {:?}", command);
greeter.write().await.mode = Mode::Processing;
@ -85,6 +90,12 @@ pub async fn run(greeter: &Arc<RwLock<Greeter>>, mut command: Command) {
let mut greeter = greeter.write().await;
greeter.mode = mode;
greeter.message = message;
if message.is_none() {
PowerPostAction::ClearScreen
} else {
greeter.mode = mode;
greeter.message = message;
PowerPostAction::Noop
}
}

View File

@ -44,7 +44,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
let command_label_text = prompt_value(theme, Some(fl!("new_command")));
let command_label = Paragraph::new(command_label_text).style(theme.of(&[Themed::Prompt]));
let command_value_text = Span::from(greeter.buffer.clone());
let command_value_text = Span::from(&greeter.buffer);
let command_value = Paragraph::new(command_value_text).style(theme.of(&[Themed::Input]));
f.render_widget(command_label, chunks[0]);

View File

@ -1,4 +1,4 @@
use std::error::Error;
use std::{borrow::Cow, error::Error};
use tui::{
prelude::Rect,
@ -18,7 +18,7 @@ use crate::{
use super::style::Themed;
pub trait MenuItem {
fn format(&self) -> String;
fn format(&self) -> Cow<'_, str>;
}
#[derive(Default)]

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::{power::PowerOption, ui::common::menu::MenuItem};
#[derive(SmartDefault, Clone)]
@ -8,7 +10,7 @@ pub struct Power {
}
impl MenuItem for Power {
fn format(&self) -> String {
self.label.clone()
fn format(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.label)
}
}

View File

@ -10,7 +10,7 @@ use tui::{
use crate::{
info::get_hostname,
ui::{prompt_value, util::*, Frame},
Greeter, Mode, SecretDisplay,
Greeter, Mode, SecretDisplay, GreetAlign
};
use super::common::style::Themed;
@ -27,6 +27,11 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
let container_padding = greeter.container_padding();
let prompt_padding = greeter.prompt_padding();
let greeting_alignment = match greeter.greet_align() {
GreetAlign::Center => Alignment::Center,
GreetAlign::Left => Alignment::Left,
GreetAlign::Right => Alignment::Right
};
let container = Rect::new(x, y, width, height);
let frame = Rect::new(x + container_padding, y + container_padding, width - (2 * container_padding), height - (2 * container_padding));
@ -59,7 +64,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
if let Some(greeting) = &greeting {
let greeting_text = greeting.trim_end();
let greeting_label = Paragraph::new(greeting_text).alignment(Alignment::Center).style(theme.of(&[Themed::Greet]));
let greeting_label = Paragraph::new(greeting_text).alignment(greeting_alignment).style(theme.of(&[Themed::Greet]));
f.render_widget(greeting_label, chunks[GREETING_INDEX]);
}

View File

@ -1,4 +1,7 @@
use std::path::{Path, PathBuf};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use crate::Greeter;
@ -86,8 +89,8 @@ pub struct Session {
}
impl MenuItem for Session {
fn format(&self) -> String {
self.name.clone()
fn format(&self) -> Cow<'_, str> {
Cow::Borrowed(&self.name)
}
}

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use super::common::menu::MenuItem;
#[derive(Default, Clone)]
@ -7,10 +9,10 @@ pub struct User {
}
impl MenuItem for User {
fn format(&self) -> String {
fn format(&self) -> Cow<'_, str> {
match &self.name {
Some(name) => format!("{name} ({})", self.username),
None => self.username.clone(),
Some(name) => Cow::Owned(format!("{name} ({})", self.username)),
None => Cow::Borrowed(&self.username),
}
}
}