Initial commit

Restructured code architecture, with implementation of login screen.
This commit is contained in:
Amanda Graven 2021-10-15 08:42:38 +02:00
commit eed07706df
Signed by: amanda
GPG key ID: 45C461CDC9286390
9 changed files with 5204 additions and 0 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
root = true
[*.rs]
indent_style = tab

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

4771
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

23
Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "retrix"
authors = ["Amanda Graven <amanda@amandag.net>"]
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
directories = "3.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dependencies.iced]
git = "https://github.com/hecrj/iced"
rev = "a08e4eb"
features = ["image", "svg", "debug"]
[dependencies.matrix-sdk]
git = "https://github.com/matrix-org/matrix-rust-sdk"
rev = "1fd1570"
default-features = false
features = ["encryption", "sled_state_store", "sled_cryptostore", "rustls-tls", "require_auth_for_profile_requests"]

7
rustfmt.toml Normal file
View file

@ -0,0 +1,7 @@
edition = "2018"
hard_tabs=true
max_width = 100
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"

145
src/config.rs Normal file
View file

@ -0,0 +1,145 @@
//! Configuration
use std::{fmt::Display, fs::File, io::ErrorKind as IoErrorKind};
use serde::{Deserialize, Serialize};
/// Result alias for this module
pub type Result<T> = std::result::Result<T, Error>;
/// Data for a valid login, including access token and homeserver address
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Session {
/// Access token, mxid and device id of the session
pub session: matrix_sdk::Session,
/// The address of the homeserver
pub homeserver: String,
}
impl Session {
/// Read session data from disk.
pub fn from_file() -> Result<Option<Session>> {
let dirs = dirs()?;
let file = File::open(dirs.data_dir().join("session.json"));
let file = match file {
Ok(file) => file,
Err(e) => {
return match e.kind() {
IoErrorKind::NotFound => Ok(None),
_ => Err(e.into()),
}
}
};
let session = serde_json::from_reader(file)?;
Ok(session)
}
/// Writes the session data to disk.
pub fn write(&self) -> Result<()> {
let dirs = dirs()?;
std::fs::create_dir_all(dirs.data_dir())?;
let file = File::create(dirs.data_dir().join("session.json"))?;
serde_json::to_writer_pretty(file, self)?;
Ok(())
}
}
/// Configuration for the application
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
/// The global zoom level.
pub zoom: f64,
}
impl Config {
/// Creates a new `Config` with default settings
pub fn new() -> Config {
Config { zoom: 1.0 }
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl Config {
/// Reads the configuration from disk, or creates a new file with default settings if none
/// exists
pub fn from_file() -> Result<Config> {
let dirs = dirs()?;
let file = File::open(dirs.config_dir().join("retrix.json"));
// Create the file if it doesn't exist
let file = match file {
Ok(file) => file,
Err(e) => match e.kind() {
IoErrorKind::NotFound => {
let config = Config::default();
config.write()?;
return Ok(config);
}
_ => return Err(e.into()),
},
};
let config: Config = serde_json::from_reader(file)?;
Ok(config)
}
/// Writes the configuration to disk
pub fn write(&self) -> Result<()> {
let dirs = dirs()?;
std::fs::create_dir_all(dirs.config_dir())?;
let file = File::create(dirs.config_dir().join("retrix.json"))?;
serde_json::to_writer_pretty(file, self)?;
Ok(())
}
}
/// Generates structure holding paths for config dirs etc.
pub fn dirs() -> Result<dirs::ProjectDirs> {
dirs::ProjectDirs::from("net.amandag", "", "retrix").ok_or(Error::HomeDir)
}
/// Errors which can happen when handling config files
#[derive(Debug)]
pub enum Error {
/// An error happened opening or creating a file
Io(std::io::Error),
/// (De)serializing a file failed
Parsing(serde_json::Error),
/// The home directory couldn't be found
HomeDir,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Io(e) => write!(f, "Error opening file: {}", e),
Error::Parsing(e) => write!(f, "Error in config file: {}", e),
Error::HomeDir => write!(f, "Couldn't find home directory"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Error::Io(ref e) => Some(e),
Error::Parsing(ref e) => Some(e),
Error::HomeDir => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Parsing(e)
}
}

29
src/main.rs Normal file
View file

@ -0,0 +1,29 @@
//! Retrix is a matrix client
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unused_extern_crates,
unused_allocation,
unused_qualifications
)]
use config::Config;
use iced::{Application, Settings};
use ui::Flags;
use crate::{config::Session, ui::Retrix};
extern crate directories as dirs;
pub mod config;
pub mod ui;
fn main() {
let session = Session::from_file().unwrap();
let config = Config::from_file().unwrap();
let settings =
Settings { text_multithreading: true, ..Settings::with_flags(Flags { config, session }) };
Retrix::run(settings).unwrap();
}

80
src/ui.rs Normal file
View file

