diff --git a/.gitignore b/.gitignore index 4eb8695..cfae3ef 100644 --- a/.gitignore +++ b/.gitignore @@ -371,3 +371,8 @@ compile_commands.json CTestTestfile.cmake _deps + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..126b6dd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ctable" +version = "0.1.0" +edition = "2024" + +[dependencies] +dotenv = "0.15" +chrono = "0.4" +tui = "0.19" +crossterm = "0.26" +anyhow = "1.0" +untis = "0.3.0" diff --git a/src/get/get.rs b/src/get/get.rs new file mode 100644 index 0000000..76474dd --- /dev/null +++ b/src/get/get.rs @@ -0,0 +1,42 @@ +/// Code straight copied from the docs. +/// Should work. + + +use dotenvy::dotenv; +use std::env; + +fn main() -> Result<(), untis::Error> { + // Get the school by its id. + let school = untis::schools::get_by_id(&42)?; + + // Log in with your credentials. The school's details are filled in automatically. + let result = school.client_login( + &env::var("UNTIS_USERNAME")?, + &env::var("UNTIS_PASSWORD")? // avoid password in plaintext cause its fucking stupid as fuck. use env instead cuase securrrrre. + ); + let mut client: untis::Client; + + // Match the result to handle specific error cases. + match result { + Ok(v) => client = v, + Err(untis::Error::Rpc(err)) => { + if err.code == untis::jsonrpc::ErrorCode::InvalidCredentials.as_isize() { + println!("Invalid credentials"); + } + return Err(untis::Error::Rpc(err)); + } + Err(err) => return Err(err)?, + }; + + let date = chrono::Local::now().date_naive() + chrono::Duration::weeks(2); + + // Get the client's own timetable until 2 weeks from now. + let timetable = client.own_timetable_until(&date.into())?; + + for lesson in timetable { + println!("{:?}", lesson); + } + + Ok(()) +} // fn main + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7b48a14 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,261 @@ +use anyhow::{Context, Result}; +use chrono::{Datelike, Duration, Local, NaiveDate}; +use crossterm::{ + event::{self, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use dotenv::dotenv; +use std::{env, io}; +use tui::{ + backend::CrosstermBackend, + layout::{Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph}, + Terminal, +}; +use untis_rs::Client; + +struct App { + current_date: NaiveDate, + timetable: Vec, +} + +impl App { + fn new() -> Self { + let current_date = Local::now().date_naive(); + App { + current_date, + timetable: Vec::new(), + } + } + + fn fetch_timetable(&mut self) -> Result<()> { + dotenv().ok(); + + let username = env::var("UNTIS_USERNAME").context("UNTIS_USERNAME not set")?; + let password = env::var("UNTIS_PASSWORD").context("UNTIS_PASSWORD not set")?; + let school = env::var("UNTIS_SCHOOL").context("UNTIS_SCHOOL not set")?; + let server = env::var("UNTIS_SERVER").context("UNTIS_SERVER not set")?; + + let mut client = Client::new(&username, &password, &school, &server); + client.login()?; + + // Get start and end of the current week (Monday to Friday) + let start = self + .current_date + .pred_opt() + .unwrap() + .week(chrono::Weekday::Mon) + .first_day(); + let end = start + Duration::days(4); + + self.timetable = client.get_timetable(start, end)?; + Ok(()) + } + + fn next_week(&mut self) { + self.current_date += Duration::weeks(1); + } + + fn prev_week(&mut self) { + self.current_date -= Duration::weeks(1); + } + + fn get_week_range(&self) -> (NaiveDate, NaiveDate) { + let start = self + .current_date + .pred_opt() + .unwrap() + .week(chrono::Weekday::Mon) + .first_day(); + let end = start + Duration::days(4); + (start, end) + } +} + +fn main() -> Result<()> { + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app + let mut app = App::new(); + if let Err(e) = app.fetch_timetable() { + println!("Failed to fetch timetable: {}", e); + } + + // Main loop + let res = run_app(&mut terminal, &mut app); + + // Cleanup terminal + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen)?; + terminal.show_cursor()?; + + if let Err(err) = res { + println!("Error: {:?}", err); + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + app: &mut App, +) -> Result<(), anyhow::Error> { + loop { + terminal.draw(|f| ui(f, app))?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('n') => { + app.next_week(); + if let Err(e) = app.fetch_timetable() { + println!("Failed to fetch timetable: {}", e); + } + } + KeyCode::Char('p') => { + app.prev_week(); + if let Err(e) = app.fetch_timetable() { + println!("Failed to fetch timetable: {}", e); + } + } + _ => {} + } + } + } +} + +fn ui(f: &mut tui::Frame, app: &App) { + let (week_start, week_end) = app.get_week_range(); + let title = format!( + "Timetable - Week {} ({} - {})", + week_start.iso_week().week(), + week_start.format("%d.%m.%Y"), + week_end.format("%d.%m.%Y") + ); + + // Create a layout + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) + .split(f.size()); + + // Title block + let title_block = Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(Color::White)); + let title_paragraph = Paragraph::new(title) + .block(title_block) + .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)); + f.render_widget(title_paragraph, chunks[0]); + + // Timetable content + let mut periods_by_day: Vec> = vec![vec![]; 5]; // Monday to Friday + + for period in &app.timetable { + let weekday = period.start_time.weekday().num_days_from_monday(); + if weekday < 5 { + periods_by_day[weekday as usize].push(period); + } + } + + let day_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(20), + ] + .as_ref(), + ) + .split(chunks[1]); + + let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; + for (i, day) in days.iter().enumerate() { + let day_block = Block::default() + .title(*day) + .borders(Borders::ALL) + .style(Style::default().fg(Color::White)); + + let mut day_content = Vec::new(); + for period in &periods_by_day[i] { + let start = period.start_time.format("%H:%M").to_string(); + let end = period.end_time.format("%H:%M").to_string(); + let subject = period + .subjects + .first() + .map(|s| s.name.clone()) + .unwrap_or_default(); + let teacher = period + .teachers + .first() + .map(|t| t.name.clone()) + .unwrap_or_default(); + let room = period + .rooms + .first() + .map(|r| r.name.clone()) + .unwrap_or_default(); + + let period_text = Spans::from(vec![ + Span::styled( + format!("{} - {}: ", start, end), + Style::default().fg(Color::Yellow), + ), + Span::styled( + format!("{} ", subject), + Style::default().fg(Color::Green), + ), + Span::styled( + format!("({}) ", teacher), + Style::default().fg(Color::Blue), + ), + Span::styled( + format!("[{}]", room), + Style::default().fg(Color::Magenta), + ), + ]); + + day_content.push(period_text); + } + + if day_content.is_empty() { + day_content.push(Spans::from(Span::styled( + "No classes", + Style::default().fg(Color::Gray), + ))); + } + + let day_paragraph = Paragraph::new(day_content).block(day_block); + f.render_widget(day_paragraph, day_chunks[i]); + } + + // Help text at the bottom + let help_text = Spans::from(vec![ + Span::styled("n", Style::default().fg(Color::Yellow)), + Span::raw(" - next week, "), + Span::styled("p", Style::default().fg(Color::Yellow)), + Span::raw(" - previous week, "), + Span::styled("q", Style::default().fg(Color::Yellow)), + Span::raw(" - quit"), + ]); + let help_paragraph = Paragraph::new(help_text); + f.render_widget( + help_paragraph, + Layout::default() + .constraints([Constraint::Length(1)].as_ref()) + .margin(1) + .split(f.size())[0], + ); +}