Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions crates/deckard-app/assets/fonts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Bundled fonts (offline-first — no web-font CDN)

DESIGN.md mandates two bundled families. They are **not committed** here because
they are licensed binaries a human must place; the build stays green without them
(the theme sets the family names and GPUI silently falls back to the system font
until the files exist).

Drop these files into this directory, then **uncomment the `add_fonts(...)` block
in `crates/deckard-app/src/main.rs`** (search `TODO(fonts)`):

| File | Family / weight | Source | License |
|------|-----------------|--------|---------|
| `GeneralSans-Regular.otf` | General Sans 400 | https://www.fontshare.com/fonts/general-sans | Fontshare (free) |
| `GeneralSans-Medium.otf` | General Sans 500 | same | same |
| `GeneralSans-Semibold.otf` | General Sans 600 | same | same |
| `JetBrainsMono-Regular.ttf`| JetBrains Mono 400 | https://www.jetbrains.com/lega/font / GitHub `JetBrains/JetBrainsMono` | OFL 1.1 |
| `JetBrainsMono-Medium.ttf` | JetBrains Mono 500 | same | OFL 1.1 |

Notes:
- DESIGN caps weight at **600** — do not bundle Bold (700+).
- The family-name strings in `theme.rs` (`"General Sans"` / `"JetBrains Mono"`)
must match the font files' internal name table, or GPUI silently falls back.
Verify by launching the app and confirming money renders in mono after dropping
the files in.
48 changes: 48 additions & 0 deletions crates/deckard-app/src/capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! macOS screen-capture block — `NSWindow.sharingType = .none` (DESIGN §Trust,
//! deckard-demo-ux-locked). Opt-in and **default OFF**: tied to the privacy mask,
//! it removes the app's windows from screen recordings / screenshots so a masked
//! balance can't be captured anyway. For a demo *recording* you leave it off (or the
//! recording itself goes blank), which is exactly why the default is OFF.
//!
//! ## Why this reuses the tray feature's objc2 dep (no new dependency)
//!
//! The native call lives behind `#[cfg(all(target_os = "macos", feature = "tray"))]`
//! so it compiles against the SAME `objc2` / `objc2-app-kit` crates the tray icon
//! already pulls — no manifest churn, no `raw-window-handle`. We reach the window
//! through `NSApplication.windows` (the app owns exactly one window) rather than
//! bridging GPUI's `Window` to a raw handle, mirroring `tray.rs`'s
//! `NSApplication::sharedApplication` activation-policy call. Every other build
//! (no `tray`, or non-macOS) gets the inert no-op twin below.

/// Apply (or clear) the capture block to all of the app's native windows.
///
/// `on == true` → `NSWindowSharingType::None` (content cannot be captured by other
/// processes); `on == false` → `NSWindowSharingType::ReadOnly` (the system default,
/// capturable). Must run on the main thread — the caller invokes it from `render`,
/// which is already on GPUI's main UI thread.
#[cfg(all(target_os = "macos", feature = "tray"))]
pub fn apply_capture_block(on: bool) {
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSWindowSharingType};

// Off the main thread we have no AppKit access; bail rather than risk UB. (render
// always runs on the main thread, so this is just a guard, never hit in practice.)
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let app = NSApplication::sharedApplication(mtm);
let sharing = if on {
NSWindowSharingType::None
} else {
NSWindowSharingType::ReadOnly
};
for window in app.windows().iter() {
window.setSharingType(sharing);
}
}

/// No-op on every build without the macOS `tray` feature (Linux/Windows have no
/// `NSWindow`; a non-`tray` macOS build doesn't link AppKit). The setting still
/// persists and the toggle still renders — it just has no OS effect here.
#[cfg(not(all(target_os = "macos", feature = "tray")))]
pub fn apply_capture_block(_on: bool) {}
40 changes: 34 additions & 6 deletions crates/deckard-app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
//! Fork checklist: rename the crate in `Cargo.toml`, change `APP_NAME` and the
//! bundle identifier, swap `assets/icon.png`, then start editing the views.

mod capture;
mod money;
mod onboarding;
mod palette;
mod receive;
mod settings;
mod settings_view;
mod shell;
mod shell_chrome;
mod shield_view;
mod signer;
mod theme;
#[cfg(feature = "tray")]
Expand Down Expand Up @@ -43,7 +47,8 @@ gpui::actions!(
ToggleTheme,
NewItem,
GoBack,
TogglePalette
TogglePalette,
ToggleMask
]
);

