From 8b5a71dd3a490718540a7e9d48da2a220256a628 Mon Sep 17 00:00:00 2001 From: Naz Date: Mon, 15 Sep 2025 14:50:08 +0100 Subject: =?UTF-8?q?=E2=9C=A8feat:=20dirty=20commit=20bringing=20basic=20fu?= =?UTF-8?q?nctionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/errors.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/handlers.rs | 28 ++++++++++++++++++ src/lib.rs | 29 +++++++++++++++++++ src/main.rs | 10 +++++-- src/models.rs | 24 ++++++++++++++++ 6 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 src/config.rs create mode 100644 src/errors.rs create mode 100644 src/handlers.rs create mode 100644 src/lib.rs create mode 100644 src/models.rs (limited to 'src') 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 { + 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 { + 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 { + 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 = core::result::Result; + +#[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
, +} + +pub async fn handler(State(config): State) -> Result> { + 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
, +} + +#[derive(Deserialize)] +pub struct Section { + pub title: String, + pub cards: Vec, +} + +#[derive(Deserialize)] +pub struct Card { + pub title: String, + pub links: Vec, +} + +#[derive(Deserialize)] +pub struct Link { + pub title: String, + pub url: String, +} -- cgit v1.2.3