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