Expand All @@ -58,11 +63,33 @@ fn main() {
// 1. Bring up gpui-component (themes, fonts, icon assets, input system).
gpui_component::init(cx);

// 1b. Register the bundled offline fonts (no web-font CDN). The theme
// sets the family names ("General Sans" / "JetBrains Mono"); GPUI
// silently falls back to the system font until the files exist, so
// the family-name config alone is safe to ship now.
//
// TODO(fonts): a human must drop the licensed font files into
// crates/deckard-app/assets/fonts/ (see that dir's README.md), then
// uncomment the block below to embed + register them. Do NOT
// uncomment before the files exist — `include_bytes!` of a missing
// path is a compile error.
//
// use std::borrow::Cow;
// cx.text_system()
// .add_fonts(vec![
// Cow::Borrowed(include_bytes!("../assets/fonts/GeneralSans-Regular.otf").as_slice()),
// Cow::Borrowed(include_bytes!("../assets/fonts/GeneralSans-Medium.otf").as_slice()),
// Cow::Borrowed(include_bytes!("../assets/fonts/GeneralSans-Semibold.otf").as_slice()),
// Cow::Borrowed(include_bytes!("../assets/fonts/JetBrainsMono-Regular.ttf").as_slice()),
// Cow::Borrowed(include_bytes!("../assets/fonts/JetBrainsMono-Medium.ttf").as_slice()),
// ])
// // reason: bundled fonts are a build-time invariant; a failure here
// // is a packaging bug we want to surface loudly at startup.
// .expect("bundled fonts failed to register");

// 2. Load persisted preferences and install the refined theme from them.
let settings = Settings::load();
#[cfg(feature = "tray")]
let accent = settings.accent;
theme::install(cx, settings.accent, settings.theme_mode.to_gpui());
theme::install(cx, settings.theme_mode.to_gpui());

// 3. Keyboard shortcuts. `secondary` = ⌘ on macOS, Ctrl on Linux /
// Windows — so these are portable. Context `None` = global.
Expand All @@ -73,6 +100,7 @@ fn main() {
KeyBinding::new("secondary-shift-d", ToggleTheme, None),
KeyBinding::new("secondary-[", GoBack, None),
KeyBinding::new("secondary-k", TogglePalette, None),
KeyBinding::new("secondary-shift-m", ToggleMask, None),
]);

// 4. Global action handlers. View-local actions (NewItem, OpenSettings,
Expand Down Expand Up @@ -138,9 +166,9 @@ fn main() {
.expect("failed to open window");

// Optional: native menu-bar tray icon + dock hiding (`--features tray`).
// The tray icon uses the saved accent and restyles live when changed.
// The tray icon uses a fixed brand color.
#[cfg(feature = "tray")]
tray::install(cx, accent);
tray::install(cx);

cx.activate(true);
});
Expand Down
123 changes: 123 additions & 0 deletions crates/deckard-app/src/money.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//! Money — mono-for-money rendering (DESIGN.md §Typography).
//!
//! Every figure that is money / a balance renders in **JetBrains Mono**, tabular,
//! full precision. The fractional part **and** the ticker are dimmed *by color
//! only* (`text.muted`) — never by a size step, which would produce the
//! superscript look DESIGN rejects. The integer carries `text.primary`.
//!
//! Two entry points wrap the single canonical formatter
//! `deckard_core::format_amount(raw, decimals, max_frac)`:
//! - [`money`] — an asset amount with an optional trailing ticker (`1,934.5 ETH`).
//! - [`usd`] — a USD figure carrying the `$` prefix; zero renders `$0`, never
//! `$0.0` (the `$` discipline + the zero rule).

use gpui::{div, prelude::FluentBuilder, Hsla, IntoElement, ParentElement, SharedString, Styled};
use gpui_component::h_flex;

use deckard_core::U256;

/// The fixed-length privacy mask: **always six bullets**, never `real.len()`, so a
/// masked figure leaks neither its value nor its digit count (the magnitude-safe rule,
/// per MetaMask's `SensitiveText`). One glyph for every money surface.
pub const MASK_BULLETS: &str = "••••••";

/// String-level mask for callers that render a plain balance string rather than the
/// `money()` spans (e.g. the sidebar wallet balance). Fixed six bullets when masked.
pub fn mask_money(masked: bool, real: &str) -> String {
if masked {
MASK_BULLETS.to_string()
} else {
real.to_string()
}
}

/// Render an asset amount as mono spans: integer in `primary`, decimals + ticker
/// dimmed to `dim` by color only. `unit` is the trailing ticker (e.g. `"ETH"`),
/// or `None` for a bare number. `mono` is `cx.theme().mono_font_family`. When
/// `masked`, renders the fixed [`MASK_BULLETS`] in `dim` instead (no value, no unit).
#[allow(clippy::too_many_arguments)]
pub fn money(
raw: U256,
decimals: u8,
max_frac: usize,
unit: Option<&str>,
masked: bool,
mono: SharedString,
primary: Hsla,
dim: Hsla,
) -> impl IntoElement {
if masked {
// Magnitude-safe: a single dimmed bullet span, no decimals, no ticker.
return spans(
MASK_BULLETS.to_string(),
String::new(),
None,
mono,
dim,
dim,
);
}
let s = deckard_core::format_amount(raw, decimals, max_frac);
let (int_part, frac) = split_amount(&s);
spans(int_part, frac, unit, mono, primary, dim)
}

