diff --git a/Cargo.lock b/Cargo.lock
index 29ad0b7..3d5f62d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2336,6 +2336,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+[[package]]
+name = "hashbrown"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
+
[[package]]
name = "hashbrown"
version = "0.15.4"
@@ -3064,6 +3070,22 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+[[package]]
+name = "lnsocket"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a88bd51e5bb3753f89b0d3e73baa565064c5a9f5b2aad3ab3f3db5fffb89955"
+dependencies = [
+ "bitcoin",
+ "hashbrown 0.13.2",
+ "hex",
+ "lightning-types",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "lock_api"
version = "0.4.13"
@@ -3540,6 +3562,7 @@ dependencies = [
"egui_tabs",
"nostrdb",
"notedeck",
+ "notedeck_clndash",
"notedeck_columns",
"notedeck_dave",
"notedeck_notebook",
@@ -3559,6 +3582,20 @@ dependencies = [
"tracing-subscriber",
]
+[[package]]
+name = "notedeck_clndash"
+version = "0.6.0"
+dependencies = [
+ "eframe",
+ "egui",
+ "lnsocket",
+ "notedeck",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tracing",
+]
+
[[package]]
name = "notedeck_columns"
version = "0.6.0"
diff --git a/Cargo.toml b/Cargo.toml
index a036cba..cb7abc8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,8 +8,9 @@ members = [
"crates/notedeck_dave",
"crates/notedeck_notebook",
"crates/notedeck_ui",
+ "crates/notedeck_clndash",
- "crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui",
+ "crates/enostr", "crates/tokenator", "crates/notedeck_dave", "crates/notedeck_ui", "crates/notedeck_clndash",
]
[workspace.dependencies]
@@ -48,6 +49,7 @@ nostrdb = { git = "https://github.com/damus-io/nostrdb-rs", rev = "2b2e5e43c019b
#nostrdb = "0.6.1"
notedeck = { path = "crates/notedeck" }
notedeck_chrome = { path = "crates/notedeck_chrome" }
+notedeck_clndash = { path = "crates/notedeck_clndash" }
notedeck_columns = { path = "crates/notedeck_columns" }
notedeck_dave = { path = "crates/notedeck_dave" }
notedeck_notebook = { path = "crates/notedeck_notebook" }
diff --git a/assets/icons/clnlogo.svg b/assets/icons/clnlogo.svg
new file mode 100644
index 0000000..ab4b18b
--- /dev/null
+++ b/assets/icons/clnlogo.svg
@@ -0,0 +1,57 @@
+
+
+
+
diff --git a/crates/notedeck/src/args.rs b/crates/notedeck/src/args.rs
index bd6caed..1f413d5 100644
--- a/crates/notedeck/src/args.rs
+++ b/crates/notedeck/src/args.rs
@@ -126,6 +126,8 @@ impl Args {
res.options.set(NotedeckOptions::RelayDebug, true);
} else if arg == "--notebook" {
res.options.set(NotedeckOptions::FeatureNotebook, true);
+ } else if arg == "--clndash" {
+ res.options.set(NotedeckOptions::FeatureClnDash, true);
} else {
unrecognized_args.insert(arg.clone());
}
diff --git a/crates/notedeck/src/options.rs b/crates/notedeck/src/options.rs
index 1c01644..e948745 100644
--- a/crates/notedeck/src/options.rs
+++ b/crates/notedeck/src/options.rs
@@ -26,6 +26,9 @@ bitflags! {
// ===== Feature Flags ======
/// Is notebook enabled?
const FeatureNotebook = 1 << 32;
+
+ /// Is clndash enabled?
+ const FeatureClnDash = 1 << 33;
}
}
diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml
index 78d9901..33a1063 100644
--- a/crates/notedeck_chrome/Cargo.toml
+++ b/crates/notedeck_chrome/Cargo.toml
@@ -18,6 +18,7 @@ notedeck_columns = { workspace = true }
notedeck_ui = { workspace = true }
notedeck_dave = { workspace = true }
notedeck_notebook = { workspace = true }
+notedeck_clndash = { workspace = true }
notedeck = { workspace = true }
nostrdb = { workspace = true }
puffin = { workspace = true, optional = true }
diff --git a/crates/notedeck_chrome/src/app.rs b/crates/notedeck_chrome/src/app.rs
index d672e6c..1ce9784 100644
--- a/crates/notedeck_chrome/src/app.rs
+++ b/crates/notedeck_chrome/src/app.rs
@@ -1,4 +1,5 @@
use notedeck::{AppAction, AppContext};
+use notedeck_clndash::ClnDash;
use notedeck_columns::Damus;
use notedeck_dave::Dave;
use notedeck_notebook::Notebook;
@@ -8,6 +9,7 @@ pub enum NotedeckApp {
Dave(Box),
Columns(Box),
Notebook(Box),
+ ClnDash(Box),
Other(Box),
}
@@ -17,6 +19,7 @@ impl notedeck::App for NotedeckApp {
NotedeckApp::Dave(dave) => dave.update(ctx, ui),
NotedeckApp::Columns(columns) => columns.update(ctx, ui),
NotedeckApp::Notebook(notebook) => notebook.update(ctx, ui),
+ NotedeckApp::ClnDash(clndash) => clndash.update(ctx, ui),
NotedeckApp::Other(other) => other.update(ctx, ui),
}
}
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
index c37e709..f22f707 100644
--- a/crates/notedeck_chrome/src/chrome.rs
+++ b/crates/notedeck_chrome/src/chrome.rs
@@ -18,7 +18,6 @@ use notedeck_columns::{
Damus,
};
use notedeck_dave::{Dave, DaveAvatar};
-use notedeck_notebook::Notebook;
use notedeck_ui::{app_images, AnimationHelper, ProfilePic};
use std::collections::HashMap;
@@ -198,6 +197,10 @@ impl Chrome {
chrome.add_app(NotedeckApp::Notebook(Box::default()));
}
+ if notedeck.has_option(NotedeckOptions::FeatureClnDash) {
+ chrome.add_app(NotedeckApp::ClnDash(Box::default()));
+ }
+
chrome.set_active(0);
Ok(chrome)
@@ -231,16 +234,6 @@ impl Chrome {
None
}
- fn get_notebook(&mut self) -> Option<&mut Notebook> {
- for app in &mut self.apps {
- if let NotedeckApp::Notebook(notebook) = app {
- return Some(notebook);
- }
- }
-
- None
- }
-
fn switch_to_dave(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Dave(_) = app {
@@ -249,14 +242,6 @@ impl Chrome {
}
}
- fn switch_to_notebook(&mut self) {
- for (i, app) in self.apps.iter().enumerate() {
- if let NotedeckApp::Notebook(_) = app {
- self.active = i as i32;
- }
- }
- }
-
fn switch_to_columns(&mut self) {
for (i, app) in self.apps.iter().enumerate() {
if let NotedeckApp::Columns(_) = app {
@@ -498,32 +483,32 @@ impl Chrome {
ui.add_space(4.0);
ui.add(milestone_name(i18n));
- ui.add_space(16.0);
//let dark_mode = ui.ctx().style().visuals.dark_mode;
- if columns_button(ui)
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .clicked()
- {
- self.active = 0;
- }
- ui.add_space(32.0);
- if let Some(dave) = self.get_dave() {
- let rect = dave_sidebar_rect(ui);
- let dave_resp = dave_button(dave.avatar_mut(), ui, rect)
- .on_hover_cursor(egui::CursorIcon::PointingHand);
- if dave_resp.clicked() {
- self.switch_to_dave();
- }
- }
- //ui.add_space(32.0);
+ for (i, app) in self.apps.iter_mut().enumerate() {
+ let r = match app {
+ NotedeckApp::Columns(_columns_app) => columns_button(ui),
- if let Some(_notebook) = self.get_notebook() {
- if notebook_button(ui)
- .on_hover_cursor(egui::CursorIcon::PointingHand)
- .clicked()
- {
- self.switch_to_notebook();
+ NotedeckApp::Dave(dave) => {
+ ui.add_space(24.0);
+ let rect = dave_sidebar_rect(ui);
+ dave_button(dave.avatar_mut(), ui, rect)
+ }
+
+ NotedeckApp::ClnDash(_clndash) => clndash_button(ui),
+
+ NotedeckApp::Notebook(_notebook) => notebook_button(ui),
+
+ NotedeckApp::Other(_other) => {
+ // app provides its own button rendering ui?
+ panic!("TODO: implement other apps")
+ }
+ };
+
+ ui.add_space(4.0);
+
+ if r.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
+ self.active = i as i32;
}
}
}
@@ -720,6 +705,17 @@ fn accounts_button(ui: &mut egui::Ui) -> egui::Response {
)
}
+fn clndash_button(ui: &mut egui::Ui) -> egui::Response {
+ expanding_button(
+ "clndash-button",
+ 24.0,
+ app_images::cln_image(),
+ app_images::cln_image(),
+ ui,
+ false,
+ )
+}
+
fn notebook_button(ui: &mut egui::Ui) -> egui::Response {
expanding_button(
"notebook-button",
diff --git a/crates/notedeck_clndash/Cargo.toml b/crates/notedeck_clndash/Cargo.toml
new file mode 100644
index 0000000..1cd481f
--- /dev/null
+++ b/crates/notedeck_clndash/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "notedeck_clndash"
+edition = "2024"
+version.workspace = true
+
+[dependencies]
+egui = { workspace = true }
+notedeck = { workspace = true }
+#notedeck_ui = { workspace = true }
+eframe = { workspace = true }
+lnsocket = "0.3.0"
+tracing = { workspace = true }
+serde_json = { workspace = true }
+tokio = { workspace = true }
+serde = { workspace = true }
diff --git a/crates/notedeck_clndash/src/lib.rs b/crates/notedeck_clndash/src/lib.rs
new file mode 100644
index 0000000..1a3b45c
--- /dev/null
+++ b/crates/notedeck_clndash/src/lib.rs
@@ -0,0 +1,195 @@
+use egui::{Color32, Label, RichText};
+use lnsocket::bitcoin::secp256k1::{PublicKey, SecretKey, rand};
+use lnsocket::{CommandoClient, LNSocket};
+use notedeck::{AppAction, AppContext};
+use serde_json::{Value, json};
+use std::str::FromStr;
+use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
+
+#[derive(Default)]
+pub struct ClnDash {
+ initialized: bool,
+ connection_state: ConnectionState,
+ get_info: Option,
+ channel: Option,
+}
+
+impl Default for ConnectionState {
+ fn default() -> Self {
+ ConnectionState::Dead("uninitialized".to_string())
+ }
+}
+
+struct Channel {
+ req_tx: UnboundedSender,
+ event_rx: UnboundedReceiver,
+}
+
+/// Responses from the socket
+enum ClnResponse {
+ GetInfo(Result),
+}
+
+enum ConnectionState {
+ Dead(String),
+ Connecting,
+ Active,
+}
+
+enum Request {
+ GetInfo,
+}
+
+enum Event {
+ /// We lost the socket somehow
+ Ended {
+ reason: String,
+ },
+
+ Connected,
+
+ Response(ClnResponse),
+}
+
+impl notedeck::App for ClnDash {
+ fn update(&mut self, _ctx: &mut AppContext<'_>, ui: &mut egui::Ui) -> Option {
+ if !self.initialized {
+ self.connection_state = ConnectionState::Connecting;
+ self.setup_connection();
+ self.initialized = true;
+ }
+
+ self.process_events();
+
+ self.show(ui);
+
+ None
+ }
+}
+
+fn connection_state_ui(ui: &mut egui::Ui, state: &ConnectionState) {
+ match state {
+ ConnectionState::Active => {
+ ui.add(Label::new(RichText::new("Connected").color(Color32::GREEN)));
+ }
+
+ ConnectionState::Connecting => {
+ ui.add(Label::new(
+ RichText::new("Connecting").color(Color32::YELLOW),
+ ));
+ }
+
+ ConnectionState::Dead(reason) => {
+ ui.add(Label::new(
+ RichText::new(format!("Disconnected: {reason}")).color(Color32::RED),
+ ));
+ }
+ }
+}
+
+impl ClnDash {
+ fn show(&mut self, ui: &mut egui::Ui) {
+ egui::Frame::new()
+ .inner_margin(egui::Margin::same(50))
+ .show(ui, |ui| {
+ connection_state_ui(ui, &self.connection_state);
+
+ if let Some(info) = self.get_info.as_ref() {
+ get_info_ui(ui, info);
+ }
+ });
+ }
+
+ fn setup_connection(&mut self) {
+ let (req_tx, mut req_rx) = unbounded_channel::();
+ let (event_tx, event_rx) = unbounded_channel::();
+ self.channel = Some(Channel { req_tx, event_rx });
+
+ tokio::spawn(async move {
+ let key = SecretKey::new(&mut rand::thread_rng());
+ let their_pubkey = PublicKey::from_str(
+ "03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71",
+ )
+ .unwrap();
+
+ let lnsocket =
+ match LNSocket::connect_and_init(key, their_pubkey, "ln.damus.io:9735").await {
+ Err(err) => {
+ let _ = event_tx.send(Event::Ended {
+ reason: err.to_string(),
+ });
+ return;
+ }
+
+ Ok(lnsocket) => {
+ let _ = event_tx.send(Event::Connected);
+ lnsocket
+ }
+ };
+
+ let rune = "Vns1Zxvidr4J8pP2ZCg3Wjp2SyGyyf5RHgvFG8L36yM9MzMmbWV0aG9kPWdldGluZm8="; // getinfo only atm
+ let commando = CommandoClient::spawn(lnsocket, rune);
+
+ loop {
+ match req_rx.recv().await {
+ None => {
+ let _ = event_tx.send(Event::Ended {
+ reason: "channel dead?".to_string(),
+ });
+ break;
+ }
+
+ Some(req) => match req {
+ Request::GetInfo => match commando.call("getinfo", json!({})).await {
+ Ok(v) => {
+ let _ = event_tx.send(Event::Response(ClnResponse::GetInfo(Ok(v))));
+ }
+ Err(err) => {
+ let _ = event_tx.send(Event::Ended {
+ reason: err.to_string(),
+ });
+ }
+ },
+ },
+ }
+ }
+ });
+ }
+
+ fn process_events(&mut self) {
+ let Some(channel) = &mut self.channel else {
+ return;
+ };
+
+ while let Ok(event) = channel.event_rx.try_recv() {
+ match event {
+ Event::Ended { reason } => {
+ self.connection_state = ConnectionState::Dead(reason);
+ }
+
+ Event::Connected => {
+ self.connection_state = ConnectionState::Active;
+ let _ = channel.req_tx.send(Request::GetInfo);
+ }
+
+ Event::Response(resp) => match resp {
+ ClnResponse::GetInfo(value) => {
+ let Ok(value) = value else {
+ return;
+ };
+
+ if let Ok(s) = serde_json::to_string_pretty(&value) {
+ self.get_info = Some(s);
+ }
+ }
+ },
+ }
+ }
+ }
+}
+
+fn get_info_ui(ui: &mut egui::Ui, info: &str) {
+ ui.horizontal_wrapped(|ui| {
+ ui.add(Label::new(info).wrap_mode(egui::TextWrapMode::Wrap));
+ });
+}
diff --git a/crates/notedeck_ui/src/app_images.rs b/crates/notedeck_ui/src/app_images.rs
index 1306bd2..ba2c396 100644
--- a/crates/notedeck_ui/src/app_images.rs
+++ b/crates/notedeck_ui/src/app_images.rs
@@ -15,6 +15,10 @@ pub fn accounts_image() -> Image<'static> {
Image::new(include_image!("../../../assets/icons/accounts.png"))
}
+pub fn cln_image() -> Image<'static> {
+ Image::new(include_image!("../../../assets/icons/clnlogo.svg"))
+}
+
pub fn add_column_dark_image() -> Image<'static> {
Image::new(include_image!(
"../../../assets/icons/add_column_dark_4x.png"