@ -0,0 +1,80 @@
//! The graphical iced-based interface
use iced::Command;
use crate::config;
pub mod login;
/// Data for the running application
#[derive(Debug)]
pub struct Retrix {
/// The currently active view
pub view: View,
/// Configuration
pub config: config::Config,
}
/// Available application views.
#[derive(Debug)]
pub enum View {
/// The login prompt
Login(login::Login),
}
/// A message notifying application state should change
#[derive(Debug, Clone)]
pub enum Message {
/// Do nothing. A workaround for async stuff
Noop,
/// A message for the login view
Login(login::Message),
}
/// Data for application startup
#[derive(Debug)]
pub struct Flags {
/// The application configuration
pub config: config::Config,
/// The session data if we've loggen in
pub session: Option<config::Session>,
}
impl iced::Application for Retrix {
type Executor = iced::executor::Default;
type Message = Message;
type Flags = Flags;
fn new(flags: Self::Flags) -> (Self, Command<Message>) {
let state = Retrix { view: View::Login(login::Login::default()), config: flags.config };
(state, Command::none())
}
fn title(&self) -> String {
String::from("Retrix")
}
fn update(
&mut self,
message: Self::Message,
_clipboard: &mut iced::Clipboard,
) -> Command<Self::Message> {
match (&mut self.view, message) {
(View::Login(ref mut view), Message::Login(message)) => view.update(message),
_ => {
eprint!("WARN: Received a message for an inactive view");
Command::none()
}
}
}
fn view(&mut self) -> iced::Element<'_, Self::Message> {
match self.view {
View::Login(ref mut login) => login.view(),
}
}
fn scale_factor(&self) -> f64 {
self.config.zoom
}
}

144
src/ui/login.rs Normal file
View file

@ -0,0 +1,144 @@
//! The login prompt
use iced::{
widget::{button::State as ButtonState, text_input::State as InputState},
Button, Column, Command, Container, Element, Length, Space, Text, TextInput, Toggler,
};
use self::Message::{InputHomeserver, InputPassword, InputUser, LoginFailed, TogglePassword};
/// Data for the login prompt
#[derive(Debug, Default)]
pub struct Login {
/// The username
user: String,
/// The password
password: String,
/// Whether the password should be visible.
show_password: bool,
/// The homeserver URL.
homeserver: String,
/// Whether we're waiting for a response to a login attempt
waiting: bool,
/// Widget state
state: State,
}
/// Login prompt widget state
#[derive(Debug, Default)]
pub struct State {
/// State for the login input
user: InputState,
/// State from the password input
password: InputState,
/// State for the homeserver input
homeserver: InputState,
/// State for the login button
login: ButtonState,
}
/// Notification to change the state for the login view.
#[derive(Debug, Clone)]
pub enum Message {
/// The login input changed.
InputUser(String),
/// The password input changed.
InputPassword(String),
/// The homerserver input changed.
InputHomeserver(String),
/// The "show password" toggle has been switched.
TogglePassword(bool),
/// Triggers login
Login,
/// A login attempt failed
LoginFailed(String),
}
impl Login {
/// Update state
pub fn update(&mut self, message: Message) -> Command<super::Message> {
match message {
InputUser(input) => self.user = input,
InputPassword(input) => self.password = input,
InputHomeserver(input) => self.homeserver = input,
TogglePassword(toggle) => self.show_password = toggle,
Message::Login => {
self.waiting = true;
let command = async {
std::thread::sleep_ms(1000);
super::Message::from(LoginFailed(String::from("Not implemented :(")))
};
return command.into();
}
LoginFailed(error) => {
self.waiting = false;
}
};
Command::none()
}
/// Generate widgets for this view
pub fn view(&mut self) -> Element<super::Message> {
let user_input =
TextInput::new(&mut self.state.user, "alice", &self.user, |i| InputUser(i).into())
.padding(5);
let mut password_input =
TextInput::new(&mut self.state.password, "verysecret", &self.password, |i| {
InputPassword(i).into()
})
.padding(5);
if !self.show_password {
password_input = password_input.password();
}
let password_toggle =
Toggler::new(self.show_password, String::from("show password"), |b| {
TogglePassword(b).into()
})
.text_size(15);
let homeserver_input = TextInput::new(
&mut self.state.homeserver,
"https://matrix.org",
&self.homeserver,
|i| InputHomeserver(i).into(),
)
.padding(5);
let login_button = Button::new(
&mut self.state.login,
Text::new("Log in")
.horizontal_alignment(iced::HorizontalAlignment::Center)
.width(Length::Fill),
)
.on_press(Message::Login.into())
.width(Length::Fill);
let mut column = Column::new()
.width(500.into())
.push(Text::new("User name"))
.push(user_input)
.push(Space::with_height(10.into()))
.push(Text::new("Password"))
.push(password_input)
.push(password_toggle)
.push(Space::with_height(5.into()))
.push(Text::new("Homeserver address"))
.push(homeserver_input)
.push(login_button);
if self.waiting {
column = column.push(Text::new("Loggin in"));
}
Container::new(column).center_x().center_y().width(Length::Fill).height(Length::Fill).into()
}
}
impl From<Message> for super::Message {
fn from(message: Message) -> Self {
super::Message::Login(message)
}
}