/// Render a USD figure with the `$` prefix. Integer (incl. `$`) in `primary`,
/// decimals dimmed to `dim`. Zero renders `"$0"` (no `.00`, no `$0.0k`).
// reason: consumed by the Wave-2 shielded-balance / fiat view (Total + Private/
// Public lines carry `$`); kept now as the companion to `money`.
#[allow(dead_code, clippy::too_many_arguments)]
pub fn usd(
raw: U256,
decimals: u8,
max_frac: usize,
masked: bool,
mono: SharedString,
primary: Hsla,
dim: Hsla,
) -> impl IntoElement {
if masked {
return spans(
MASK_BULLETS.to_string(),
String::new(),
None,
mono,
dim,
dim,
);
}
let s = deckard_core::format_amount(raw, decimals, max_frac);
let (int_part, frac) = split_amount(&s);
spans(format!("${int_part}"), frac, None, mono, primary, dim)
}

/// Split a formatted amount (`"1,934.5"`) into its integer and (possibly empty)
/// fractional parts. `format_amount` already strips trailing zeros, so the frac
/// is absent for whole numbers (zero renders `"0"` → `("0", "")`).
fn split_amount(s: &str) -> (String, String) {
match s.split_once('.') {
Some((int_part, frac)) => (int_part.to_string(), frac.to_string()),
None => (s.to_string(), String::new()),
}
}

/// The shared row: a baseline-aligned mono flex with up to three colored spans
/// (integer · `.decimals` · ` ticker`). Dimming is color-only, size held flat.
fn spans(
int_part: impl Into<String>,
frac: String,
unit: Option<&str>,
mono: SharedString,
primary: Hsla,
dim: Hsla,
) -> impl IntoElement {
let unit = unit.map(|u| format!(" {u}"));
h_flex()
.items_baseline()
.font_family(mono)
.child(div().text_color(primary).child(int_part.into()))
.when(!frac.is_empty(), |e| {
e.child(div().text_color(dim).child(format!(".{frac}")))
})
.when_some(unit, |e, u| e.child(div().text_color(dim).child(u)))
}
2 changes: 1 addition & 1 deletion crates/deckard-app/src/onboarding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ impl Shell {
.child(
div()
.text_2xl()
.font_weight(FontWeight::BOLD)
.font_weight(FontWeight::SEMIBOLD)
.text_color(theme.foreground)
.child(title.to_string()),
)
Expand Down
47 changes: 43 additions & 4 deletions crates/deckard-app/src/palette.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use gpui::{
};
use gpui_component::{v_flex, ActiveTheme};

use crate::shell::{Route, Shell};
use crate::shell::{Selection, Shell, Surface};

impl Shell {
pub fn render_palette(&self, cx: &mut Context<Self>) -> impl IntoElement {
Expand Down Expand Up @@ -64,20 +64,29 @@ impl Shell {
row("cmd-portfolio", "Go to Portfolio", "").on_click(cx.listener(
|this, _, _, cx| {
this.palette_open = false;
this.navigate(Route::Welcome, cx);
this.select(Selection::Wallet, cx);
this.open(Surface::Home, cx);
},
)),
)
.child(row("cmd-receive", "Receive", "").on_click(cx.listener(
|this, _, _, cx| {
this.palette_open = false;
this.navigate(Route::Receive, cx);
this.open(Surface::Receive, cx);
},
)))
.child(
row("cmd-shield", "Shield to private", "").on_click(cx.listener(
|this, _, _, cx| {
this.palette_open = false;
this.open_shield(cx);
},
)),
)
.child(row("cmd-settings", "Settings", "⌘,").on_click(cx.listener(
|this, _, _, cx| {
this.palette_open = false;
this.navigate(Route::Settings, cx);
this.open(Surface::Settings, cx);
},
)))
.child(row("cmd-copy", "Copy address", "").on_click(cx.listener(
Expand All @@ -97,6 +106,36 @@ impl Shell {
},
)),
)
.child(
row(
"cmd-mask",
if self.mask {
"Show balances"
} else {
"Mask balances"
},
"⌘⇧M",
)
.on_click(cx.listener(|this, _, _, cx| {
this.palette_open = false;
this.toggle_mask(cx);
})),
)
.child(
row(
"cmd-agent-acting",
if self.agent_acting {
"Stop agent activity (demo)"
} else {
"Simulate agent activity (demo)"
},
"",
)
.on_click(cx.listener(|this, _, _, cx| {
this.palette_open = false;
this.toggle_agent_acting(cx);
})),
)
.child(row("cmd-lock", "Lock wallet", "").on_click(cx.listener(
|this, _, _, cx| {
this.palette_open = false;
Expand Down
Loading
Loading