summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs68
-rw-r--r--src/errors.rs88
-rw-r--r--src/handlers.rs28
-rw-r--r--src/lib.rs29
-rw-r--r--src/main.rs10
-rw-r--r--src/models.rs24
6 files changed, 245 insertions, 2 deletions
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..97dac88
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,68 @@
+use std::path::PathBuf;
+
+use serde::{Deserialize, Serialize};
+
+use crate::{Error, Result};
+
+const STYLE: &str = include_str!("../examples/simple-gruvbox.css");
+const BOOKMARKS: &str = include_str!("../examples/bookmarks.json");
+const FAVICON: &str = include_str!("../examples/favicon.svg");
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct Config {
+ pub port: u16,
+ pub style_file: PathBuf,
+ pub bookmarks_file: PathBuf,
+ pub favicon_file: PathBuf,
+}
+
+impl Config {
+ pub fn new() -> Result<Config> {
+ let config_home = if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
+ PathBuf::from(xdg_config_home).join("sbm-rs")
+ } else if let Ok(home) = std::env::var("HOME") {
+ PathBuf::from(home).join(".config/sbm-rs")
+ } else {
+ return Err(Error::ConfigNotFound);
+ };
+
+ if config_home.exists() {
+ Config::load_config(&config_home)
+ } else {
+ Config::generate_defaults(&config_home)
+ }
+ }
+ pub fn load_config(config_home: &PathBuf) -> Result<Config> {
+ let config = config_home.join("config.toml");
+
+ let config = std::fs::read_to_string(config)?;
+
+ let config: Config = toml::from_str(&config)?;
+
+ Ok(config)
+ }
+ pub fn generate_defaults(config_home: &PathBuf) -> Result<Config> {
+ std::fs::create_dir_all(&config_home)?;
+
+ let style_file = config_home.join("style.css");
+ let bookmarks_file = config_home.join("bookmarks.json");
+ let favicon_file = config_home.join("favicon.svg");
+ let config_file = config_home.join("config.toml");
+
+ std::fs::write(&style_file, &STYLE)?;
+ std::fs::write(&bookmarks_file, &BOOKMARKS)?;
+ std::fs::write(&favicon_file, &FAVICON)?;
+
+ let config = Config {
+ port: 8080,
+ style_file,
+ bookmarks_file,
+ favicon_file,
+ };
+
+ let config_file_content = toml::to_string(&config)?;
+ std::fs::write(config_file, config_file_content)?;
+
+ Ok(config)
+ }
+}
diff --git a/src/errors.rs b/src/errors.rs
new file mode 100644
index 0000000..251ee5f
--- /dev/null
+++ b/src/errors.rs
@@ -0,0 +1,88 @@
+use axum::{
+ http::StatusCode,
+ response::{IntoResponse, Response},
+};
+use derive_more::From;
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+#[derive(Debug, From)]
+pub enum Error {
+ ConfigNotFound,
+ ConfigHomeNotFound,
+
+ #[from]
+ Io(std::io::Error),
+
+ #[from]
+ EnvVar(std::env::VarError),
+
+ #[from]
+ Askama(askama::Error),
+
+ #[from]
+ SerdeError(serde_json::Error),
+
+ #[from]
+ TomlDeError(toml::de::Error),
+
+ #[from]
+ TomlSerError(toml::ser::Error),
+}
+
+impl core::fmt::Display for Error {
+ fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), std::fmt::Error> {
+ match self {
+ Error::ConfigNotFound => write!(
+ fmt,
+ "Config file can't be found at ~/.config/sbm-rs/config.toml"
+ ),
+ Error::ConfigHomeNotFound => write!(fmt, "Config home can't be found"),
+ Error::Io(e) => write!(fmt, "{e}"),
+ Error::EnvVar(e) => write!(fmt, "Environment variable error: {e}"),
+ Error::Askama(e) => write!(fmt, "Askama error: {e}"),
+ Error::SerdeError(e) => write!(fmt, "Serde error: {e}"),
+ Error::TomlDeError(e) => write!(fmt, "TOML deserialization error: {e}"),
+ Error::TomlSerError(e) => write!(fmt, "TOML serialization error: {e}"),
+ }
+ }
+}
+
+impl IntoResponse for Error {
+ fn into_response(self) -> Response {
+ let (status, error_message) = match self {
+ Error::ConfigNotFound => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Config file can't be found at ~/.config/sbm-rs/config.toml"),
+ ),
+ Error::ConfigHomeNotFound => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Config home can't be found"),
+ ),
+ Error::Io(ref e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")),
+ Error::EnvVar(ref e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Environment variable error: {e}"),
+ ),
+ Error::Askama(ref e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Askama error: {e}"),
+ ),
+ Error::SerdeError(ref e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("Serde error: {e}"),
+ ),
+ Error::TomlDeError(ref e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("TOML deserialization error: {e}"),
+ ),
+ Error::TomlSerError(ref e) => (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ format!("TOML serialization error: {e}"),
+ ),
+ };
+
+ println!("{} {}", &status, &error_message);
+ (status, error_message).into_response()
+ }
+}
diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644
index 0000000..b235c84
--- /dev/null
+++ b/src/handlers.rs
@@ -0,0 +1,28 @@
+use askama::Template;
+use axum::{extract::State, response::Html};
+
+use crate::{
+ Result,
+ config::Config,
+ models::{Bookmarks, Section},
+};
+
+#[derive(Template)]
+#[template(path = "index.html")]
+pub struct MyTemplate {
+ bookmarks: Vec<Section>,
+}
+
+pub async fn handler(State(config): State<Config>) -> Result<Html<String>> {
+ let bookmarks = std::fs::read_to_string(config.bookmarks_file)?;
+
+ let bookmarks: Bookmarks = serde_json::from_str(&bookmarks)?;
+
+ let template = MyTemplate {
+ bookmarks: bookmarks.sections,
+ };
+
+ let html = template.render()?;
+
+ Ok(Html(html))
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..c328a44
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,29 @@
+mod config;
+mod errors;
+mod handlers;
+mod models;
+
+pub use errors::{Error, Result};
+
+use std::net::{IpAddr, Ipv6Addr, SocketAddr};
+
+use axum::{Router, routing::get};
+use tokio::net::TcpListener;
+use tower_http::services::ServeFile;
+
+pub async fn run() -> Result<()> {
+ let config = config::Config::new()?;
+
+ let app = Router::new()
+ .nest_service("/static/style.css", ServeFile::new(&config.style_file))
+ .nest_service("/static/favicon.svg", ServeFile::new(&config.favicon_file))
+ .route("/", get(handlers::handler))
+ .with_state(config.clone());
+
+ let socket = SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), config.port);
+ let listener = TcpListener::bind(socket).await?;
+
+ axum::serve(listener, app).await?;
+
+ Ok(())
+}
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..4d78916 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,9 @@
-fn main() {
- println!("Hello, world!");
+use colored::Colorize;
+
+#[tokio::main]
+async fn main() {
+ if let Err(e) = sbm_rs::run().await {
+ eprintln!("{} {}", "Error:".red().bold(), e);
+ std::process::exit(1);
+ }
}
diff --git a/src/models.rs b/src/models.rs
new file mode 100644
index 0000000..7b99ba5
--- /dev/null
+++ b/src/models.rs
@@ -0,0 +1,24 @@
+use serde::Deserialize;
+
+#[derive(Deserialize)]
+pub struct Bookmarks {
+ pub sections: Vec<Section>,
+}
+
+#[derive(Deserialize)]
+pub struct Section {
+ pub title: String,
+ pub cards: Vec<Card>,
+}
+
+#[derive(Deserialize)]
+pub struct Card {
+ pub title: String,
+ pub links: Vec<Link>,
+}
+
+#[derive(Deserialize)]
+pub struct Link {
+ pub title: String,
+ pub url: String,
+}