From 17448f5de71343a243ba2899f7c9dbdad5590ad1 Mon Sep 17 00:00:00 2001 From: Jan-Bulthuis Date: Fri, 21 Feb 2025 18:34:31 +0100 Subject: [PATCH] Initial implementation --- Cargo.lock | 21 ++++ Cargo.toml | 2 +- src/gather.rs | 104 +++++++++++++++++ src/greetd.rs | 65 +++++++++++ src/main.rs | 318 ++++++++++++++++++++++++++++++++++---------------- 5 files changed, 408 insertions(+), 102 deletions(-) create mode 100644 src/gather.rs create mode 100644 src/greetd.rs diff --git a/Cargo.lock b/Cargo.lock index 62da3b6..b489479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,7 @@ checksum = "700d2e5bfc91a042d1d531b46de245085c6a179debedf149aa97c7ca71492078" dependencies = [ "serde", "serde_json", + "thiserror", ] [[package]] @@ -526,6 +527,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tui-rain" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 13fa6dd..5ac182e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" [dependencies] crossterm = "0.28.1" -greetd_ipc = "0.10.3" +greetd_ipc = { version = "0.10.3", features = ["sync-codec"] } ratatui = "0.29.0" tui-rain = "1.0.1" diff --git a/src/gather.rs b/src/gather.rs new file mode 100644 index 0000000..0ad31c6 --- /dev/null +++ b/src/gather.rs @@ -0,0 +1,104 @@ +use std::{ + fs::{read_dir, read_link, DirEntry}, + path::{Path, PathBuf}, +}; + +use crate::{Session, User}; + +#[derive(Debug)] +struct PasswdUser { + name: String, + uid: u32, + gid: u32, + gecos: String, + home: String, + shell: String, +} + +pub fn gather_users() -> Vec { + let passwd = std::fs::read_to_string("/etc/passwd").unwrap(); + passwd + .lines() + .map(|line| { + let fields: Vec<&str> = line.split(":").collect(); + let name = fields[0].to_owned(); + let uid = fields[2].parse().unwrap(); + let gid = fields[3].parse().unwrap(); + let gecos = fields[4].to_owned(); + let home = fields[5].to_owned(); + let shell = fields[6].to_owned(); + + PasswdUser { + name, + uid, + gid, + gecos, + home, + shell, + } + }) + .filter(|user| !user.shell.ends_with("nologin")) + .map(|user| User { + name: user.name, + home: user.home.into(), + shell: user.shell.into(), + }) + .collect() +} + +pub fn gather_sessions(user: &User) -> Vec { + let hm_profile = user.home.join(".local/state/nix/profiles/home-manager"); + match hm_profile.exists() { + true => gather_hm_sessions(&hm_profile), + false => vec![Session { + name: String::from("Shell"), + path: user.shell.clone(), + }], + } +} + +#[allow(clippy::ptr_arg)] +fn resolve_link(path: &PathBuf) -> PathBuf { + let mut location = path.clone(); + while let Ok(next) = read_link(&location) { + let base_path = location.parent().unwrap(); + let next_path = base_path.join(next); + location = next_path; + } + location +} + +fn gather_hm_sessions(path: &PathBuf) -> Vec { + let generation = resolve_link(path); + let mut sessions = vec![Session { + name: String::from("Home Manager"), + path: generation, + }]; + + sessions.append(&mut gather_specialisations(&sessions[0])); + + sessions +} + +fn gather_specialisations(session: &Session) -> Vec { + let specialisation_path = session.path.join("specialisation"); + if !specialisation_path.exists() { + return vec![]; + } + + read_dir(specialisation_path) + .unwrap() + .flatten() + .map(|entry| { + let path = resolve_link(&entry.path()); + let name = entry + .path() + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_owned(); + Session { name, path } + }) + .collect() +} diff --git a/src/greetd.rs b/src/greetd.rs new file mode 100644 index 0000000..b681831 --- /dev/null +++ b/src/greetd.rs @@ -0,0 +1,65 @@ +use std::{env, error::Error, os::unix::net::UnixStream}; + +use greetd_ipc::{codec::SyncCodec, AuthMessageType, ErrorType, Request, Response}; + +use crate::{Session, User}; + +pub enum LoginResult { + Success, + Failure, +} + +pub fn login( + user: &User, + session: &Session, + password: &str, +) -> Result> { + let mut stream = UnixStream::connect(env::var("GREETD_SOCK")?)?; + + Request::CreateSession { + username: user.name.clone(), + } + .write_to(&mut stream)?; + + let mut starting = false; + + loop { + match Response::read_from(&mut stream)? { + Response::Success => { + if starting { + return Ok(LoginResult::Success); + } else { + starting = true; + let cmd = vec![user.shell.to_str().unwrap().to_owned()]; + let env = vec![]; + Request::StartSession { cmd, env }.write_to(&mut stream)?; + } + } + Response::Error { + error_type, + description, + } => { + Request::CancelSession.write_to(&mut stream)?; + match error_type { + ErrorType::Error => { + return Err(format!("login error: {:?}", description).into()) + } + ErrorType::AuthError => return Ok(LoginResult::Failure), + } + } + Response::AuthMessage { + auth_message_type, + auth_message, + } => { + let response = match auth_message_type { + AuthMessageType::Visible => todo!(), + AuthMessageType::Secret => Some(password.to_string()), + AuthMessageType::Info => todo!(), + AuthMessageType::Error => todo!(), + }; + + Request::PostAuthMessageResponse { response }.write_to(&mut stream)?; + } + } + } +} diff --git a/src/main.rs b/src/main.rs index c65e49a..ce4b762 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,77 +1,163 @@ use std::{ + env, + fmt::Display, io, - marker::PhantomData, + os::unix::net::UnixStream, + path::PathBuf, time::{Duration, Instant}, }; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; +use greetd::LoginResult; use ratatui::{ - buffer::Buffer, layout::{Constraint, Direction, Layout, Margin, Rect}, - style::{Color, Stylize}, - symbols::border, + style::Color, text::Line, - widgets::{Block, Clear, Widget}, + widgets::{Block, Clear}, DefaultTerminal, Frame, }; use tui_rain::Rain; +mod gather; +mod greetd; + fn main() -> io::Result<()> { let mut terminal = ratatui::init(); let app_result = App::default().run(&mut terminal); ratatui::restore(); app_result } - -#[derive(Debug)] -pub struct App<'a> { +struct App { start_time: Instant, - last_draw: Instant, exit: bool, - + error: bool, focus: Focus, - - _marker: PhantomData<&'a ()>, + user_input: SelectorInput, + session_input: SelectorInput, + password_input: TextInput, } -#[derive(Debug)] +#[derive(PartialEq, Eq)] enum Focus { - Username, + User, + Session, Password, } -impl Default for App<'_> { - fn default() -> Self { - let start_time = Instant::now(); - let last_draw = Instant::now(); - let exit = false; +struct SelectorInput { + values: Vec, + state: usize, +} - let focus = Focus::Username; +impl SelectorInput { + fn value(&self) -> &T { + &self.values[self.state] + } - Self { - start_time, - last_draw, - exit, - - focus, - - _marker: PhantomData, + fn handle_key_event(&mut self, event: KeyEvent) { + match event.code { + KeyCode::Left => { + self.state = (self.state + self.values.len() - 1) % self.values.len(); + } + KeyCode::Right => { + self.state = (self.state + 1) % self.values.len(); + } + _ => {} } } } -impl App<'_> { +struct TextInput { + state: String, +} + +impl TextInput { + fn value(&self) -> &str { + &self.state + } + + fn handle_key_event(&mut self, event: KeyEvent) { + match event.code { + KeyCode::Char(c) => self.state.push(c), + KeyCode::Backspace => { + self.state.pop(); + } + _ => {} + } + } +} + +#[derive(PartialEq, Eq, Clone)] +struct User { + name: String, + home: PathBuf, + shell: PathBuf, +} + +impl Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +#[derive(PartialEq, Eq, Clone)] +struct Session { + name: String, + path: PathBuf, +} + +impl Display for Session { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Default for App { + fn default() -> Self { + let start_time = Instant::now(); + let exit = false; + let error = false; + let focus = Focus::User; + let user_input = SelectorInput { + values: gather::gather_users(), + state: 0, + }; + let session_input = SelectorInput { + values: gather::gather_sessions(user_input.value()), + state: 0, + }; + let password_input = TextInput { + state: String::new(), + }; + + Self { + start_time, + exit, + error, + focus, + user_input, + session_input, + password_input, + } + } +} + +impl App { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { + self.start_time = Instant::now(); while !self.exit { - let draw_time = Instant::now(); terminal.draw(|frame| self.draw(frame))?; - self.last_draw = draw_time; self.handle_events()?; } Ok(()) } - fn draw(&mut self, frame: &mut Frame) { + fn draw(&self, frame: &mut Frame<'_>) { + self.draw_background(frame); + self.draw_prompt_box(frame); + } + + fn draw_background(&self, frame: &mut Frame<'_>) { let rain_widget = Rain::new_matrix(self.start_time.elapsed()) .with_character_set(tui_rain::CharacterSet::UnicodeRange { start: 0x21, @@ -86,106 +172,136 @@ impl App<'_> { .with_head_color(Color::White) .with_noise_interval(Duration::from_millis(2000)); - let layout = Layout::default() + frame.render_widget(rain_widget, frame.area()); + } + + fn draw_prompt_box(&self, frame: &mut Frame<'_>) { + let columns = Layout::default() .direction(Direction::Horizontal) .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(20)]) .split(frame.area()); - let layout = Layout::default() + + let middle_rows = Layout::default() .direction(Direction::Vertical) - .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(20)]) - .split(layout[1]); + .constraints(vec![Constraint::Percentage(40), Constraint::Length(9)]) + .split(columns[1]); - let prompt_area = layout[1]; - let inner_area = prompt_area.inner(Margin::new(1, 1)); + let box_area = middle_rows[1]; - let prompt_areas = Layout::default() - .direction(Direction::Vertical) - .constraints(vec![Constraint::Length(1), Constraint::Length(1)]) - .split(inner_area); + let box_widget = Block::bordered().title("Bone jaw").style(Color::Gray); - frame.render_widget(rain_widget, frame.area()); - frame.render_widget(Clear, prompt_area); + frame.render_widget(Clear, box_area); + frame.render_widget(box_widget, box_area); - let title = Line::from(" Hewwo! >w< :3 ".bold()); - let instructions = Line::from(vec![ - " Decrement ".into(), - "".blue().bold(), - " Increment ".into(), - "".blue().bold(), - " Quit ".into(), - " ".blue().bold(), - ]); + let inner_area = box_area.inner(Margin::new(2, 2)); - let prompt_block = Block::bordered() - .title(title.left_aligned()) - .title_bottom(instructions.centered()) - .border_set(border::PLAIN); + let rows: Vec = inner_area.rows().collect(); - frame.render_widget(prompt_block, prompt_area); + let user_row = + Line::from(format!("User: {}", self.user_input.value())).style(match self.focus { + Focus::User => Color::White, + _ => Color::Gray, + }); + frame.render_widget(user_row, rows[0]); - // TextPrompt::from("User").draw(frame, prompt_areas[0], &mut self.username_state); - // TextPrompt::from("Password") - // .with_render_style(TextRenderStyle::Password) - // .draw(frame, prompt_areas[1], &mut self.password_state); + let session_row = Line::from(format!("Session: {}", self.session_input.value())).style( + match self.focus { + Focus::Session => Color::White, + _ => Color::Gray, + }, + ); + frame.render_widget(session_row, rows[2]); + + let password_row = Line::from(format!( + "Password: {}", + self.password_input + .value() + .chars() + .map(|_| '*') + .collect::() + )) + .style(match self.focus { + Focus::Password => Color::White, + _ => Color::Gray, + }); + frame.render_widget(password_row, rows[4]); } fn handle_events(&mut self) -> io::Result<()> { - if event::poll(Duration::from_millis(10))? { + if event::poll(Duration::from_millis(16))? { match event::read()? { Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { - self.handle_key_event(key_event) + self.handle_key_event(key_event)?; } _ => {} } - }; + } Ok(()) } - fn handle_key_event(&mut self, key_event: KeyEvent) { - match key_event.code { + fn handle_key_event(&mut self, event: KeyEvent) -> io::Result<()> { + match event.code { KeyCode::Esc => self.exit(), - KeyCode::Tab => match self.focus { - Focus::Username => self.focus = Focus::Password, - Focus::Password => self.focus = Focus::Username, - }, - KeyCode::BackTab => match self.focus { - Focus::Username => self.focus = Focus::Password, - Focus::Password => self.focus = Focus::Username, - }, - _ => self.focus_handle_key_event(key_event), + KeyCode::Enter if self.focus == Focus::Password => self.submit(), + KeyCode::Enter => self.focus_cycle_forward(), + KeyCode::Tab => self.focus_cycle_forward(), + KeyCode::Down => self.focus_cycle_forward(), + KeyCode::BackTab => self.focus_cycle_backward(), + KeyCode::Up => self.focus_cycle_backward(), + _ => self.focus_handle_key_event(event), + } + Ok(()) + } + + fn focus_cycle_forward(&mut self) { + self.focus = match self.focus { + Focus::User => Focus::Session, + Focus::Session => Focus::Password, + Focus::Password => Focus::User, } } - fn focus_handle_key_event(&mut self, key_event: KeyEvent) { - // let state = match self.focus { - // Focus::Username => &mut self.username_state, - // Focus::Password => &mut self.password_state, - // }; - // state.handle_key_event(key_event); + fn focus_cycle_backward(&mut self) { + self.focus = match self.focus { + Focus::User => Focus::Password, + Focus::Session => Focus::User, + Focus::Password => Focus::Session, + } + } + + fn focus_handle_key_event(&mut self, event: KeyEvent) { + match self.focus { + Focus::User => { + let old_value = self.user_input.value().clone(); + self.user_input.handle_key_event(event); + if self.user_input.value() != &old_value { + self.session_input = SelectorInput { + values: gather::gather_sessions(self.user_input.value()), + state: 0, + } + }; + } + Focus::Session => self.session_input.handle_key_event(event), + Focus::Password => self.password_input.handle_key_event(event), + } } fn exit(&mut self) { self.exit = true; } -} -impl Widget for &mut App<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let title = Line::from(" Hewwo >w< :3 ".bold()); - let instructions = Line::from(vec![ - " Decrement ".into(), - "".blue().bold(), - " Increment ".into(), - "".blue().bold(), - " Quit ".into(), - " ".blue().bold(), - ]); - - let block = Block::bordered() - .title(title.left_aligned()) - .title_bottom(instructions.centered()) - .border_set(border::PLAIN); - - block.render(area, buf); + fn submit(&mut self) { + match greetd::login( + self.user_input.value(), + self.session_input.value(), + self.password_input.value(), + ) + .unwrap() + { + LoginResult::Success => { + self.error = false; + } + LoginResult::Failure => self.error = true, + }; } }