diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index ac3cdcca70..f0f9ab847b 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -208,6 +208,10 @@ path = "examples/learn/transition.rs" name = "blur" path = "examples/learn/blur.rs" +[[example]] +name = "keyring" +path = "examples/learn/keyring.rs" + # ============================================================================ # Bench Examples - Performance benchmarks # ============================================================================ diff --git a/crates/gpui/examples/learn/keyring.rs b/crates/gpui/examples/learn/keyring.rs new file mode 100644 index 0000000000..b2121a41b4 --- /dev/null +++ b/crates/gpui/examples/learn/keyring.rs @@ -0,0 +1,176 @@ +//! Keyring Example +//! +//! This example demonstrates the platform credentials API: +//! +//! 1. `cx.write_credentials` - store a username/password in the system keyring +//! 2. `cx.read_credentials` - read them back +//! 3. `cx.delete_credentials` - remove them +//! +//! On Linux/FreeBSD, every stored item is tagged with a keyring *label*. It +//! defaults to `"gpui-ce"`, but consumers can override it with +//! `cx.set_keyring_label(..)` so the items show up under their own app's name. + +#[path = "../shared/prelude.rs"] +mod example_prelude; + +use gpui::colors::Colors; +use gpui::{ + App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, prelude::*, px, size, +}; + +/// The URL the credentials are keyed by. The keyring label (Linux/FreeBSD) is a +/// separate, app-wide identifier set via `cx.set_keyring_label`. +const CREDENTIAL_URL: &str = "https://example.com/keyring-demo"; + +struct KeyringExample { + status: String, +} + +impl KeyringExample { + fn new() -> Self { + Self { + status: "Use the buttons to store, load, and delete credentials.".into(), + } + } + + fn set_status(&mut self, status: impl Into, cx: &mut Context) { + self.status = status.into(); + cx.notify(); + } + + fn save(&mut self, cx: &mut Context) { + self.set_status("Saving...", cx); + cx.spawn(async move |this, cx| { + let task = + cx.update(|cx| cx.write_credentials(CREDENTIAL_URL, "ada@example.com", b"hunter2")); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(()) => this.set_status("Saved credentials for ada@example.com.", cx), + Err(err) => this.set_status(format!("Failed to save: {err}"), cx), + }) + }) + .detach(); + } + + fn load(&mut self, cx: &mut Context) { + self.set_status("Loading...", cx); + cx.spawn(async move |this, cx| { + let task = cx.update(|cx| cx.read_credentials(CREDENTIAL_URL)); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(Some((username, password))) => this.set_status( + format!("Loaded {username} (password is {} bytes).", password.len()), + cx, + ), + Ok(None) => this.set_status("No credentials stored yet.", cx), + Err(err) => this.set_status(format!("Failed to load: {err}"), cx), + }) + }) + .detach(); + } + + fn delete(&mut self, cx: &mut Context) { + self.set_status("Deleting...", cx); + cx.spawn(async move |this, cx| { + let task = cx.update(|cx| cx.delete_credentials(CREDENTIAL_URL)); + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(()) => this.set_status("Deleted stored credentials.", cx), + Err(err) => this.set_status(format!("Failed to delete: {err}"), cx), + }) + }) + .detach(); + } +} + +impl Render for KeyringExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div().size_full().p_6().bg(colors.background).child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(460.)) + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Keyring Credentials"), + ) + .child( + div() + .text_sm() + .text_color(colors.disabled) + .child(format!("Stored under: {CREDENTIAL_URL}")), + ) + .child( + div() + .p_4() + .rounded_lg() + .bg(colors.container) + .border_1() + .border_color(colors.border) + .text_sm() + .text_color(colors.text) + .child(self.status.clone()), + ) + .child( + div() + .flex() + .gap_2() + .child( + button(&colors, "save", "Save") + .on_click(cx.listener(|this, _, _, cx| this.save(cx))), + ) + .child( + button(&colors, "load", "Load") + .on_click(cx.listener(|this, _, _, cx| this.load(cx))), + ) + .child( + button(&colors, "delete", "Delete") + .on_click(cx.listener(|this, _, _, cx| this.delete(cx))), + ), + ), + ) + } +} + +fn button( + colors: &Colors, + id: impl Into, + label: &'static str, +) -> gpui::Stateful { + let bg_hover = colors.border; + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(colors.selected_text) + .bg(colors.selected) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .child(label) +} + +fn main() { + gpui_platform::application().run(|cx: &mut App| { + cx.set_keyring_label("gpui-ce-keyring-example"); + + let bounds = Bounds::centered(None, size(px(500.), px(360.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| KeyringExample::new()), + ) + .expect("Failed to open window"); + + example_prelude::init_example(cx, "Keyring"); + }); +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9f20035681..ae34b3617a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1129,6 +1129,12 @@ impl App { self.platform.set_gpu_requirements(requirements); } + /// Sets the label applied to credentials stored in the system keyring. + /// Call before writing credentials. Only Linux/FreeBSD apply the label. + pub fn set_keyring_label(&self, label: impl Into) { + self.platform.set_keyring_label(label.into()); + } + /// Returns a handle to the window that is currently focused at the platform level, if one exists. pub fn active_window(&self) -> Option { self.platform.active_window() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1504b6e114..bb91f91abb 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -248,6 +248,10 @@ pub trait Platform: 'static { /// `gpui_wgpu::WgpuDeviceRequirements`. #[cfg(any(target_os = "linux", target_os = "freebsd"))] fn set_gpu_requirements(&self, _requirements: Box) {} + + /// Sets the label applied to credentials stored in the system keyring. + /// Only Linux/FreeBSD use this label. + fn set_keyring_label(&self, _label: SharedString) {} } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index ce412568fe..5c8f21c7ac 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -26,7 +26,7 @@ use gpui::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, RunnableVariant, Task, ThermalState, WindowAppearance, + PlatformWindow, Result, RunnableVariant, SharedString, Task, ThermalState, WindowAppearance, WindowButtonLayout, WindowParams, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -41,7 +41,7 @@ pub(crate) const SCROLL_LINES: f32 = 3.0; pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400); #[cfg(any(feature = "wayland", feature = "x11"))] pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0); -pub(crate) const KEYRING_LABEL: &str = "zed-github-account"; +pub(crate) const KEYRING_LABEL: &str = "gpui-ce"; #[cfg(any(feature = "wayland", feature = "x11"))] const FILE_PICKER_PORTAL_MISSING: &str = @@ -124,6 +124,7 @@ pub(crate) struct LinuxCommon { pub(crate) callbacks: PlatformHandlers, pub(crate) signal: LoopSignal, pub(crate) menus: Vec, + pub(crate) keyring_label: SharedString, } impl LinuxCommon { @@ -151,6 +152,7 @@ impl LinuxCommon { callbacks, signal, menus: Vec::new(), + keyring_label: KEYRING_LABEL.into(), }; (common, main_receiver) @@ -321,6 +323,11 @@ impl Platform for LinuxPlatform

