Refactored session management, added comments and tests.

This commit is contained in:
Antoine POPINEAU 2023-10-29 20:00:00 +01:00 committed by Antoine POPINEAU
parent 158a98e85d
commit a3b95d1bad
9 changed files with 456 additions and 107 deletions

View File

@ -29,7 +29,7 @@ use crate::{
ui::{
common::menu::Menu,
power::Power,
sessions::{Session, SessionType},
sessions::{Session, SessionSource, SessionType},
users::User,
},
};
@ -55,6 +55,8 @@ impl Display for AuthStatus {
impl Error for AuthStatus {}
// A mode represents the large section of the software, usually screens to be
// displayed, or the state of the application.
#[derive(SmartDefault, Debug, Copy, Clone, PartialEq)]
pub enum Mode {
#[default]
@ -75,43 +77,71 @@ pub struct Greeter {
pub socket: String,
pub stream: Option<Arc<RwLock<UnixStream>>>,
// Current mode of the application, will define what actions are permitted.
pub mode: Mode,
// Mode the application will return to when exiting the current mode.
pub previous_mode: Mode,
// Offset the cursor should be at from its base position for the current mode.
pub cursor_offset: i16,
pub users: Menu<User>,
pub command: Option<String>,
pub new_command: String,
pub session_path: Option<PathBuf>,
// Buffer to be used as a temporary editing zone for the various modes.
pub buffer: String,
// Define the selected session and how to resolve it.
pub session_source: SessionSource,
// List of session files found on disk.
pub session_paths: Vec<(PathBuf, SessionType)>,
// Menu for session selection.
pub sessions: Menu<Session>,
// Wrapper command to prepend to non-X11 sessions.
pub session_wrapper: Option<String>,
// Wrapper command to prepend to X11 sessions.
pub xsession_wrapper: Option<String>,
pub username: String,
pub username_mask: Option<String>,
pub prompt: Option<String>,
pub answer: String,
pub secret: bool,
// Whether user menu is enabled.
pub user_menu: bool,
pub remember: bool,
pub remember_session: bool,
pub remember_user_session: bool,
// Menu for user selection.
pub users: Menu<User>,
// Current username.
pub username: String,
// Value to display in place of the username (e.g. full name of the user).
pub username_mask: Option<String>,
// Prompt that should be displayed to ask for entry.
pub prompt: Option<String>,
// Whether the current edition prompt should be hidden.
pub secret: bool,
// Whether to display replacement characters for secret entries.
pub asterisks: bool,
// Which character to use for secret entries.
#[default(DEFAULT_ASTERISKS_CHAR)]
pub asterisks_char: char,
// Whether last logged-in user should be remembered.
pub remember: bool,
// Whether last launched session (regardless of user) should be remembered.
pub remember_session: bool,
// Whether last launched session for the current user should be remembered.
pub remember_user_session: bool,
// Greeting message (MOTD) to use to welcome the user.
pub greeting: Option<String>,
// Transaction message to show to the user.
pub message: Option<String>,
// Menu for power options.
pub powers: Menu<Power>,
// Power command that was selected.
pub power_command: Option<Command>,
// Channel to notify of a power command selection.
pub power_command_notify: Arc<Notify>,
// Whether to prefix the power commands with `setsid`.
pub power_setsid: bool,
// The software is waiting for a response from `greetd`.
pub working: bool,
// We are done working.
pub done: bool,
// Should we exit?
pub exit: Option<AuthStatus>,
}
@ -134,54 +164,55 @@ impl Greeter {
};
greeter.parse_options().await;
greeter.sessions = Menu {
title: fl!("title_session"),
options: get_sessions(&greeter).unwrap_or_default(),
selected: 0,
};
if let Some(Session { command, .. }) = greeter.sessions.options.get(0) {
if greeter.command.is_none() {
greeter.command = Some(command.clone());
}
}
// If we should remember the last logged-in user.
if greeter.remember {
if let Some(username) = get_last_user_username() {
greeter.username = username.clone();
greeter.username_mask = get_last_user_name();
// If, on top of that, we should remember their last session.
if greeter.remember_user_session {
if let Ok(session_path) = get_last_user_session_path(&username) {
greeter.session_path = Some(session_path);
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_session(&username) {
greeter.command = Some(command);
greeter.session_source = SessionSource::Command(command);
}
}
}
}
// Same thing, but not user specific.
if greeter.remember_session {
if let Ok(session_path) = get_last_session_path() {
greeter.session_path = Some(session_path.clone());
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 Some(session) = Session::from_path(&greeter, session_path) {
greeter.command = Some(session.command.clone());
}
} else if let Ok(command) = get_last_session() {
greeter.command = Some(command.trim().to_string());
if let Ok(command) = get_last_session() {
greeter.session_source = SessionSource::Command(command.trim().to_string());
}
}
greeter.sessions.selected = greeter.sessions.options.iter().position(|Session { path, .. }| path == &greeter.session_path).unwrap_or(0);
greeter
}
// Scrub memory of all data, unless `soft` is true, in which case, we will
// keep the username (can happen if a wrong password was entered, we want to
// give the user another chance, as PAM would).
fn scrub(&mut self, scrub_message: bool, soft: bool) {
self.answer.zeroize();
self.buffer.zeroize();
self.prompt.zeroize();
if !soft {
@ -194,6 +225,7 @@ impl Greeter {
}
}
// Reset the software to its initial state.
pub async fn reset(&mut self, soft: bool) {
if soft {
self.mode = Mode::Password;
@ -210,6 +242,7 @@ impl Greeter {
self.connect().await;
}
// Connect to `greetd` and return a strea 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))),
@ -233,6 +266,8 @@ impl Greeter {
self.config().opt_str(name)
}
// Returns the width of the main window where content is displayed from the
// provided arguments.
pub fn width(&self) -> u16 {
if let Some(value) = self.option("width") {
if let Ok(width) = value.parse::<u16>() {
@ -243,6 +278,7 @@ impl Greeter {
80
}
// Returns the padding of the screen from the provided arguments.
pub fn window_padding(&self) -> u16 {
if let Some(value) = self.option("window-padding") {
if let Ok(padding) = value.parse::<u16>() {
@ -253,6 +289,8 @@ impl Greeter {
0
}
// Returns the padding of the main window where content is displayed from the
// provided arguments.
pub fn container_padding(&self) -> u16 {
if let Some(value) = self.option("container-padding") {
if let Ok(padding) = value.parse::<u16>() {
@ -263,6 +301,7 @@ impl Greeter {
2
}
// Returns the spacing between each prompt from the provided arguments.
pub fn prompt_padding(&self) -> u16 {
if let Some(value) = self.option("prompt-padding") {
if let Ok(padding) = value.parse::<u16>() {
@ -273,6 +312,7 @@ impl Greeter {
1
}
// Sets the locale that will be used for this invocation from environment.
fn set_locale(&mut self) {
let locale = DesktopLanguageRequester::requested_languages()
.into_iter()
@ -285,7 +325,7 @@ impl Greeter {
}
}
async fn parse_options(&mut self) {
pub fn options() -> Options {
let mut opts = Options::new();
let xsession_wrapper_desc = format!("wrapper command to initialize X server and launch X11 sessions (default: {DEFAULT_XSESSION_WRAPPER})");
@ -319,6 +359,13 @@ impl Greeter {
opts.optopt("", "power-reboot", "command to run to reboot the system", "'CMD [ARGS]...'");
opts.optflag("", "power-no-setsid", "do not prefix power commands with setsid");
opts
}
// Parses command line arguments to configured the software accordingly.
async fn parse_options(&mut self) {
let opts = Greeter::options();
self.config = match opts.parse(env::args().collect::<Vec<String>>()) {
Ok(matches) => Some(matches),
@ -404,7 +451,11 @@ impl Greeter {
self.remember_user_session = self.config().opt_present("remember-user-session");
self.asterisks = self.config().opt_present("asterisks");
self.greeting = self.option("greeting");
self.command = self.option("cmd");
// If the `--cmd` argument is provided, it will override the selected session.
if let Some(command) = self.option("cmd") {
self.session_source = SessionSource::Command(command);
}
if let Some(dirs) = self.option("sessions") {
self.session_paths.extend(env::split_paths(&dirs).map(|dir| (dir, SessionType::Wayland)));
@ -451,6 +502,7 @@ impl Greeter {
self.prompt = None;
}
// Computes the size of the prompt to help determine where input should start.
pub fn prompt_width(&self) -> usize {
match &self.prompt {
None => 0,
@ -471,3 +523,37 @@ fn print_version() {
println!("This is free software, you are welcome to redistribute it under some conditions.");
println!("There is NO WARRANTY, to the extent provided by law.");
}
#[cfg(test)]
mod test {
use crate::Greeter;
#[test]
fn test_prompt_width() {
let mut greeter = Greeter::default();
greeter.prompt = None;
assert_eq!(greeter.prompt_width(), 0);
greeter.prompt = Some("Hello:".into());
assert_eq!(greeter.prompt_width(), 6);
}
#[test]
fn test_set_prompt() {
let mut greeter = Greeter::default();
greeter.set_prompt("Hello:");
assert_eq!(greeter.prompt, Some("Hello: ".into()));
greeter.set_prompt("Hello World: ");
assert_eq!(greeter.prompt, Some("Hello World: ".into()));
greeter.remove_prompt();
assert_eq!(greeter.prompt, None);
}
}

View File

@ -234,15 +234,7 @@ pub fn get_sessions(greeter: &Greeter) -> Result<Vec<Session>, Box<dyn Error>> {
&greeter.session_paths
};
let mut files = match &greeter.command {
Some(command) => vec![Session {
name: command.clone(),
command: command.clone(),
session_type: SessionType::default(),
path: None,
}],
_ => vec![],
};
let mut files = vec![];
for (path, session_type) in paths.iter() {
if let Ok(entries) = fs::read_dir(path) {

View File

@ -8,7 +8,7 @@ use tokio::sync::{
use crate::{
info::{delete_last_user_session_path, write_last_user_session, write_last_user_session_path, write_last_username},
ui::sessions::{Session, SessionType},
ui::sessions::{Session, SessionSource, SessionType},
AuthStatus, Greeter, Mode,
};
@ -107,40 +107,49 @@ impl Ipc {
write_last_username(&greeter.username, greeter.username_mask.as_deref());
if greeter.remember_user_session {
if let Some(command) = &greeter.command {
write_last_user_session(&greeter.username, command);
}
match greeter.session_source {
SessionSource::Command(ref command) => {
write_last_user_session(&greeter.username, command);
delete_last_user_session_path(&greeter.username);
}
if let Some(session_path) = &greeter.session_path {
write_last_user_session_path(&greeter.username, session_path);
} else {
delete_last_user_session_path(&greeter.username);
SessionSource::Session(index) => {
if let Some(Session { path: Some(session_path), .. }) = greeter.sessions.options.get(index) {
write_last_user_session_path(&greeter.username, session_path);
}
}
_ => {}
}
}
}
crate::exit(greeter, AuthStatus::Success).await;
} else if let Some(command) = &greeter.command {
greeter.done = true;
greeter.mode = Mode::Processing;
} else {
let command = greeter.session_source.command(greeter).map(str::to_string);
let session = Session::get_selected(greeter);
let (command, env) = wrap_session_command(greeter, session, command);
if let Some(command) = command {
greeter.done = true;
greeter.mode = Mode::Processing;
#[cfg(not(debug_assertions))]
self.send(Request::StartSession { cmd: vec![command.to_string()], env }).await;
let session = Session::get_selected(greeter);
let (command, env) = wrap_session_command(greeter, session, &command);
#[cfg(debug_assertions)]
{
let _ = command;
let _ = env;
#[cfg(not(debug_assertions))]
self.send(Request::StartSession { cmd: vec![command.to_string()], env }).await;
self
.send(Request::StartSession {
cmd: vec!["true".to_string()],
env: vec![],
})
.await;
#[cfg(debug_assertions)]
{
let _ = command;
let _ = env;
self
.send(Request::StartSession {
cmd: vec!["true".to_string()],
env: vec![],
})
.await;
}
}
}
}

View File

@ -8,25 +8,36 @@ use crate::{
info::{delete_last_session_path, get_last_user_session, get_last_user_session_path, write_last_session, write_last_session_path},
ipc::Ipc,
power::power,
ui::{sessions::Session, users::User},
ui::{
sessions::{Session, SessionSource},
users::User,
},
Greeter, Mode,
};
// Act on keyboard events.
//
// This function will be called whenever a keyboard event was captured by the
// application. It takes a reference to the `Greeter` so it can be aware of the
// current state of the application and act accordinly; It also receives the
// `Ipc` interface so it is able to interact with `greetd` if necessary.
pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) -> Result<(), Box<dyn Error>> {
let mut greeter = greeter.write().await;
match input {
// ^U should erase the current buffer.
KeyEvent {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::CONTROL,
..
} => match greeter.mode {
Mode::Username => greeter.username = String::new(),
Mode::Password => greeter.answer = String::new(),
Mode::Command => greeter.new_command = String::new(),
Mode::Password => greeter.buffer = String::new(),
Mode::Command => greeter.buffer = String::new(),
_ => {}
},
// In debug mode only, ^X will exit the application.
#[cfg(debug_assertions)]
KeyEvent {
code: KeyCode::Char('x'),
@ -38,6 +49,9 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
crate::exit(&mut greeter, AuthStatus::Cancel).await;
}
// Depending on the active screen, pressing Escape will either return to the
// previous mode (close a popup, for example), or cancel the `greetd`
// session.
KeyEvent { code: KeyCode::Esc, .. } => match greeter.mode {
Mode::Users | Mode::Command | Mode::Sessions | Mode::Power => {
greeter.mode = greeter.previous_mode;
@ -49,19 +63,27 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
}
},
// Simple cursor directions in text fields.
KeyEvent { code: KeyCode::Left, .. } => greeter.cursor_offset -= 1,
KeyEvent { code: KeyCode::Right, .. } => greeter.cursor_offset += 1,
// F2 will display the command entry prompt. If we are already in one of the
// popup screens, we set the previous screen as being the current previous
// screen.
KeyEvent { code: KeyCode::F(2), .. } => {
greeter.previous_mode = match greeter.mode {
Mode::Users | Mode::Command | Mode::Sessions | Mode::Power => greeter.previous_mode,
_ => greeter.mode,
};
greeter.new_command = greeter.command.clone().unwrap_or_default();
// Set the edition buffer to the current command.
greeter.buffer = greeter.session_source.command(&greeter).map(str::to_string).unwrap_or_default();
greeter.mode = Mode::Command;
}
// F3 will display the session selection menu. If we are already in one of
// the popup screens, we set the previous screen as being the current
// previous screen.
KeyEvent { code: KeyCode::F(3), .. } => {
greeter.previous_mode = match greeter.mode {
Mode::Users | Mode::Command | Mode::Sessions | Mode::Power => greeter.previous_mode,
@ -71,6 +93,9 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
greeter.mode = Mode::Sessions;
}
// F12 will display the user selection menu. If we are already in one of the
// popup screens, we set the previous screen as being the current previous
// screen.
KeyEvent { code: KeyCode::F(12), .. } => {
greeter.previous_mode = match greeter.mode {
Mode::Users | Mode::Command | Mode::Sessions | Mode::Power => greeter.previous_mode,
@ -80,6 +105,7 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
greeter.mode = Mode::Power;
}
// Handle moving up in menus.
KeyEvent { code: KeyCode::Up, .. } => {
if let Mode::Users = greeter.mode {
if greeter.users.selected > 0 {
@ -100,6 +126,7 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
}
}
// Handle moving down in menus.
KeyEvent { code: KeyCode::Down, .. } => {
if let Mode::Users = greeter.mode {
if greeter.users.selected < greeter.users.options.len() - 1 {
@ -120,6 +147,7 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
}
}
// ^A should go to the start of the current prompt
KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
@ -128,24 +156,27 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
let value = {
match greeter.mode {
Mode::Username => &greeter.username,
_ => &greeter.answer,
_ => &greeter.buffer,
}
};
greeter.cursor_offset = -(value.chars().count() as i16);
}
// ^A should go to the end of the current prompt
KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
..
} => greeter.cursor_offset = 0,
// Tab should validate the username entry (same as Enter).
KeyEvent { code: KeyCode::Tab, .. } => match greeter.mode {
Mode::Username if !greeter.username.is_empty() => validate_username(&mut greeter, &ipc).await,
_ => {}
},
// Enter validates the current entry, depending on the active mode.
KeyEvent { code: KeyCode::Enter, .. } => match greeter.mode {
Mode::Username if !greeter.username.is_empty() => validate_username(&mut greeter, &ipc).await,
@ -166,20 +197,19 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
ipc
.send(Request::PostAuthMessageResponse {
response: Some(greeter.answer.clone()),
response: Some(greeter.buffer.clone()),
})
.await;
greeter.answer = String::new();
greeter.buffer = String::new();
}
Mode::Command => {
greeter.sessions.selected = 0;
greeter.session_path = None;
greeter.command = Some(greeter.new_command.clone());
greeter.session_source = SessionSource::Command(greeter.buffer.clone());
if greeter.remember_session {
write_last_session(&greeter.new_command);
write_last_session(&greeter.buffer);
delete_last_session_path();
}
@ -209,8 +239,7 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
write_last_session(&command);
}
greeter.session_path = path.clone();
greeter.command = Some(command.clone());
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
}
greeter.mode = greeter.previous_mode;
@ -229,8 +258,10 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
Mode::Processing => {}
},
// Handle free-form entry of characters.
KeyEvent { code: KeyCode::Char(c), .. } => insert_key(&mut greeter, c).await,
// Handle deletion of characters.
KeyEvent { code: KeyCode::Backspace, .. } | KeyEvent { code: KeyCode::Delete, .. } => delete_key(&mut greeter, input.code).await,
_ => {}
@ -239,11 +270,13 @@ pub async fn handle(greeter: Arc<RwLock<Greeter>>, input: KeyEvent, ipc: Ipc) ->
Ok(())
}
// Handle insertion of characters into the proper buffer, depending on the
// current mode and the position of the cursor.
async fn insert_key(greeter: &mut Greeter, c: char) {
let value = match greeter.mode {
Mode::Username => &greeter.username,
Mode::Password => &greeter.answer,
Mode::Command => &greeter.new_command,
Mode::Password => &greeter.buffer,
Mode::Command => &greeter.buffer,
Mode::Users | Mode::Sessions | Mode::Power | Mode::Processing => return,
};
@ -256,17 +289,20 @@ async fn insert_key(greeter: &mut Greeter, c: char) {
match mode {
Mode::Username => greeter.username = value,
Mode::Password => greeter.answer = value,
Mode::Command => greeter.new_command = value,
Mode::Password => greeter.buffer = value,
Mode::Command => greeter.buffer = value,
_ => {}
};
}
// Handle deletion of characters from a prompt into the proper buffer, depending
// on the current mode, whether Backspace or Delete was pressed and the position
// of the cursor.
async fn delete_key(greeter: &mut Greeter, key: KeyCode) {
let value = match greeter.mode {
Mode::Username => &greeter.username,
Mode::Password => &greeter.answer,
Mode::Command => &greeter.new_command,
Mode::Password => &greeter.buffer,
Mode::Command => &greeter.buffer,
Mode::Users | Mode::Sessions | Mode::Power | Mode::Processing => return,
};
@ -284,8 +320,8 @@ async fn delete_key(greeter: &mut Greeter, key: KeyCode) {
match greeter.mode {
Mode::Username => greeter.username = value,
Mode::Password => greeter.answer = value,
Mode::Command => greeter.new_command = value,
Mode::Password => greeter.buffer = value,
Mode::Command => greeter.buffer = value,
Mode::Users | Mode::Sessions | Mode::Power | Mode::Processing => return,
};
@ -295,24 +331,24 @@ async fn delete_key(greeter: &mut Greeter, key: KeyCode) {
}
}
// Creates a `greetd` session for the provided username.
async fn validate_username(greeter: &mut Greeter, ipc: &Ipc) {
greeter.working = true;
greeter.message = None;
ipc.send(Request::CreateSession { username: greeter.username.clone() }).await;
greeter.answer = String::new();
greeter.buffer = String::new();
if greeter.remember_user_session {
if let Ok(last_session) = get_last_user_session_path(&greeter.username) {
if let Some(last_session) = Session::from_path(greeter, last_session).cloned() {
greeter.sessions.selected = greeter.sessions.options.iter().position(|sess| sess.path == last_session.path).unwrap_or(0);
greeter.session_path = last_session.path;
greeter.command = Some(last_session.command);
greeter.session_source = SessionSource::Session(greeter.sessions.selected);
}
}
if let Ok(command) = get_last_user_session(&greeter.username) {
greeter.command = Some(command);
greeter.session_source = SessionSource::Command(command);
}
}
}

View File

@ -34,7 +34,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
let command_label_text = prompt_value(Some(fl!("new_command")));
let command_label = Paragraph::new(command_label_text);
let command_value_text = Span::from(greeter.new_command.clone());
let command_value_text = Span::from(greeter.buffer.clone());
let command_value = Paragraph::new(command_value_text);
f.render_widget(command_label, chunks[0]);
@ -48,7 +48,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
),
);
let new_command = greeter.new_command.clone();
let new_command = greeter.buffer.clone();
let offset = get_cursor_offset(greeter, new_command.chars().count());
Ok((2 + cursor.x + fl!("new_command").chars().count() as u16 + offset as u16, cursor.y + 1))

View File

@ -84,7 +84,7 @@ pub async fn draw(greeter: Arc<RwLock<Greeter>>, terminal: &mut Term) -> Result<
)
.split(chunks[STATUSBAR_INDEX]);
let command = greeter.command.clone().unwrap_or_else(|| "-".to_string());
let command = greeter.session_source.label(&greeter).unwrap_or("-");
let status_left_text = Line::from(vec![
status_label("ESC"),
status_value(fl!("action_reset")),

View File

@ -92,9 +92,9 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
if !greeter.secret || greeter.asterisks {
let value = if greeter.secret && greeter.asterisks {
greeter.asterisks_char.to_string().repeat(greeter.answer.chars().count())
greeter.asterisks_char.to_string().repeat(greeter.buffer.chars().count())
} else {
greeter.answer.clone()
greeter.buffer.clone()
};
let answer_value_text = Span::from(value);
@ -132,7 +132,7 @@ pub fn draw(greeter: &mut Greeter, f: &mut Frame) -> Result<(u16, u16), Box<dyn
}
Mode::Password => {
let answer_length = greeter.answer.chars().count();
let answer_length = greeter.buffer.chars().count();
let offset = get_cursor_offset(greeter, answer_length);
if greeter.secret && !greeter.asterisks {

View File

@ -4,6 +4,50 @@ use crate::Greeter;
use super::common::menu::MenuItem;
// SessionSource models the selected session and where it comes from.
//
// A session can either come from a free-form command or an XDG-defined session
// file. Each variant contains a reference to the data required to create a
// session, either the String of the command or the index of the session in the
// session list.
#[derive(SmartDefault)]
pub enum SessionSource {
#[default]
None,
Command(String),
Session(usize),
}
impl SessionSource {
// Returns a human-readable label for the selected session.
//
// For free-form commands, this is the command itself. For session files, it
// is the value of the `Name` attribute in that file.
pub fn label<'g, 'ss: 'g>(&'ss self, greeter: &'g Greeter) -> Option<&'g str> {
match self {
SessionSource::None => None,
SessionSource::Command(command) => Some(command),
SessionSource::Session(index) => greeter.sessions.options.get(*index).map(|session| session.name.as_str()),
}
}
// Returns the command that should be spawned when the selected session is
// started.
pub fn command<'g, 'ss: 'g>(&'ss self, greeter: &'g Greeter) -> Option<&'g str> {
match self {
SessionSource::None => None,
SessionSource::Command(command) => Some(command.as_str()),
SessionSource::Session(index) => greeter
.sessions
.options
.get(*index)
.and_then(|session| session.path.as_ref())
.and_then(|path| path.as_os_str().to_str()),
}
}
}
// Represents the XDG type of the selected session.
#[derive(SmartDefault, Debug, Copy, Clone, PartialEq)]
pub enum SessionType {
X11,
@ -14,6 +58,8 @@ pub enum SessionType {
}
impl SessionType {
// Returns the value that should be set in `XDG_SESSION_TYPE` when the session
// is started.
pub fn as_xdg_session_type(&self) -> &'static str {
match self {
SessionType::X11 => "x11",
@ -24,11 +70,18 @@ impl SessionType {
}
}
// A session, as defined by an XDG session file.
#[derive(SmartDefault, Clone)]
pub struct Session {
// Human-friendly name for the session, maps to the `Name` attribute.
pub name: String,
// Command used to start the session, maps to the `Exec` attribute.
pub command: String,
// XDG session type for the session, detected from the location of the session
// file.
pub session_type: SessionType,
// Path to the session file. Used to uniquely identify sessions, since names
// and commands can be identital between two different sessions.
pub path: Option<PathBuf>,
}
@ -39,6 +92,10 @@ impl MenuItem for Session {
}
impl Session {
// Get a `Session` from the path of a session file.
//
// If the path maps to a valid session file, will return the associated
// session. Otherwise, will return `None`.
pub fn from_path<P>(greeter: &Greeter, path: P) -> Option<&Session>
where
P: AsRef<Path>,
@ -46,9 +103,15 @@ impl Session {
greeter.sessions.options.iter().find(|session| session.path.as_deref() == Some(path.as_ref()))
}
// Retrieves the `Session` that is currently selected.
//
// Note that this does not indicate which menu item is "highlighted", but the
// session that was selected.
pub fn get_selected(greeter: &Greeter) -> Option<&Session> {
greeter.session_path.as_ref()?;
greeter.sessions.options.get(greeter.sessions.selected)
match greeter.session_source {
SessionSource::Session(index) => greeter.sessions.options.get(index),
_ => None,
}
}
}
@ -57,7 +120,7 @@ mod test {
use crate::{
ui::{
common::menu::Menu,
sessions::{Session, SessionType},
sessions::{Session, SessionSource, SessionType},
},
Greeter,
};
@ -65,7 +128,7 @@ mod test {
#[test]
fn from_path_existing() {
let mut greeter = Greeter::default();
greeter.session_path = Some("/Session2Path".into());
greeter.session_source = SessionSource::Session(1);
greeter.sessions = Menu::<Session> {
title: "Sessions".into(),
@ -96,7 +159,7 @@ mod test {
#[test]
fn from_path_non_existing() {
let mut greeter = Greeter::default();
greeter.session_path = Some("/Session2Path".into());
greeter.session_source = SessionSource::Session(1);
greeter.sessions = Menu::<Session> {
title: "Sessions".into(),
@ -124,7 +187,7 @@ mod test {
#[test]
fn distinct_session() {
let mut greeter = Greeter::default();
greeter.session_path = Some("/Session2Path".into());
greeter.session_source = SessionSource::Session(1);
greeter.sessions = Menu::<Session> {
title: "Sessions".into(),
@ -155,7 +218,7 @@ mod test {
#[test]
fn same_name_session() {
let mut greeter = Greeter::default();
greeter.session_path = Some("/Session2Path".into());
greeter.session_source = SessionSource::Session(1);
greeter.sessions = Menu::<Session> {
title: "Sessions".into(),

View File

@ -6,6 +6,9 @@ pub fn titleize(message: &str) -> String {
format!(" {message} ")
}
// Determinew whether the cursor should be shown or hidden from the current
// mode and configuration. Usually, we will show the cursor only when expecting
// text entries from the user.
pub fn should_hide_cursor(greeter: &Greeter) -> bool {
greeter.working
|| greeter.done
@ -17,6 +20,17 @@ pub fn should_hide_cursor(greeter: &Greeter) -> bool {
|| greeter.mode == Mode::Processing
}
// Computes the height of the main window where we display content, depending on
// the mode and spacing configuration.
//
// +------------------------+
// | | <- container padding
// | Greeting | <- greeting height
// | | <- auto-padding if greeting
// | Username: | <- username
// | Password: | <- password if prompt == Some(_)
// | | <- container padding
// +------------------------+
pub fn get_height(greeter: &Greeter) -> u16 {
let (_, greeting_height) = get_greeting_height(greeter, 1, 0);
let container_padding = greeter.container_padding();
@ -37,6 +51,8 @@ pub fn get_height(greeter: &Greeter) -> u16 {
}
}
// Get the coordinates and size of the main window area, from the terminal size,
// and the content we need to display.
pub fn get_rect_bounds(greeter: &Greeter, area: Rect, items: usize) -> (u16, u16, u16, u16) {
let width = greeter.width();
let height: u16 = get_height(greeter) + items as u16;
@ -50,6 +66,8 @@ pub fn get_rect_bounds(greeter: &Greeter, area: Rect, items: usize) -> (u16, u16
(x, y, width, height)
}
// Computes the size of a text entry, from the container width and, if
// applicable, the prompt length.
pub fn get_input_width(greeter: &Greeter, width: u16, label: &Option<String>) -> u16 {
let width = std::cmp::min(greeter.width(), width);
@ -100,3 +118,148 @@ pub fn get_message_height(greeter: &Greeter, padding: u16, fallback: u16) -> (Op
(None, fallback)
}
}
#[cfg(test)]
mod test {
use tui::prelude::Rect;
use crate::{
ui::util::{get_greeting_height, get_height},
Greeter, Mode,
};
use super::{get_input_width, get_rect_bounds};
// +-----------+
// | Username: |
// +-----------+
#[test]
fn test_container_height_username_padding_zero() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--container-padding", "0"]).ok();
greeter.mode = Mode::Username;
assert_eq!(get_height(&greeter), 3);
}
// +-----------+
// | |
// | Username: |
// | |
// +-----------+
#[test]
fn test_container_height_username_padding_one() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok();
greeter.mode = Mode::Username;
assert_eq!(get_height(&greeter), 5);
}
// +-----------+
// | |
// | Greeting |
// | |
// | Username: |
// | |
// +-----------+
#[test]
fn test_container_height_username_greeting_padding_one() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok();
greeter.greeting = Some("Hello".into());
greeter.mode = Mode::Username;
assert_eq!(get_height(&greeter), 7);
}
// +-----------+
// | |
// | Greeting |
// | |
// | Username: |
// | |
// | Password: |
// | |
// +-----------+
#[test]
fn test_container_height_password_greeting_padding_one_prompt_padding_1() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--container-padding", "1"]).ok();
greeter.greeting = Some("Hello".into());
greeter.mode = Mode::Password;
greeter.prompt = Some("Password:".into());
assert_eq!(get_height(&greeter), 9);
}
// +-----------+
// | |
// | Greeting |
// | |
// | Username: |
// | Password: |
// | |
// +-----------+
#[test]
fn test_container_height_password_greeting_padding_one_prompt_padding_0() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--container-padding", "1", "--prompt-padding", "0"]).ok();
greeter.greeting = Some("Hello".into());
greeter.mode = Mode::Password;
greeter.prompt = Some("Password:".into());
assert_eq!(get_height(&greeter), 8);
}
#[test]
fn test_rect_bounds() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "50"]).ok();
let (x, y, width, height) = get_rect_bounds(&greeter, Rect::new(0, 0, 100, 100), 1);
assert_eq!(x, 25);
assert_eq!(y, 47);
assert_eq!(width, 50);
assert_eq!(height, 6);
}
// | Username: __________________________ |
// <--------------------------------------> width 40 (padding 1)
// <-------> prompt width 9
// <------------------------> input width 26
#[test]
fn input_width() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "40", "--container-padding", "1"]).ok();
let input_width = get_input_width(&greeter, 40, &Some("Username:".into()));
assert_eq!(input_width, 26);
}
#[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());
let (text, width) = get_greeting_height(&greeter, 1, 0);
assert!(matches!(text.as_deref(), Some("Hello")));
assert_eq!(width, 2);
}
#[test]
fn greeting_height_two_lines() {
let mut greeter = Greeter::default();
greeter.config = Greeter::options().parse(&["--width", "10", "--container-padding", "1"]).ok();
greeter.greeting = Some("Hello World".into());
let (text, width) = get_greeting_height(&greeter, 1, 0);
assert!(matches!(text.as_deref(), Some("Hello\nWorld")));
assert_eq!(width, 3);
}
}