chore: setup project structure
This commit is contained in:
983
Cargo.lock
generated
983
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ publish = false
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# GPUI
|
# GPUI
|
||||||
gpui = { git = "https://github.com/zed-industries/zed" }
|
gpui = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
gpui-component = { git = "https://github.com/longbridge/gpui-component" }
|
||||||
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
gpui_tokio = { git = "https://github.com/zed-industries/zed" }
|
||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
|
|||||||
11
crates/assets/Cargo.toml
Normal file
11
crates/assets/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "assets"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
rust-embed.workspace = true
|
||||||
59
crates/assets/src/lib.rs
Normal file
59
crates/assets/src/lib.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use gpui::{App, AssetSource, Result, SharedString};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "../../assets"]
|
||||||
|
#[include = "fonts/**/*"]
|
||||||
|
#[include = "brand/**/*"]
|
||||||
|
#[include = "icons/**/*"]
|
||||||
|
#[exclude = "*.DS_Store"]
|
||||||
|
pub struct Assets;
|
||||||
|
|
||||||
|
impl AssetSource for Assets {
|
||||||
|
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
|
||||||
|
Self::get(path)
|
||||||
|
.map(|f| Some(f.data))
|
||||||
|
.with_context(|| format!("loading asset at path {path:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||||
|
Ok(Self::iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
if p.starts_with(path) {
|
||||||
|
Some(p.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Assets {
|
||||||
|
/// Populate the [`TextSystem`] of the given [`AppContext`] with all `.ttf` fonts in the `fonts` directory.
|
||||||
|
pub fn load_fonts(&self, cx: &App) -> anyhow::Result<()> {
|
||||||
|
let font_paths = self.list("fonts")?;
|
||||||
|
let mut embedded_fonts = Vec::new();
|
||||||
|
for font_path in font_paths {
|
||||||
|
if font_path.ends_with(".ttf") {
|
||||||
|
let font_bytes = cx
|
||||||
|
.asset_source()
|
||||||
|
.load(&font_path)?
|
||||||
|
.expect("Assets should never return None");
|
||||||
|
embedded_fonts.push(font_bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.text_system().add_fonts(embedded_fonts)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_test_fonts(&self, cx: &App) {
|
||||||
|
cx.text_system()
|
||||||
|
.add_fonts(vec![self
|
||||||
|
.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap()])
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/common/Cargo.toml
Normal file
21
crates/common/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "common"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
gpui.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
dirs = "5.0"
|
||||||
|
nostr = { git = "https://github.com/rust-nostr/nostr" }
|
||||||
21
crates/common/src/constants.rs
Normal file
21
crates/common/src/constants.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
pub const CLIENT_NAME: &str = "Lume";
|
||||||
|
pub const APP_ID: &str = "su.reya.lume";
|
||||||
|
|
||||||
|
/// Bootstrap Relays.
|
||||||
|
pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
||||||
|
"wss://relay.damus.io",
|
||||||
|
"wss://relay.primal.net",
|
||||||
|
"wss://relay.nos.social",
|
||||||
|
"wss://user.kindpag.es",
|
||||||
|
"wss://purplepag.es",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Search Relays.
|
||||||
|
pub const SEARCH_RELAYS: [&str; 3] = [
|
||||||
|
"wss://relay.nostr.band",
|
||||||
|
"wss://search.nos.today",
|
||||||
|
"wss://relay.noswhere.com",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Default relay for Nostr Connect
|
||||||
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
5
crates/common/src/lib.rs
Normal file
5
crates/common/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub use constants::*;
|
||||||
|
pub use paths::*;
|
||||||
|
|
||||||
|
mod constants;
|
||||||
|
mod paths;
|
||||||
64
crates/common/src/paths.rs
Normal file
64
crates/common/src/paths.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
/// Returns the path to the user's home directory.
|
||||||
|
pub fn home_dir() -> &'static PathBuf {
|
||||||
|
static HOME_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the configuration directory used by Lume.
|
||||||
|
pub fn config_dir() -> &'static PathBuf {
|
||||||
|
static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
CONFIG_DIR.get_or_init(|| {
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return dirs::config_dir()
|
||||||
|
.expect("failed to determine RoamingAppData directory")
|
||||||
|
.join("Lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
return if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") {
|
||||||
|
flatpak_xdg_config.into()
|
||||||
|
} else {
|
||||||
|
dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory")
|
||||||
|
}
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
home_dir().join(".config").join("lume")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the support directory used by Lume.
|
||||||
|
pub fn support_dir() -> &'static PathBuf {
|
||||||
|
static SUPPORT_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
SUPPORT_DIR.get_or_init(|| {
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
return home_dir().join("Library/Application Support/Lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
return if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") {
|
||||||
|
flatpak_xdg_data.into()
|
||||||
|
} else {
|
||||||
|
dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory")
|
||||||
|
}
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg!(target_os = "windows") {
|
||||||
|
return dirs::data_local_dir()
|
||||||
|
.expect("failed to determine LocalAppData directory")
|
||||||
|
.join("lume");
|
||||||
|
}
|
||||||
|
|
||||||
|
config_dir().clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the `nostr` file.
|
||||||
|
pub fn nostr_file() -> &'static PathBuf {
|
||||||
|
static NOSTR_FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||||
|
NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db"))
|
||||||
|
}
|
||||||
@@ -9,7 +9,12 @@ name = "lume"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
assets = { path = "../assets" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
gpui-component.workspace = true
|
||||||
gpui_tokio.workspace = true
|
gpui_tokio.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
|
|
||||||
|
|||||||
34
crates/lume/src/actions.rs
Normal file
34
crates/lume/src/actions.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use gpui::{actions, App};
|
||||||
|
|
||||||
|
actions!(coop, [Quit]);
|
||||||
|
|
||||||
|
pub fn load_embedded_fonts(cx: &App) {
|
||||||
|
let asset_source = cx.asset_source();
|
||||||
|
let font_paths = asset_source.list("fonts").unwrap();
|
||||||
|
let embedded_fonts = Mutex::new(Vec::new());
|
||||||
|
let executor = cx.background_executor();
|
||||||
|
|
||||||
|
executor.block(executor.scoped(|scope| {
|
||||||
|
for font_path in &font_paths {
|
||||||
|
if !font_path.ends_with(".ttf") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.spawn(async {
|
||||||
|
let font_bytes = asset_source.load(font_path).unwrap().unwrap();
|
||||||
|
embedded_fonts.lock().unwrap().push(font_bytes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
cx.text_system()
|
||||||
|
.add_fonts(embedded_fonts.into_inner().unwrap())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quit(_: &Quit, cx: &mut App) {
|
||||||
|
log::info!("Gracefully quitting the application . . .");
|
||||||
|
cx.quit();
|
||||||
|
}
|
||||||
@@ -1,3 +1,102 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use assets::Assets;
|
||||||
|
use common::{APP_ID, CLIENT_NAME};
|
||||||
|
use gpui::{
|
||||||
|
div, point, px, size, AppContext, Application, Bounds, Context, IntoElement, KeyBinding, Menu,
|
||||||
|
MenuItem, ParentElement, Render, SharedString, Styled, TitlebarOptions, Window,
|
||||||
|
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||||
|
};
|
||||||
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
|
use gpui_component::{Root, StyledExt};
|
||||||
|
|
||||||
|
use crate::actions::{load_embedded_fonts, quit, Quit};
|
||||||
|
|
||||||
|
mod actions;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
// Initialize logging
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
// Initialize the Application
|
||||||
|
let app = Application::new()
|
||||||
|
.with_assets(Assets)
|
||||||
|
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||||
|
|
||||||
|
// Run application
|
||||||
|
app.run(move |cx| {
|
||||||
|
// Load embedded fonts in assets/fonts
|
||||||
|
load_embedded_fonts(cx);
|
||||||
|
|
||||||
|
// Register the `quit` function
|
||||||
|
cx.on_action(quit);
|
||||||
|
|
||||||
|
// Register the `quit` function with CMD+Q (macOS)
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||||
|
|
||||||
|
// Register the `quit` function with Super+Q (others)
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
cx.bind_keys([KeyBinding::new("super-q", Quit, None)]);
|
||||||
|
|
||||||
|
// Set menu items
|
||||||
|
cx.set_menus(vec![Menu {
|
||||||
|
name: "Lume".into(),
|
||||||
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Set up the window bounds
|
||||||
|
let bounds = Bounds::centered(None, size(px(920.0), px(700.0)), cx);
|
||||||
|
|
||||||
|
// Set up the window options
|
||||||
|
let opts = WindowOptions {
|
||||||
|
window_background: WindowBackgroundAppearance::Opaque,
|
||||||
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
|
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||||
|
kind: WindowKind::Normal,
|
||||||
|
app_id: Some(APP_ID.to_owned()),
|
||||||
|
titlebar: Some(TitlebarOptions {
|
||||||
|
title: Some(SharedString::new_static(CLIENT_NAME)),
|
||||||
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
|
appears_transparent: true,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open a window with default options
|
||||||
|
cx.open_window(opts, |window, cx| {
|
||||||
|
// Bring the app to the foreground
|
||||||
|
cx.activate(true);
|
||||||
|
|
||||||
|
// Initialize the tokio runtime
|
||||||
|
gpui_tokio::init(cx);
|
||||||
|
|
||||||
|
// Initialize components
|
||||||
|
gpui_component::init(cx);
|
||||||
|
|
||||||
|
let view = cx.new(|_| HelloWorld);
|
||||||
|
cx.new(|cx| Root::new(view, window, cx))
|
||||||
|
})
|
||||||
|
.expect("Failed to open window. Please restart the application.");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HelloWorld;
|
||||||
|
|
||||||
|
impl Render for HelloWorld {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child("Hello, World!")
|
||||||
|
.child(
|
||||||
|
Button::new("ok")
|
||||||
|
.primary()
|
||||||
|
.label("Let's Go!")
|
||||||
|
.on_click(|_, _, _| println!("Clicked!")),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
crates/state/Cargo.toml
Normal file
21
crates/state/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "state"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
nostr-lmdb.workspace = true
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
|
rustls = "0.23.23"
|
||||||
111
crates/state/src/lib.rs
Normal file
111
crates/state/src/lib.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
|
use nostr_lmdb::NostrLmdb;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||||
|
|
||||||
|
impl Global for GlobalNostrRegistry {}
|
||||||
|
|
||||||
|
/// Nostr Registry
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NostrRegistry {
|
||||||
|
/// Nostr Client
|
||||||
|
client: Client,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrRegistry {
|
||||||
|
/// Retrieve the global nostr state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalNostrRegistry>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global nostr instance
|
||||||
|
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalNostrRegistry(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new nostr instance
|
||||||
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
// rustls uses the `aws_lc_rs` provider by default
|
||||||
|
// This only errors if the default provider has already
|
||||||
|
// been installed. We can ignore this `Result`.
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider()
|
||||||
|
.install_default()
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// Construct the nostr client options
|
||||||
|
let opts = ClientOptions::new()
|
||||||
|
.automatic_authentication(false)
|
||||||
|
.verify_subscriptions(false)
|
||||||
|
.sleep_when_idle(SleepWhenIdle::Enabled {
|
||||||
|
timeout: Duration::from_secs(600),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the lmdb
|
||||||
|
let lmdb = cx.background_executor().block(async move {
|
||||||
|
let path = config_dir().join("nostr");
|
||||||
|
NostrLmdb::open(path)
|
||||||
|
.await
|
||||||
|
.expect("Failed to initialize database")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Construct the nostr client
|
||||||
|
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||||
|
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Establish connection to the bootstrap relays
|
||||||
|
//
|
||||||
|
// And handle notifications from the nostr relay pool channel
|
||||||
|
cx.background_spawn({
|
||||||
|
let client = client.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
// Connect to the bootstrap relays
|
||||||
|
Self::connect(&client).await;
|
||||||
|
|
||||||
|
// Handle notifications from the relay pool
|
||||||
|
// Self::handle_notifications(&client, &gossip, &tracker).await;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
client,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establish connection to the bootstrap relays
|
||||||
|
async fn connect(client: &Client) {
|
||||||
|
// Get all bootstrapping relays
|
||||||
|
let mut urls = vec![];
|
||||||
|
urls.extend(BOOTSTRAP_RELAYS);
|
||||||
|
urls.extend(SEARCH_RELAYS);
|
||||||
|
|
||||||
|
// Add relay to the relay pool
|
||||||
|
for url in urls.into_iter() {
|
||||||
|
client.add_relay(url).await.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to all added relays
|
||||||
|
client.connect().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the nostr client instance
|
||||||
|
pub fn client(&self) -> Client {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.88"
|
channel = "1.90"
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
targets = [
|
targets = [
|
||||||
|
|||||||
Reference in New Issue
Block a user