Initial implementation

This commit is contained in:
Jan-Bulthuis 2025-02-21 18:34:31 +01:00
parent cd54eff6fe
commit 17448f5de7
5 changed files with 408 additions and 102 deletions

21
Cargo.lock generated
View File

@ -157,6 +157,7 @@ checksum = "700d2e5bfc91a042d1d531b46de245085c6a179debedf149aa97c7ca71492078"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"thiserror",
] ]
[[package]] [[package]]
@ -526,6 +527,26 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tui-rain" name = "tui-rain"
version = "1.0.1" version = "1.0.1"

View File

@ -5,6 +5,6 @@ edition = "2021"
[dependencies] [dependencies]
crossterm = "0.28.1" crossterm = "0.28.1"
greetd_ipc = "0.10.3" greetd_ipc = { version = "0.10.3", features = ["sync-codec"] }
ratatui = "0.29.0" ratatui = "0.29.0"
tui-rain = "1.0.1" tui-rain = "1.0.1"

104
src/gather.rs Normal file
View File

@ -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<User> {
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<Session> {
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<Session> {
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<Session> {
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()
}

65
src/greetd.rs Normal file
View File

@ -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<LoginResult, Box<dyn Error>> {
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)?;
}
}
}
}

View File

@ -1,77 +1,163 @@
use std::{ use std::{
env,
fmt::Display,
io, io,
marker::PhantomData, os::unix::net::UnixStream,
path::PathBuf,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use greetd::LoginResult;
use ratatui::{ use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Margin, Rect}, layout::{Constraint, Direction, Layout, Margin, Rect},
style::{Color, Stylize}, style::Color,
symbols::border,
text::Line, text::Line,
widgets::{Block, Clear, Widget}, widgets::{Block, Clear},
DefaultTerminal, Frame, DefaultTerminal, Frame,
}; };
use tui_rain::Rain; use tui_rain::Rain;
mod gather;
mod greetd;
fn main() -> io::Result<()> { fn main() -> io::Result<()> {
let mut terminal = ratatui::init(); let mut terminal = ratatui::init();
let app_result = App::default().run(&mut terminal); let app_result = App::default().run(&mut terminal);
ratatui::restore(); ratatui::restore();
app_result app_result
} }
struct App {
#[derive(Debug)]
pub struct App<'a> {
start_time: Instant, start_time: Instant,
last_draw: Instant,
exit: bool, exit: bool,
error: bool,
focus: Focus, focus: Focus,
user_input: SelectorInput<User>,
_marker: PhantomData<&'a ()>, session_input: SelectorInput<Session>,
password_input: TextInput,
} }
#[derive(Debug)] #[derive(PartialEq, Eq)]
enum Focus { enum Focus {
Username, User,
Session,
Password, Password,
} }
impl Default for App<'_> { struct SelectorInput<T: Display> {
fn default() -> Self { values: Vec<T>,
let start_time = Instant::now(); state: usize,
let last_draw = Instant::now(); }
let exit = false;
let focus = Focus::Username; impl<T: Display> SelectorInput<T> {
fn value(&self) -> &T {
&self.values[self.state]
}
Self { fn handle_key_event(&mut self, event: KeyEvent) {
start_time, match event.code {
last_draw, KeyCode::Left => {
exit, self.state = (self.state + self.values.len() - 1) % self.values.len();
}
focus, KeyCode::Right => {
self.state = (self.state + 1) % self.values.len();
_marker: PhantomData, }
_ => {}
} }
} }
} }
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<()> { pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
self.start_time = Instant::now();
while !self.exit { while !self.exit {
let draw_time = Instant::now();
terminal.draw(|frame| self.draw(frame))?; terminal.draw(|frame| self.draw(frame))?;
self.last_draw = draw_time;
self.handle_events()?; self.handle_events()?;
} }
Ok(()) 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()) let rain_widget = Rain::new_matrix(self.start_time.elapsed())
.with_character_set(tui_rain::CharacterSet::UnicodeRange { .with_character_set(tui_rain::CharacterSet::UnicodeRange {
start: 0x21, start: 0x21,
@ -86,106 +172,136 @@ impl App<'_> {
.with_head_color(Color::White) .with_head_color(Color::White)
.with_noise_interval(Duration::from_millis(2000)); .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) .direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(40), Constraint::Percentage(20)]) .constraints(vec![Constraint::Percentage(40), Constraint::Percentage(20)])
.split(frame.area()); .split(frame.area());
let layout = Layout::default()
let middle_rows = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(40), Constraint::Percentage(20)]) .constraints(vec![Constraint::Percentage(40), Constraint::Length(9)])
.split(layout[1]); .split(columns[1]);
let prompt_area = layout[1]; let box_area = middle_rows[1];
let inner_area = prompt_area.inner(Margin::new(1, 1));
let prompt_areas = Layout::default() let box_widget = Block::bordered().title("Bone jaw").style(Color::Gray);
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Length(1)])
.split(inner_area);
frame.render_widget(rain_widget, frame.area()); frame.render_widget(Clear, box_area);
frame.render_widget(Clear, prompt_area); frame.render_widget(box_widget, box_area);
let title = Line::from(" Hewwo! >w< :3 ".bold()); let inner_area = box_area.inner(Margin::new(2, 2));
let instructions = Line::from(vec![
" Decrement ".into(),
"<Left>".blue().bold(),
" Increment ".into(),
"<Right>".blue().bold(),
" Quit ".into(),
"<Q> ".blue().bold(),
]);
let prompt_block = Block::bordered() let rows: Vec<Rect> = inner_area.rows().collect();
.title(title.left_aligned())
.title_bottom(instructions.centered())
.border_set(border::PLAIN);
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); let session_row = Line::from(format!("Session: {}", self.session_input.value())).style(
// TextPrompt::from("Password") match self.focus {
// .with_render_style(TextRenderStyle::Password) Focus::Session => Color::White,
// .draw(frame, prompt_areas[1], &mut self.password_state); _ => Color::Gray,
},
);
frame.render_widget(session_row, rows[2]);
let password_row = Line::from(format!(
"Password: {}",
self.password_input
.value()
.chars()
.map(|_| '*')
.collect::<String>()
))
.style(match self.focus {
Focus::Password => Color::White,
_ => Color::Gray,
});
frame.render_widget(password_row, rows[4]);
} }
fn handle_events(&mut self) -> io::Result<()> { fn handle_events(&mut self) -> io::Result<()> {
if event::poll(Duration::from_millis(10))? { if event::poll(Duration::from_millis(16))? {
match event::read()? { match event::read()? {
Event::Key(key_event) if key_event.kind == KeyEventKind::Press => { Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
self.handle_key_event(key_event) self.handle_key_event(key_event)?;
} }
_ => {} _ => {}
} }
}; }
Ok(()) Ok(())
} }
fn handle_key_event(&mut self, key_event: KeyEvent) { fn handle_key_event(&mut self, event: KeyEvent) -> io::Result<()> {
match key_event.code { match event.code {
KeyCode::Esc => self.exit(), KeyCode::Esc => self.exit(),
KeyCode::Tab => match self.focus { KeyCode::Enter if self.focus == Focus::Password => self.submit(),
Focus::Username => self.focus = Focus::Password, KeyCode::Enter => self.focus_cycle_forward(),
Focus::Password => self.focus = Focus::Username, KeyCode::Tab => self.focus_cycle_forward(),
}, KeyCode::Down => self.focus_cycle_forward(),
KeyCode::BackTab => match self.focus { KeyCode::BackTab => self.focus_cycle_backward(),
Focus::Username => self.focus = Focus::Password, KeyCode::Up => self.focus_cycle_backward(),
Focus::Password => self.focus = Focus::Username, _ => self.focus_handle_key_event(event),
}, }
_ => self.focus_handle_key_event(key_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) { fn focus_cycle_backward(&mut self) {
// let state = match self.focus { self.focus = match self.focus {
// Focus::Username => &mut self.username_state, Focus::User => Focus::Password,
// Focus::Password => &mut self.password_state, Focus::Session => Focus::User,
// }; Focus::Password => Focus::Session,
// state.handle_key_event(key_event); }
}
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) { fn exit(&mut self) {
self.exit = true; self.exit = true;
} }
}
impl Widget for &mut App<'_> { fn submit(&mut self) {
fn render(self, area: Rect, buf: &mut Buffer) { match greetd::login(
let title = Line::from(" Hewwo >w< :3 ".bold()); self.user_input.value(),
let instructions = Line::from(vec![ self.session_input.value(),
" Decrement ".into(), self.password_input.value(),
"<Left>".blue().bold(), )
" Increment ".into(), .unwrap()
"<Right>".blue().bold(), {
" Quit ".into(), LoginResult::Success => {
"<Q> ".blue().bold(), self.error = false;
]); }
LoginResult::Failure => self.error = true,
let block = Block::bordered() };
.title(title.left_aligned())
.title_bottom(instructions.centered())
.border_set(border::PLAIN);
block.render(area, buf);
} }
} }