{ self.inner.set_gpu_requirements(requirements); } + fn set_keyring_label(&self, label: SharedString) { + self.inner + .with_common(|common| common.keyring_label = label); + } + fn open_url(&self, url: &str) { self.inner.open_uri(url); } @@ -555,12 +562,15 @@ impl Platform for LinuxPlatform

{ let url = url.to_string(); let username = username.to_string(); let password = password.to_vec(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); self.background_executor().spawn(async move { let keyring = oo7::Keyring::new().await?; keyring.unlock().await?; keyring .create_item( - KEYRING_LABEL, + &label, &vec![("url", &url), ("username", &username)], password, true, @@ -572,6 +582,9 @@ impl Platform for LinuxPlatform

{ fn read_credentials(&self, url: &str) -> Task)>>> { let url = url.to_string(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); self.background_executor().spawn(async move { let keyring = oo7::Keyring::new().await?; keyring.unlock().await?; @@ -579,7 +592,7 @@ impl Platform for LinuxPlatform

{ let items = keyring.search_items(&vec![("url", &url)]).await?; for item in items.into_iter() { - if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + if item.label().await.is_ok_and(|l| l == label.as_ref()) { let attributes = item.attributes().await?; let username = attributes .get("username") @@ -600,6 +613,9 @@ impl Platform for LinuxPlatform

{ fn delete_credentials(&self, url: &str) -> Task> { let url = url.to_string(); + let label = self + .inner + .with_common(|common| common.keyring_label.clone()); self.background_executor().spawn(async move { let keyring = oo7::Keyring::new().await?; keyring.unlock().await?; @@ -607,7 +623,7 @@ impl Platform for LinuxPlatform

{ let items = keyring.search_items(&vec![("url", &url)]).await?; for item in items.into_iter() { - if item.label().await.is_ok_and(|label| label == KEYRING_LABEL) { + if item.label().await.is_ok_and(|l| l == label.as_ref()) { item.delete().await?; return Ok(()); }