Initial implementation
This commit is contained in:
parent
cd54eff6fe
commit
17448f5de7
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
318
src/main.rs
318
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<User>,
|
||||
session_input: SelectorInput<Session>,
|
||||
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<T: Display> {
|
||||
values: Vec<T>,
|
||||
state: usize,
|
||||
}
|
||||
|
||||
let focus = Focus::Username;
|
||||
impl<T: Display> SelectorInput<T> {
|
||||
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(),
|
||||
"<Left>".blue().bold(),
|
||||
" Increment ".into(),
|
||||
"<Right>".blue().bold(),
|
||||
" Quit ".into(),
|
||||
"<Q> ".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<Rect> = 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::<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<()> {
|
||||
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(),
|
||||
"<Left>".blue().bold(),
|
||||
" Increment ".into(),
|
||||
"<Right>".blue().bold(),
|
||||
" Quit ".into(),
|
||||
"<Q> ".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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue