mirror of
https://github.com/apognu/tuigreet.git
synced 2024-09-11 07:25:29 +03:00
Refactored session management, added comments and tests.
This commit is contained in:
parent
158a98e85d
commit
a3b95d1bad
158
src/greeter.rs
158
src/greeter.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
10
src/info.rs
10
src/info.rs
@ -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) {
|
||||
|
59
src/ipc.rs
59
src/ipc.rs
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
|
@ -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")),
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
|
163
src/ui/util.rs
163
src/ui/util.rs
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user