Initial commit
Restructured code architecture, with implementation of login screen.
This commit is contained in:
commit
eed07706df
4
.editorconfig
Normal file
4
.editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = tab
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
4771
Cargo.lock
generated
Normal file
4771
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal 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
7
rustfmt.toml
Normal 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
145
src/config.rs
Normal 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
29
src/main.rs
Normal 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
80
src/ui.rs
Normal 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
144
src/ui/login.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue