restructure signed_app into flat modules and bump gpui-native-kit
Rust / build (macos-latest, stable) (push) Waiting to run
Rust / build (ubuntu-latest, stable) (push) Waiting to run
Rust / build (windows-latest, stable) (push) Waiting to run
Rust / build (macos-latest, stable) (pull_request) Waiting to run
Rust / build (ubuntu-latest, stable) (pull_request) Waiting to run
Rust / build (windows-latest, stable) (pull_request) Waiting to run

This commit is contained in:
2026-11-12 07:53:23 +07:00
parent 78b7cac4c8
commit 530be762ff
44 changed files with 1132 additions and 1078 deletions
Generated
+8 -8
View File
@@ -3059,7 +3059,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-core" name = "gpui-native-core"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"cpal", "cpal",
"flume 0.12.0", "flume 0.12.0",
@@ -3074,7 +3074,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-data" name = "gpui-native-data"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"gpui", "gpui",
"gpui-native-core", "gpui-native-core",
@@ -3086,7 +3086,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-editor" name = "gpui-native-editor"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"gpui", "gpui",
"gpui-native-core", "gpui-native-core",
@@ -3105,7 +3105,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-icons" name = "gpui-native-icons"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -3114,7 +3114,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-kit" name = "gpui-native-kit"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"gpui", "gpui",
"gpui-native-core", "gpui-native-core",
@@ -3128,7 +3128,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-layout" name = "gpui-native-layout"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"gpui", "gpui",
"gpui-native-core", "gpui-native-core",
@@ -3141,7 +3141,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-theme" name = "gpui-native-theme"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-channel", "async-channel",
@@ -3162,7 +3162,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui-native-utils" name = "gpui-native-utils"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.reya.info/reya/gpui-native-kit?rev=97106b7a3a54ec929c47feb70cdab4bed044d20e#97106b7a3a54ec929c47feb70cdab4bed044d20e" source = "git+https://git.reya.info/reya/gpui-native-kit?rev=72118bdd6c6c6a90f17f92f767c6ad342c1516b8#72118bdd6c6c6a90f17f92f767c6ad342c1516b8"
dependencies = [ dependencies = [
"gpui", "gpui",
"gpui-native-theme", "gpui-native-theme",
+1 -1
View File
@@ -14,7 +14,7 @@ gpui = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503de
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc", features = ["font-kit"] } gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc", features = ["font-kit"] }
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc" } reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "f8cfc7ed9e57503def9479138894f3139f5400cc" }
gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "97106b7a3a54ec929c47feb70cdab4bed044d20e", features = ["layout", "editor"] } gpui-native-kit = { git = "https://git.reya.info/reya/gpui-native-kit", rev = "72118bdd6c6c6a90f17f92f767c6ad342c1516b8", features = ["layout", "editor"] }
settings = { path = "crates/settings" } settings = { path = "crates/settings" }
signed_app = { path = "crates/signed_app" } signed_app = { path = "crates/signed_app" }
+1
View File
@@ -96,3 +96,4 @@ impl IconNamed for CustomIconName {
.into() .into()
} }
} }
+12
View File
@@ -0,0 +1,12 @@
use gpui::App;
use gpui_native_kit::theme::{ThemeVariant, follow_system, set_mode_override};
use settings::{AppearanceMode, Settings};
/// Applies the persisted light/dark/system preference to the kit theme.
pub fn apply_appearance(settings: &Settings, cx: &mut App) {
match settings.appearance {
AppearanceMode::System => follow_system(cx),
AppearanceMode::Light => set_mode_override(Some(ThemeVariant::Light), cx),
AppearanceMode::Dark => set_mode_override(Some(ThemeVariant::Dark), cx),
}
}
@@ -1,18 +1,23 @@
pub mod helpers;
mod placeholder;
use gpui::{ use gpui::{
App, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
Subscription, Window, div, Subscription, Window, div,
}; };
use gpui_native_kit::icons::IconRole; use gpui_native_kit::icons::IconRole;
use gpui_native_kit::layout::dock::{Dock, DockState}; use gpui_native_kit::layout::dock::{Dock, DockState};
use gpui_native_kit::layout::shell::{LayoutState, Shell, SidebarToggles}; use gpui_native_kit::layout::shell::{LayoutState, Shell, SidebarToggles};
use gpui_native_kit::theme::{ActiveTheme as _, ThemeVariant, follow_system, set_mode_override}; use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::toast::Toast; use gpui_native_kit::toast::Toast;
use settings::{AppearanceMode, Settings, SettingsStore}; use helpers::apply_appearance;
use settings::SettingsStore;
use signed_state::{Backend, BackendEvent}; use signed_state::{Backend, BackendEvent};
use crate::dialogs::{self, DialogHost}; use crate::app::placeholder::PlaceholderView;
use crate::views::placeholder::PlaceholderView; use crate::identity;
use crate::views::sidebar::SidebarView; use crate::shared::dialogs::DialogHost;
use crate::sidebar::SidebarView;
pub struct AppRoot { pub struct AppRoot {
layout: Entity<LayoutState>, layout: Entity<LayoutState>,
@@ -53,7 +58,7 @@ impl AppRoot {
subscriptions.push( subscriptions.push(
cx.subscribe(&backend, |this, _backend, event, cx| match event { cx.subscribe(&backend, |this, _backend, event, cx| match event {
BackendEvent::PassphraseRequired => dialogs::passphrase::open(cx), BackendEvent::PassphraseRequired => identity::helpers::open_passphrase(cx),
BackendEvent::Error(message) => { BackendEvent::Error(message) => {
this.layout.update(cx, |layout, cx| { this.layout.update(cx, |layout, cx| {
layout.push_toast(Toast::new(message.clone()).icon(IconRole::Warning), cx); layout.push_toast(Toast::new(message.clone()).icon(IconRole::Warning), cx);
@@ -70,7 +75,7 @@ impl AppRoot {
window.defer(cx, move |_window, cx| { window.defer(cx, move |_window, cx| {
if backend.read(cx).passphrase_required() { if backend.read(cx).passphrase_required() {
dialogs::passphrase::open(cx); identity::helpers::open_passphrase(cx);
} }
}); });
@@ -104,11 +109,3 @@ impl Render for AppRoot {
.child(self.dialogs.clone()) .child(self.dialogs.clone())
} }
} }
pub fn apply_appearance(settings: &Settings, cx: &mut App) {
match settings.appearance {
AppearanceMode::System => follow_system(cx),
AppearanceMode::Light => set_mode_override(Some(ThemeVariant::Light), cx),
AppearanceMode::Dark => set_mode_override(Some(ThemeVariant::Dark), cx),
}
}
-32
View File
@@ -1,32 +0,0 @@
use assets::CustomIconName;
use gpui_native_kit::badge::{Badge, BadgeVariant};
use gpui_native_kit::icons::Icon;
use gpui_native_kit::tooltip::Tooltip;
use signed_core::RepoStatus;
/// The status pill shown on issue and pull-request rows.
pub fn status_badge(status: RepoStatus) -> Badge {
let (label, variant, icon) = match status {
RepoStatus::Open => ("Open", BadgeVariant::Neutral, CustomIconName::GitIssueOpen),
RepoStatus::Closed => (
"Closed",
BadgeVariant::Danger,
CustomIconName::GitIssueClosed,
),
RepoStatus::Draft => (
"Draft",
BadgeVariant::Warning,
CustomIconName::GitIssueOngoing,
),
RepoStatus::Applied => (
"Applied",
BadgeVariant::Success,
CustomIconName::GitIssueDone,
),
};
Badge::new(format!("status-{label}"), label)
.variant(variant)
.icon(Icon::named(icon))
.tooltip(Tooltip::text(label))
}
+191
View File
@@ -0,0 +1,191 @@
use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, Entity, InteractiveElement as _, IntoElement, ParentElement as _, SharedString,
StatefulInteractiveElement as _, Styled as _, WeakEntity, div, px,
};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
use gpui_native_kit::icons::Icon;
use gpui_native_kit::layout::dock::DockState;
use gpui_native_kit::theme::ActiveTheme as _;
use metadata::ProfileDirectory;
use signed_core::Announcement;
use signed_state::{RepoListStore, Timestamp};
use utils::relative_time;
use crate::repo::actions::open_repo_panel;
const RECENT_COUNT: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum RepoFilter {
All,
#[default]
Popular,
Recent,
}
impl RepoFilter {
/// Indices into the store's `announcements` this filter includes, in display order.
pub(super) fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
let announcements = &store.announcements;
let mut indices: Vec<usize> = (0..announcements.len()).collect();
let query = query.trim().to_lowercase();
if !query.is_empty() {
indices.retain(|&ix| {
let announcement = &announcements[ix];
let name = announcement.name.as_deref().unwrap_or(&announcement.id);
name.to_lowercase().contains(&query)
});
}
match self {
Self::All => {}
Self::Recent => indices.truncate(RECENT_COUNT),
Self::Popular => {
let counts = &store.counts;
let scores: Vec<u32> = announcements
.iter()
.map(|a| counts.get(&a.addr()).map_or(0, |c| c.score()))
.collect();
indices.sort_by(|&a, &b| scores[b].cmp(&scores[a]));
}
}
indices
}
}
pub(super) fn render_card(
ix: usize,
announcement: &Announcement,
last_activity: Option<Timestamp>,
announcements: &[Announcement],
directory: &Entity<ProfileDirectory>,
dock: &WeakEntity<DockState>,
cx: &mut gpui::App,
) -> AnyElement {
let owner = directory.read(cx).profile(&announcement.owner, cx);
let owner_name = owner.name();
let dock = dock.clone();
let announcement = announcement.clone();
let name = announcement
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let description = announcement
.description
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("No description"));
let activity = last_activity
.map(relative_time)
.map(|label| SharedString::from(format!("Updated {label}")))
.unwrap_or_default();
let fork_label: Option<SharedString> = announcement.upstream.as_ref().and_then(|upstream| {
let addr = upstream.addr.as_ref()?;
let name = announcements
.iter()
.find(|a| a.addr() == *addr)
.map(|a| {
a.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone()))
})
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
Some(SharedString::from(format!("forked from {name}")))
});
v_flex()
.id(ix)
.flex_1()
.h_full()
.min_w_0()
.px_3()
.rounded(cx.theme().radius.control)
.border_1()
.border_color(cx.theme().colors.border)
.hover(|this| this.bg(cx.theme().colors.muted_hover))
.child(
h_flex()
.h_10()
.gap_2()
.child(
div()
.min_w_0()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.whitespace_nowrap()
.text_ellipsis()
.child(name),
)
.when_some(fork_label, |this, label| {
this.child(
h_flex()
.gap_1()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(
Icon::named(CustomIconName::GitBranch)
.size(px(12.))
.color(cx.theme().colors.muted_foreground),
)
.child(label),
)
}),
)
.child(
div()
.h_16()
.min_w_0()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.line_clamp(2)
.child(description),
)
.child(
h_flex()
.h_12()
.gap_2()
.overflow_hidden()
.child(
h_flex()
.gap_2()
.child(
Avatar::new(announcement.owner)
.name(owner_name.clone())
.picture(owner.picture())
.small(),
)
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(owner_name),
),
)
.child(
h_flex()
.flex_1()
.justify_end()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(activity),
),
)
.on_click(move |_, window, cx| {
open_repo_panel(&dock, &announcement.addr(), Some(&announcement), window, cx);
})
.into_any_element()
}
@@ -1,12 +1,13 @@
mod helpers;
use assets::CustomIconName; use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _,
Render, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Render, SharedString, Styled as _, Subscription, UniformListScrollHandle, WeakEntity, Window,
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list, div, px, uniform_list,
}; };
use gpui_native_kit::avatar::Avatar; use gpui_native_kit::common::{h_flex, v_flex};
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
use gpui_native_kit::empty_state::EmptyState; use gpui_native_kit::empty_state::EmptyState;
use gpui_native_kit::icons::{Icon, IconRole}; use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::input::{Input, InputState, InputStateEvent}; use gpui_native_kit::input::{Input, InputState, InputStateEvent};
@@ -14,57 +15,13 @@ use gpui_native_kit::layout::dock::{DockState, Panel};
use gpui_native_kit::scrollbar::Scrollbar; use gpui_native_kit::scrollbar::Scrollbar;
use gpui_native_kit::segmented::{Segmented, SegmentedOption}; use gpui_native_kit::segmented::{Segmented, SegmentedOption};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use helpers::{RepoFilter, render_card};
use metadata::ProfileDirectory; use metadata::ProfileDirectory;
use nostr::prelude::PublicKey; use nostr::prelude::PublicKey;
use signed_core::Announcement; use signed_state::RepoListStore;
use signed_state::{RepoListStore, Timestamp};
use utils::relative_time;
use crate::views::repo::actions::open_repo_panel;
const COLUMNS: usize = 2; const COLUMNS: usize = 2;
const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.; const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.;
const RECENT_COUNT: usize = 10;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum RepoFilter {
All,
#[default]
Popular,
Recent,
}
impl RepoFilter {
/// Indices into the store's `announcements` this filter includes, in display order.
fn visible(self, store: &RepoListStore, query: &str) -> Vec<usize> {
let announcements = &store.announcements;
let mut indices: Vec<usize> = (0..announcements.len()).collect();
let query = query.trim().to_lowercase();
if !query.is_empty() {
indices.retain(|&ix| {
let announcement = &announcements[ix];
let name = announcement.name.as_deref().unwrap_or(&announcement.id);
name.to_lowercase().contains(&query)
});
}
match self {
Self::All => {}
Self::Recent => indices.truncate(RECENT_COUNT),
Self::Popular => {
let counts = &store.counts;
let scores: Vec<u32> = announcements
.iter()
.map(|a| counts.get(&a.addr()).map_or(0, |c| c.score()))
.collect();
indices.sort_by(|&a, &b| scores[b].cmp(&scores[a]));
}
}
indices
}
}
pub struct ExploreView { pub struct ExploreView {
store: Entity<RepoListStore>, store: Entity<RepoListStore>,
@@ -85,7 +42,9 @@ impl ExploreView {
let store = RepoListStore::global(cx); let store = RepoListStore::global(cx);
let directory = ProfileDirectory::new(cx); let directory = ProfileDirectory::new(cx);
let entity = cx.entity();
let search = InputState::new(cx); let search = InputState::new(cx);
let search_subscription = cx.subscribe(&search, |this, _search, event, cx| { let search_subscription = cx.subscribe(&search, |this, _search, event, cx| {
if matches!(event, InputStateEvent::TextChanged) { if matches!(event, InputStateEvent::TextChanged) {
this.rebuild_rows(cx); this.rebuild_rows(cx);
@@ -96,7 +55,13 @@ impl ExploreView {
this.rebuild_rows(cx); this.rebuild_rows(cx);
}); });
let mut view = Self { cx.defer(move |cx| {
entity.update(cx, |this, cx| {
this.rebuild_rows(cx);
});
});
Self {
store, store,
dock, dock,
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
@@ -106,9 +71,7 @@ impl ExploreView {
directory, directory,
_search_subscription: search_subscription, _search_subscription: search_subscription,
_store_subscription: store_subscription, _store_subscription: store_subscription,
}; }
view.rebuild_rows(cx);
view
} }
fn rebuild_rows(&mut self, cx: &mut Context<Self>) { fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
@@ -290,133 +253,3 @@ impl Render for ExploreView {
) )
} }
} }
fn render_card(
ix: usize,
announcement: &Announcement,
last_activity: Option<Timestamp>,
announcements: &[Announcement],
directory: &Entity<ProfileDirectory>,
dock: &WeakEntity<DockState>,
cx: &mut gpui::App,
) -> AnyElement {
let owner = directory.read(cx).profile(&announcement.owner, cx);
let name = announcement
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let description = announcement
.description
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("No description"));
let activity = last_activity
.map(relative_time)
.map(|label| SharedString::from(format!("Updated {label}")))
.unwrap_or_default();
let fork_label: Option<SharedString> = announcement.upstream.as_ref().and_then(|upstream| {
let addr = upstream.addr.as_ref()?;
let name = announcements
.iter()
.find(|a| a.addr() == *addr)
.map(|a| {
a.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone()))
})
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
Some(SharedString::from(format!("forked from {name}")))
});
let owner_name = owner.name();
let dock = dock.clone();
let announcement = announcement.clone();
v_flex()
.id(ix)
.flex_1()
.h_full()
.min_w_0()
.px_3()
.rounded(cx.theme().radius.control)
.border_1()
.border_color(cx.theme().colors.border)
.hover(|this| this.bg(cx.theme().colors.muted_hover))
.child(
h_flex()
.h_10()
.gap_2()
.child(
div()
.min_w_0()
.text_sm()
.font_weight(gpui::FontWeight::SEMIBOLD)
.whitespace_nowrap()
.text_ellipsis()
.child(name),
)
.when_some(fork_label, |this, label| {
this.child(
h_flex()
.gap_1()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(
Icon::named(CustomIconName::GitBranch)
.size(px(12.))
.color(cx.theme().colors.muted_foreground),
)
.child(label),
)
}),
)
.child(
div()
.h_16()
.min_w_0()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.line_clamp(2)
.child(description),
)
.child(
h_flex()
.h_12()
.gap_2()
.overflow_hidden()
.child(
h_flex()
.gap_2()
.child(
Avatar::new(announcement.owner)
.name(owner_name.clone())
.picture(owner.picture())
.small(),
)
.child(
div()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(owner_name),
),
)
.child(
h_flex()
.flex_1()
.justify_end()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.whitespace_nowrap()
.child(activity),
),
)
.on_click(move |_, window, cx| {
open_repo_panel(&dock, &announcement.addr(), Some(&announcement), window, cx);
})
.into_any_element()
}
@@ -1,6 +1,5 @@
use gpui::{ use gpui::{
AnyElement, App, AppContext as _, Context, IntoElement, ParentElement as _, Render, AnyElement, Context, IntoElement, ParentElement as _, Render, Styled as _, Window, div,
Styled as _, Window, div,
}; };
use gpui_native_kit::button::{Button, ButtonVariant}; use gpui_native_kit::button::{Button, ButtonVariant};
use gpui_native_kit::common::v_flex; use gpui_native_kit::common::v_flex;
@@ -8,7 +7,7 @@ use gpui_native_kit::dialog::Dialog;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::Backend; use signed_state::Backend;
use super::{close_global, open_global}; use crate::shared::dialogs::close_global;
pub struct ExitDialog; pub struct ExitDialog;
@@ -55,8 +54,3 @@ impl Render for ExitDialog {
}) })
} }
} }
/// Opens the sign-out confirmation.
pub fn open(cx: &mut App) {
open_global(cx.new(|_| ExitDialog), cx);
}
+23
View File
@@ -0,0 +1,23 @@
use gpui::{App, AppContext as _};
use super::exit::ExitDialog;
use super::import::ImportDialog;
use super::onboarding::OnboardingDialog;
use super::passphrase::PassphraseDialog;
use crate::shared::dialogs::open_global;
pub fn open_passphrase(cx: &mut App) {
open_global(PassphraseDialog::new(cx), cx);
}
pub fn open_onboarding(cx: &mut App) {
open_global(OnboardingDialog::new(cx), cx);
}
pub fn open_import(cx: &mut App) {
open_global(ImportDialog::new(cx), cx);
}
pub fn open_exit(cx: &mut App) {
open_global(cx.new(|_| ExitDialog), cx);
}
@@ -11,8 +11,8 @@ use gpui_native_kit::input::{Input, InputState, InputStateEvent};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::Backend; use signed_state::Backend;
use super::close_global; use crate::shared::dialogs::close_global;
use super::progress::{DialogProgress, error_row}; use crate::shared::progress::{DialogProgress, error_row};
#[derive(Clone, Copy, Default, PartialEq, Eq)] #[derive(Clone, Copy, Default, PartialEq, Eq)]
enum Stage { enum Stage {
@@ -65,12 +65,14 @@ pub struct ImportDialog {
impl ImportDialog { impl ImportDialog {
pub fn new(cx: &mut App) -> Entity<Self> { pub fn new(cx: &mut App) -> Entity<Self> {
let state = cx.new(|_| ImportState::default());
let secret_input = InputState::new(cx); let secret_input = InputState::new(cx);
let pass_input = InputState::new(cx); let pass_input = InputState::new(cx);
pass_input.update(cx, |state, cx| state.set_password(true, cx)); pass_input.update(cx, |state, cx| state.set_password(true, cx));
let repass_input = InputState::new(cx); let repass_input = InputState::new(cx);
repass_input.update(cx, |state, cx| state.set_password(true, cx)); repass_input.update(cx, |state, cx| state.set_password(true, cx));
let state = cx.new(|_| ImportState::default());
cx.new(|cx| { cx.new(|cx| {
let mut subscriptions = Vec::new(); let mut subscriptions = Vec::new();
@@ -101,12 +103,15 @@ impl ImportDialog {
impl Render for ImportDialog { impl Render for ImportDialog {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.clone();
let secret_input = self.secret_input.clone(); let secret_input = self.secret_input.clone();
let pass_input = self.pass_input.clone(); let pass_input = self.pass_input.clone();
let repass_input = self.repass_input.clone(); let repass_input = self.repass_input.clone();
let state = self.state.clone();
let action_state = state.clone(); let action_state = state.clone();
let action_secret = secret_input.clone(); let action_secret = secret_input.clone();
let action_pass = pass_input.clone(); let action_pass = pass_input.clone();
let action_repass = repass_input.clone(); let action_repass = repass_input.clone();
@@ -122,6 +127,7 @@ impl Render for ImportDialog {
.content(move |_window, cx| { .content(move |_window, cx| {
let state = state.read(cx); let state = state.read(cx);
let stage = state.stage; let stage = state.stage;
let description = match stage { let description = match stage {
Stage::Credential => { Stage::Credential => {
"Paste the secret key or bunker URI of an existing identity." "Paste the secret key or bunker URI of an existing identity."
@@ -168,11 +174,13 @@ impl Render for ImportDialog {
.actions(move |_, cx| { .actions(move |_, cx| {
let state = action_state.read(cx); let state = action_state.read(cx);
let busy = state.progress.busy; let busy = state.progress.busy;
let label = match state.stage { let label = match state.stage {
Stage::Credential => "Continue", Stage::Credential => "Continue",
Stage::Encrypt => "Import identity", Stage::Encrypt => "Import identity",
Stage::Decrypt => "Unlock", Stage::Decrypt => "Unlock",
}; };
let secret = action_secret.clone(); let secret = action_secret.clone();
let pass = action_pass.clone(); let pass = action_pass.clone();
let repass = action_repass.clone(); let repass = action_repass.clone();
+5
View File
@@ -0,0 +1,5 @@
pub mod exit;
pub mod helpers;
pub mod import;
pub mod onboarding;
pub mod passphrase;
@@ -11,8 +11,8 @@ use gpui_native_kit::input::{Input, InputState};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::Backend; use signed_state::Backend;
use super::close_global; use crate::shared::dialogs::close_global;
use super::progress::{DialogProgress, error_row}; use crate::shared::progress::{DialogProgress, error_row};
pub struct OnboardingDialog { pub struct OnboardingDialog {
name_input: Entity<InputState>, name_input: Entity<InputState>,
@@ -23,12 +23,14 @@ pub struct OnboardingDialog {
impl OnboardingDialog { impl OnboardingDialog {
pub fn new(cx: &mut App) -> Entity<Self> { pub fn new(cx: &mut App) -> Entity<Self> {
let state = cx.new(|_| DialogProgress::default());
let name_input = InputState::new(cx); let name_input = InputState::new(cx);
let pass_input = InputState::new(cx); let pass_input = InputState::new(cx);
pass_input.update(cx, |state, cx| state.set_password(true, cx)); pass_input.update(cx, |state, cx| state.set_password(true, cx));
let repass_input = InputState::new(cx); let repass_input = InputState::new(cx);
repass_input.update(cx, |state, cx| state.set_password(true, cx)); repass_input.update(cx, |state, cx| state.set_password(true, cx));
let state = cx.new(|_| DialogProgress::default());
cx.new(|_| Self { cx.new(|_| Self {
name_input, name_input,
@@ -13,8 +13,8 @@ use gpui_native_kit::input::{Input, InputState, InputStateEvent};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::Backend; use signed_state::Backend;
use super::progress::{DialogProgress, error_row, push_steps_row}; use crate::shared::dialogs::close_global;
use super::{close_global, open_global}; use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
pub struct PassphraseDialog { pub struct PassphraseDialog {
pass_input: Entity<InputState>, pass_input: Entity<InputState>,
@@ -24,9 +24,10 @@ pub struct PassphraseDialog {
impl PassphraseDialog { impl PassphraseDialog {
pub fn new(cx: &mut App) -> Entity<Self> { pub fn new(cx: &mut App) -> Entity<Self> {
let state = cx.new(|_| DialogProgress::default());
let pass_input = InputState::new(cx); let pass_input = InputState::new(cx);
pass_input.update(cx, |state, cx| state.set_password(true, cx)); pass_input.update(cx, |state, cx| state.set_password(true, cx));
let state = cx.new(|_| DialogProgress::default());
cx.new(|cx| { cx.new(|cx| {
let enter_subscription = let enter_subscription =
@@ -132,8 +133,3 @@ impl Render for PassphraseDialog {
}) })
} }
} }
/// Opens the dialog; used by the backend passphrase event and the startup fallback.
pub fn open(cx: &mut App) {
open_global(PassphraseDialog::new(cx), cx);
}
@@ -15,8 +15,8 @@ use signed_core::GitEvent;
use signed_state::RepoStore; use signed_state::RepoStore;
use utils::relative_time; use utils::relative_time;
use crate::components::status_badge; use crate::issues::helpers::{comment_form, comments_section, issue_roots, sidebar_section};
use crate::views::discussion::{comment_form, comments_section, issue_roots, sidebar_section}; use crate::shared::widgets::{repo_tab_avatar, status_badge};
pub struct IssueDetailView { pub struct IssueDetailView {
store: Entity<RepoStore>, store: Entity<RepoStore>,
@@ -97,11 +97,7 @@ impl Panel for IssueDetailView {
} }
fn lead(&self, cx: &gpui::App) -> Option<AnyElement> { fn lead(&self, cx: &gpui::App) -> Option<AnyElement> {
Some(crate::views::repo::repo_tab_avatar( Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
self.store.read(cx),
&self.directory,
cx,
))
} }
} }
@@ -13,23 +13,75 @@ use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip; use gpui_native_kit::tooltip::Tooltip;
use metadata::ProfileDirectory; use metadata::ProfileDirectory;
use nostr::prelude::{Event, EventId, PublicKey}; use nostr::prelude::{Event, EventId, PublicKey};
use signed_core::RepoStatus;
use signed_state::RepoStore; use signed_state::RepoStore;
use utils::relative_time; use utils::relative_time;
pub fn issue_roots(store: &RepoStore) -> &[Event] { #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum IssueFilter {
All,
Open,
Closed,
}
impl IssueFilter {
pub(super) fn matches(self, status: RepoStatus) -> bool {
match self {
Self::All => true,
Self::Open => status == RepoStatus::Open,
Self::Closed => matches!(status, RepoStatus::Closed | RepoStatus::Applied),
}
}
}
/// Root events counted by their resolved status.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct StatusCounts {
pub(super) total: usize,
pub(super) open: usize,
pub(super) closed: usize,
pub(super) draft: usize,
pub(super) applied: usize,
}
impl StatusCounts {
fn record(&mut self, status: RepoStatus) {
self.total += 1;
match status {
RepoStatus::Open => self.open += 1,
RepoStatus::Closed => self.closed += 1,
RepoStatus::Draft => self.draft += 1,
RepoStatus::Applied => self.applied += 1,
}
}
}
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
pub(super) fn filter_by_status<'a>(
roots: impl IntoIterator<Item = &'a Event>,
status_of: impl Fn(&Event) -> RepoStatus,
keep: impl Fn(RepoStatus) -> bool,
) -> (Vec<usize>, StatusCounts) {
let mut counts = StatusCounts::default();
let visible = roots
.into_iter()
.enumerate()
.filter_map(|(index, root)| {
let status = status_of(root);
counts.record(status);
keep(status).then_some(index)
})
.collect();
(visible, counts)
}
pub(super) fn issue_roots(store: &RepoStore) -> &[Event] {
&store.issues &store.issues
} }
fn sidebar_title(text: &str, cx: &App) -> AnyElement { pub(super) fn sidebar_section(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().colors.muted_foreground)
.child(text.to_string())
.into_any_element()
}
pub fn sidebar_section(
store: &Entity<RepoStore>, store: &Entity<RepoStore>,
id: EventId, id: EventId,
roots: fn(&RepoStore) -> &[Event], roots: fn(&RepoStore) -> &[Event],
@@ -61,7 +113,7 @@ pub fn sidebar_section(
v_flex() v_flex()
.when(top_gap, |this| this.mt_4()) .when(top_gap, |this| this.mt_4())
.gap_2() .gap_2()
.child(sidebar_title("Participants", cx)) .child(crate::shared::widgets::section_label("Participants", cx))
.children(participants.iter().map(|pubkey| { .children(participants.iter().map(|pubkey| {
let profile = directory.read(cx).profile(pubkey, cx); let profile = directory.read(cx).profile(pubkey, cx);
let name = profile.name(); let name = profile.name();
@@ -81,7 +133,7 @@ pub fn sidebar_section(
.child( .child(
v_flex() v_flex()
.gap_2() .gap_2()
.child(sidebar_title("Labels", cx)) .child(crate::shared::widgets::section_label("Labels", cx))
.map(|this| { .map(|this| {
if labels.is_empty() { if labels.is_empty() {
this.child( this.child(
@@ -101,7 +153,7 @@ pub fn sidebar_section(
.into_any_element() .into_any_element()
} }
pub fn comments_section( pub(super) fn comments_section(
store: &Entity<RepoStore>, store: &Entity<RepoStore>,
root: EventId, root: EventId,
directory: &Entity<ProfileDirectory>, directory: &Entity<ProfileDirectory>,
@@ -162,8 +214,7 @@ pub fn comments_section(
.into_any_element() .into_any_element()
} }
/// `roots` selects the root's list within the store, issues or pull requests. pub(super) fn comment_form(
pub fn comment_form(
store: &Entity<RepoStore>, store: &Entity<RepoStore>,
root: EventId, root: EventId,
roots: fn(&RepoStore) -> &[Event], roots: fn(&RepoStore) -> &[Event],
@@ -16,37 +16,22 @@ use gpui_native_kit::segmented::{Segmented, SegmentedOption};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use metadata::ProfileDirectory; use metadata::ProfileDirectory;
use nostr::prelude::PublicKey; use nostr::prelude::PublicKey;
use signed_core::{GitEvent, RepoStatus}; use signed_core::GitEvent;
use signed_state::RepoStore; use signed_state::RepoStore;
use utils::relative_time; use utils::relative_time;
use self::detail::IssueDetailView; use self::detail::IssueDetailView;
use super::status_list::{StatusCounts, filter_by_status}; use helpers::{IssueFilter, StatusCounts, filter_by_status};
use crate::components::status_badge;
use crate::dialogs::new_issue; use crate::shared::panels;
use crate::panels; use crate::shared::widgets::{repo_tab_avatar, status_badge};
pub mod detail; pub mod detail;
mod helpers;
pub mod new_issue;
const ISSUE_ROW_HEIGHT: f32 = 73.; const ISSUE_ROW_HEIGHT: f32 = 73.;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IssueFilter {
All,
Open,
Closed,
}
impl IssueFilter {
fn matches(self, status: RepoStatus) -> bool {
match self {
Self::All => true,
Self::Open => status == RepoStatus::Open,
Self::Closed => matches!(status, RepoStatus::Closed | RepoStatus::Applied),
}
}
}
pub struct IssuesView { pub struct IssuesView {
dock: WeakEntity<DockState>, dock: WeakEntity<DockState>,
store: Entity<RepoStore>, store: Entity<RepoStore>,
@@ -185,11 +170,7 @@ impl Panel for IssuesView {
} }
fn lead(&self, cx: &App) -> Option<AnyElement> { fn lead(&self, cx: &App) -> Option<AnyElement> {
Some(super::repo::repo_tab_avatar( Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
self.store.read(cx),
&self.directory,
cx,
))
} }
} }
@@ -10,7 +10,7 @@ use gpui_native_kit::input::{Input, InputState};
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::RepoStore; use signed_state::RepoStore;
use super::{close_global, open_global}; use crate::shared::dialogs::{close_global, open_global};
pub struct NewIssueDialog { pub struct NewIssueDialog {
store: Entity<RepoStore>, store: Entity<RepoStore>,
+8 -5
View File
@@ -1,10 +1,13 @@
mod app; mod app;
pub mod components; mod explore;
mod dialogs; mod identity;
mod panels; mod issues;
mod views; mod repo;
mod shared;
mod sidebar;
pub use app::{AppRoot, apply_appearance}; pub use app::AppRoot;
pub use app::helpers::apply_appearance;
use gpui::{App, AppContext as _, Entity, Window}; use gpui::{App, AppContext as _, Entity, Window};
@@ -2,8 +2,8 @@ use gpui::{App, AppContext as _, SharedString, WeakEntity, Window};
use gpui_native_kit::layout::dock::DockState; use gpui_native_kit::layout::dock::DockState;
use signed_core::{Announcement, RepoAddr}; use signed_core::{Announcement, RepoAddr};
use crate::panels; use crate::repo::RepoDetailView;
use crate::views::repo::RepoDetailView; use crate::shared::panels;
/// Opens a repository as a dock panel, keyed by its address. /// Opens a repository as a dock panel, keyed by its address.
pub fn open_repo_panel( pub fn open_repo_panel(
@@ -11,7 +11,6 @@ use gpui_native_kit::badge::{Badge, BadgeVariant};
use gpui_native_kit::clipboard::ClipboardButton; use gpui_native_kit::clipboard::ClipboardButton;
use gpui_native_kit::common::{h_flex, v_flex}; use gpui_native_kit::common::{h_flex, v_flex};
use gpui_native_kit::data::tree::{Tree, TreeItem}; use gpui_native_kit::data::tree::{Tree, TreeItem};
use gpui_native_kit::empty_state::EmptyState;
use gpui_native_kit::icons::{Icon, IconRole}; use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::layout::dock::Panel; use gpui_native_kit::layout::dock::Panel;
use gpui_native_kit::layout::resizable::{Resizable, ResizableState}; use gpui_native_kit::layout::resizable::{Resizable, ResizableState};
@@ -19,17 +18,17 @@ use gpui_native_kit::scrollbar::Scrollbar;
use gpui_native_kit::spinner::Spinner; use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use metadata::ProfileDirectory; use metadata::ProfileDirectory;
use signed_git::{ use signed_git::{CommitDiff, DiffStatus, FileCommit, FileDiff, Repo};
CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff, Repo,
};
use signed_state::RepoStore; use signed_state::RepoStore;
use utils::relative_time_secs; use utils::relative_time_secs;
use crate::views::tree::{TreeRow, build_tree_items, collect_expanded, flatten}; use super::helpers::{DiffRow, diff_rows, render_diff_row};
use crate::shared::tree::{TreeItemSeed, TreeRow, build_tree_items, collect_expanded, flatten};
use crate::shared::widgets::{empty_state, repo_tab_avatar};
pub struct DiffPane { pub struct DiffPane {
diff: Option<CommitDiff>, diff: Option<CommitDiff>,
seeds: Vec<crate::views::tree::TreeItemSeed>, seeds: Vec<TreeItemSeed>,
expanded: HashSet<String>, expanded: HashSet<String>,
rows: Vec<TreeRow>, rows: Vec<TreeRow>,
selected_file: Option<SharedString>, selected_file: Option<SharedString>,
@@ -195,17 +194,17 @@ impl DiffPane {
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement { fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
let Some(diff) = self.diff.as_ref() else { let Some(diff) = self.diff.as_ref() else {
return placeholder("No changes"); return empty_state("No changes");
}; };
let Some(path) = self.selected_file.clone() else { let Some(path) = self.selected_file.clone() else {
return if diff.files.is_empty() { return if diff.files.is_empty() {
placeholder("No files changed") empty_state("No files changed")
} else { } else {
placeholder("Select a file") empty_state("Select a file")
}; };
}; };
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else { let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
return placeholder("File not found"); return empty_state("File not found");
}; };
self.render_file_diff(file, cx) self.render_file_diff(file, cx)
} }
@@ -230,9 +229,9 @@ impl DiffPane {
}; };
let body: AnyElement = if file.binary { let body: AnyElement = if file.binary {
placeholder("Diff not available") empty_state("Diff not available")
} else if file.hunks.is_empty() { } else if file.hunks.is_empty() {
placeholder("No content changes") empty_state("No content changes")
} else { } else {
let hunks = file.hunks.clone(); let hunks = file.hunks.clone();
let rows = self.diff_rows.clone(); let rows = self.diff_rows.clone();
@@ -515,11 +514,7 @@ impl Panel for CommitDiffView {
} }
fn lead(&self, cx: &App) -> Option<AnyElement> { fn lead(&self, cx: &App) -> Option<AnyElement> {
Some(super::repo_tab_avatar( Some(repo_tab_avatar(self.store.read(cx), &self.directory, cx))
self.store.read(cx),
&self.directory,
cx,
))
} }
} }
@@ -533,7 +528,7 @@ impl Render for CommitDiffView {
.child(Spinner::new("commit-diff-loading")) .child(Spinner::new("commit-diff-loading"))
.into_any_element() .into_any_element()
} else if let Some(error) = self.error.clone() { } else if let Some(error) = self.error.clone() {
placeholder(&error) empty_state(error)
} else { } else {
self.pane.clone().into_any_element() self.pane.clone().into_any_element()
}; };
@@ -545,117 +540,3 @@ impl Render for CommitDiffView {
.child(div().flex_1().min_h_0().child(body)) .child(div().flex_1().min_h_0().child(body))
} }
} }
const GUTTER_WIDTH: f32 = 44.;
pub const DIFF_ROW_HEIGHT: f32 = 20.;
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
#[derive(Clone, Copy)]
enum DiffRow {
Hunk {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
},
Line {
hunk: usize,
line: usize,
},
}
fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
let mut rows = Vec::new();
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
rows.push(DiffRow::Hunk {
old_start: hunk.old_start,
old_lines: hunk.old_lines,
new_start: hunk.new_start,
new_lines: hunk.new_lines,
});
rows.extend((0..hunk.lines.len()).map(|line| DiffRow::Line {
hunk: hunk_ix,
line,
}));
}
rows
}
fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
match row {
DiffRow::Hunk {
old_start,
old_lines,
new_start,
new_lines,
} => div()
.px_2()
.w_full()
.h(px(DIFF_ROW_HEIGHT))
.font_family(cx.theme().mono_font)
.text_xs()
.bg(cx.theme().colors.muted)
.border_y_1()
.border_color(cx.theme().colors.border)
.text_color(cx.theme().colors.muted_foreground)
.child(SharedString::from(format!(
"@@ -{},{} +{},{} @@",
old_start, old_lines, new_start, new_lines
)))
.into_any_element(),
DiffRow::Line { hunk, line } => render_diff_line(&hunks[hunk].lines[line], cx),
}
}
fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
let bg = match line.kind {
DiffLineKind::Addition => Some(cx.theme().ramps.success.base.opacity(0.2)),
DiffLineKind::Deletion => Some(cx.theme().ramps.destructive.base.opacity(0.2)),
DiffLineKind::Context => None,
};
let gutter = cx.theme().colors.muted_foreground;
h_flex()
.w_full()
.h(px(DIFF_ROW_HEIGHT))
.font_family(cx.theme().mono_font)
.text_xs()
.when_some(bg, |this, bg| this.bg(bg))
.child(
div()
.w(px(GUTTER_WIDTH))
.flex_none()
.pr_2()
.text_right()
.text_color(gutter)
.child(line.old.map(|n| n.to_string()).unwrap_or_default()),
)
.child(
div()
.w(px(GUTTER_WIDTH))
.flex_none()
.pr_2()
.text_right()
.text_color(gutter)
.child(line.new.map(|n| n.to_string()).unwrap_or_default()),
)
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_color(cx.theme().colors.foreground)
.child(line.text.clone()),
)
.into_any_element()
}
fn placeholder(message: impl Into<SharedString>) -> AnyElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(EmptyState::new(message))
.into_any_element()
}
@@ -12,7 +12,7 @@ use nostr::prelude::PublicKey;
use signed_core::Announcement; use signed_core::Announcement;
use utils::middle_truncate; use utils::middle_truncate;
use super::{close_global, open_global}; use crate::shared::dialogs::{close_global, open_global};
pub struct AboutDialog { pub struct AboutDialog {
announcement: Announcement, announcement: Announcement,
@@ -18,9 +18,9 @@ use settings::SettingsStore;
use signed_state::{Backend, CheckoutsStore}; use signed_state::{Backend, CheckoutsStore};
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers}; use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
use super::progress::{DialogProgress, error_row, push_steps_row}; use crate::repo::actions::open_repo_panel;
use super::{close_global, open_global}; use crate::shared::dialogs::{close_global, open_global};
use crate::views::repo::actions::open_repo_panel; use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
/// Progress of the create-repository flow, so async results can be rendered. /// Progress of the create-repository flow, so async results can be rendered.
pub struct CreateRepoDialog { pub struct CreateRepoDialog {
@@ -19,9 +19,9 @@ use signed_git::Repo;
use signed_state::Backend; use signed_state::Backend;
use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers}; use super::grasp_servers::{GraspServersState, grasp_servers_field, load_user_grasp_servers};
use super::progress::{DialogProgress, error_row, push_steps_row}; use crate::repo::RepoDetailView;
use super::{close_global, open_global}; use crate::shared::dialogs::{close_global, open_global};
use crate::views::repo::RepoDetailView; use crate::shared::progress::{DialogProgress, error_row, push_steps_row};
pub struct InitRepoDialog { pub struct InitRepoDialog {
local_path: PathBuf, local_path: PathBuf,
@@ -0,0 +1,5 @@
pub mod about;
pub mod create;
pub mod grasp_servers;
pub mod init;
pub mod reset;
@@ -8,7 +8,7 @@ use gpui_native_kit::dialog::Dialog;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_state::RepoStore; use signed_state::RepoStore;
use super::{close_global, open_global}; use crate::shared::dialogs::{close_global, open_global};
pub struct ResetRemoteDialog { pub struct ResetRemoteDialog {
store: Entity<RepoStore>, store: Entity<RepoStore>,
@@ -10,15 +10,14 @@ use gpui::{
use gpui_native_kit::common::{h_flex, v_flex}; use gpui_native_kit::common::{h_flex, v_flex};
use gpui_native_kit::data::tree::{Tree, TreeItem}; use gpui_native_kit::data::tree::{Tree, TreeItem};
use gpui_native_kit::editor::code::CodeViewState; use gpui_native_kit::editor::code::CodeViewState;
use gpui_native_kit::editor::highlight::Language;
use gpui_native_kit::editor::markdown::MarkdownViewState; use gpui_native_kit::editor::markdown::MarkdownViewState;
use gpui_native_kit::empty_state::EmptyState;
use gpui_native_kit::icons::{Icon, IconRole}; use gpui_native_kit::icons::{Icon, IconRole};
use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_git::{FileCommit, Repo, WorktreeSnapshot}; use signed_git::{FileCommit, Repo, WorktreeSnapshot};
use crate::views::tree::{TreeItemSeed, TreeRow, flatten}; use super::helpers::{code_language, is_markdown_path, preview_spinner, source_hash};
use crate::shared::tree::{TreeItemSeed, TreeRow, collect_expanded, flatten};
use crate::shared::widgets::empty_state;
const TREE_WIDTH: f32 = 240.; const TREE_WIDTH: f32 = 240.;
const MAX_PREVIEW_BYTES: usize = 1024 * 1024; const MAX_PREVIEW_BYTES: usize = 1024 * 1024;
@@ -106,7 +105,7 @@ impl RepoFilesView {
) { ) {
self.worktree_paths = paths; self.worktree_paths = paths;
self.seeds = tree; self.seeds = tree;
let folder_ids = folder_ids(&self.seeds); let folder_ids = collect_expanded(&self.seeds);
self.expanded.retain(|id| folder_ids.contains(id)); self.expanded.retain(|id| folder_ids.contains(id));
self.rebuild_rows(); self.rebuild_rows();
cx.notify(); cx.notify();
@@ -323,15 +322,15 @@ impl RepoFilesView {
self.code_element(path.as_ref()) self.code_element(path.as_ref())
} }
} }
Some(FileContent::Binary) => placeholder("Binary file - preview not supported"), Some(FileContent::Binary) => empty_state("Binary file - preview not supported"),
Some(FileContent::TooLarge) => placeholder("File is too large to preview"), Some(FileContent::TooLarge) => empty_state("File is too large to preview"),
Some(FileContent::Failed(message)) => placeholder(message), Some(FileContent::Failed(message)) => empty_state(message),
None => preview_spinner(), None => preview_spinner(),
} }
} else if self.readme_name.is_some() { } else if self.readme_name.is_some() {
self.markdown_element(None) self.markdown_element(None)
} else { } else {
placeholder("No README found") empty_state("No README found")
}; };
let commit = match &self.selected_file { let commit = match &self.selected_file {
@@ -735,75 +734,3 @@ impl Render for RepoFilesView {
.child(self.render_content_column(pane_title, cx)) .child(self.render_content_column(pane_title, cx))
} }
} }
fn folder_ids(seeds: &[TreeItemSeed]) -> HashSet<String> {
fn push(seed: &TreeItemSeed, ids: &mut HashSet<String>) {
if !seed.children.is_empty() {
ids.insert(seed.id.clone());
for child in &seed.children {
push(child, ids);
}
}
}
let mut ids = HashSet::new();
for seed in seeds {
push(seed, &mut ids);
}
ids
}
fn source_hash(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
fn preview_spinner() -> AnyElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(Spinner::new("repo-preview-spinner"))
.into_any_element()
}
fn placeholder(message: impl Into<SharedString>) -> AnyElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(EmptyState::new(message))
.into_any_element()
}
/// The kit grammar for a path; only four languages are supported.
fn code_language(path: &str) -> Language {
match Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("rs") => Language::Rust,
Some("ts" | "mts" | "cts" | "tsx") => Language::TypeScript,
Some("js" | "mjs" | "cjs" | "jsx") => Language::JavaScript,
Some("py") => Language::Python,
_ => Language::Plain,
}
}
/// Whether a file path has a markdown extension.
fn is_markdown_path(path: &str) -> bool {
Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
matches!(
ext.to_ascii_lowercase().as_str(),
"md" | "markdown" | "mdown" | "mkdn"
)
})
}
+514
View File
@@ -0,0 +1,514 @@
use std::path::{Path, PathBuf};
use anyhow::Error;
use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement as _, SharedString, StatefulInteractiveElement as _, Styled as _, Window, div,
px,
};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::clipboard::ClipboardButton;
use gpui_native_kit::combobox::ComboboxState;
use gpui_native_kit::common::{Sizable as _, h_flex, v_flex};
use gpui_native_kit::icons::Icon;
use gpui_native_kit::menu::MenuItem;
use gpui_native_kit::theme::ActiveTheme as _;
use metadata::ProfileDirectory;
use nostr::nips::nip19::Nip19Coordinate;
use nostr::prelude::{RelayUrl, ToBech32};
use signed_core::Announcement;
use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileCommit, FileDiff, Repo};
use signed_state::{Nip34Binding, RepoListStore, RepoStore};
use utils::{middle_truncate, relative_time_secs};
use super::RepoDetailView;
use crate::shared::tree::{TreeItemSeed, build_tree_items};
pub(super) struct RepoData {
pub(super) tree: Vec<TreeItemSeed>,
pub(super) entries: Vec<PathBuf>,
pub(super) readme_path: Option<PathBuf>,
pub(super) readme: Option<Vec<u8>>,
pub(super) worktree: Option<PathBuf>,
pub(super) branches: Vec<String>,
pub(super) tags: Vec<String>,
pub(super) current_branch: Option<String>,
pub(super) head_commit: Option<FileCommit>,
}
pub(super) fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
v_flex()
.gap_1()
.child(div().text_xs().child(SharedString::from(label)))
.child(ClipboardButton::new(id, command.clone()))
.into_any_element()
}
pub(super) fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
let owner = announcement.owner;
let user = nip05
.map(str::to_owned)
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_string()));
let mut url = format!("nostr://{user}");
if let Some(hint) = announcement.relays.first().and_then(RelayUrl::domain) {
url.push('/');
url.push_str(hint);
}
url.push('/');
url.push_str(&announcement.id);
SharedString::from(url)
}
pub(super) fn fork_row(
announcement: &Announcement,
cx: &mut Context<RepoDetailView>,
) -> Option<AnyElement> {
let upstream = announcement.upstream.as_ref()?;
let (label, clickable) = match &upstream.addr {
Some(addr) => {
let name = RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|a| a.addr() == *addr)
.map(|a| {
a.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone()))
})
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
(SharedString::from(format!("Forked from {name}")), true)
}
None => (SharedString::from(upstream.display().as_str()), false),
};
let row = h_flex()
.gap_1()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.child(Icon::named(CustomIconName::GitBranch).size(px(14.)))
.child(div().whitespace_nowrap().text_ellipsis().child(label));
Some(if clickable {
row.id("fork-upstream")
.cursor_pointer()
.on_click(cx.listener(|this, _, window, cx| this.open_upstream(window, cx)))
.into_any_element()
} else {
row.into_any_element()
})
}
#[derive(Clone)]
pub(super) struct ShareTargets {
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
pub(super) naddr: String,
/// Hex ID of the announcement event itself.
event_id: String,
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
coordinate: String,
gitworkshop: String,
ditto: String,
}
impl ShareTargets {
pub(super) fn from_announcement(announcement: &Announcement) -> Self {
let addr = announcement.addr();
let coordinate = addr.to_string();
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
.to_bech32()
.unwrap_or_else(|_| coordinate.clone());
Self {
naddr: naddr.clone(),
event_id: announcement
.event_id
.to_bech32()
.unwrap_or_else(|_| announcement.event_id.to_string()),
coordinate,
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
ditto: format!("https://ditto.pub/{naddr}"),
}
}
pub(super) fn menu(&self) -> Vec<MenuItem> {
let items = [
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
("Ditto", truncate_naddr_link(&self.ditto, 4)),
("Event ID", middle_truncate(&self.event_id, 10, 10)),
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
];
let values = [
self.gitworkshop.clone(),
self.ditto.clone(),
self.event_id.clone(),
self.coordinate.clone(),
];
items
.into_iter()
.zip(values)
.map(|((label, detail), value)| {
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
},
)
})
.collect()
}
}
pub(super) fn truncate_naddr_link(url: &str, tail: usize) -> String {
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
return url.to_string();
};
if url.len() - end <= tail + 3 {
return url.to_string();
}
format!("{}...{}", &url[..end], &url[url.len() - tail..])
}
pub(super) fn load_repo_data(repo: &Repo) -> Result<RepoData, Error> {
let snapshot = repo.snapshot()?;
let worktree = repo.workdir().map(Path::to_path_buf);
let tree = build_tree_items(&snapshot.entries);
Ok(RepoData {
tree,
entries: snapshot.entries,
readme_path: snapshot.readme_path,
readme: snapshot.readme,
worktree,
branches: snapshot.branches,
tags: snapshot.tags,
current_branch: snapshot.current_branch,
head_commit: snapshot.head_commit,
})
}
/// The announcement's name or ID for announced repositories, the directory name for local ones.
pub fn repo_display_name(store: &RepoStore) -> SharedString {
if store.addr().is_none() {
return store
.path
.as_ref()
.map(|path| {
SharedString::from(
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string()),
)
})
.unwrap_or_default();
}
store
.announcement
.as_ref()
.map(|announcement| {
announcement
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
})
.unwrap_or_default()
}
/// The owner and identifier a local repository is already bound to.
pub(super) fn bound_repo_label(
binding: &Nip34Binding,
directory: &Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let profile = binding.owner.map(|pk| directory.read(cx).profile(&pk, cx));
h_flex()
.flex_shrink_0()
.gap_2()
.text_sm()
.child("Initialized by")
.text_color(cx.theme().colors.muted_foreground)
.when_some(profile, |this, profile| {
let name = profile.name();
this.child(
h_flex()
.gap_1()
.text_color(cx.theme().colors.foreground)
.child(
Avatar::new(profile.public_key())
.name(name.clone())
.picture(profile.picture())
.small(),
)
.child(name),
)
})
.when_some(binding.identifier.as_deref(), |this, ident| {
this.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(SharedString::from(format!("/{ident}"))),
)
})
.into_any_element()
}
/// Computes a source hash from the given text.
pub(super) fn source_hash(text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
pub(super) fn preview_spinner() -> AnyElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(gpui_native_kit::spinner::Spinner::new(
"repo-preview-spinner",
))
.into_any_element()
}
/// The kit grammar for a path; only four languages are supported.
pub(super) fn code_language(path: &str) -> gpui_native_kit::editor::highlight::Language {
use gpui_native_kit::editor::highlight::Language;
match Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase())
.as_deref()
{
Some("rs") => Language::Rust,
Some("ts" | "mts" | "cts" | "tsx") => Language::TypeScript,
Some("js" | "mjs" | "cjs" | "jsx") => Language::JavaScript,
Some("py") => Language::Python,
_ => Language::Plain,
}
}
/// Whether a file path has a markdown extension.
pub(super) fn is_markdown_path(path: &str) -> bool {
Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
matches!(
ext.to_ascii_lowercase().as_str(),
"md" | "markdown" | "mdown" | "mkdn"
)
})
}
const GUTTER_WIDTH: f32 = 44.;
pub const DIFF_ROW_HEIGHT: f32 = 20.;
pub const COMMIT_ROW_HEIGHT: f32 = 56.;
#[derive(Clone, Copy)]
pub(super) enum DiffRow {
Hunk {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
},
Line {
hunk: usize,
line: usize,
},
}
pub(super) fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
let mut rows = Vec::new();
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
rows.push(DiffRow::Hunk {
old_start: hunk.old_start,
old_lines: hunk.old_lines,
new_start: hunk.new_start,
new_lines: hunk.new_lines,
});
rows.extend((0..hunk.lines.len()).map(|line| DiffRow::Line {
hunk: hunk_ix,
line,
}));
}
rows
}
pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
match row {
DiffRow::Hunk {
old_start,
old_lines,
new_start,
new_lines,
} => div()
.px_2()
.w_full()
.h(px(DIFF_ROW_HEIGHT))
.font_family(cx.theme().mono_font)
.text_xs()
.bg(cx.theme().colors.muted)
.border_y_1()
.border_color(cx.theme().colors.border)
.text_color(cx.theme().colors.muted_foreground)
.child(SharedString::from(format!(
"@@ -{},{} +{},{} @@",
old_start, old_lines, new_start, new_lines
)))
.into_any_element(),
DiffRow::Line { hunk, line } => render_diff_line(&hunks[hunk].lines[line], cx),
}
}
fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement {
let bg = match line.kind {
DiffLineKind::Addition => Some(cx.theme().ramps.success.base.opacity(0.2)),
DiffLineKind::Deletion => Some(cx.theme().ramps.destructive.base.opacity(0.2)),
DiffLineKind::Context => None,
};
let gutter = cx.theme().colors.muted_foreground;
h_flex()
.w_full()
.h(px(DIFF_ROW_HEIGHT))
.font_family(cx.theme().mono_font)
.text_xs()
.when_some(bg, |this, bg| this.bg(bg))
.child(
div()
.w(px(GUTTER_WIDTH))
.flex_none()
.pr_2()
.text_right()
.text_color(gutter)
.child(line.old.map(|n| n.to_string()).unwrap_or_default()),
)
.child(
div()
.w(px(GUTTER_WIDTH))
.flex_none()
.pr_2()
.text_right()
.text_color(gutter)
.child(line.new.map(|n| n.to_string()).unwrap_or_default()),
)
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_color(cx.theme().colors.foreground)
.child(line.text.clone()),
)
.into_any_element()
}
pub(super) fn commit_row(
ix: usize,
commit: &FileCommit,
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
cx: &gpui::App,
) -> gpui::AnyElement {
h_flex()
.id(ix)
.px_4()
.h(px(COMMIT_ROW_HEIGHT))
.w_full()
.gap_3()
.border_b_1()
.border_color(cx.theme().colors.border)
.hover(|this| this.bg(cx.theme().colors.muted_hover))
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.justify_center()
.child(
h_flex()
.gap_2()
.overflow_hidden()
.child(
div()
.font_family(cx.theme().mono_font)
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(commit.id.clone()),
)
.child(
div()
.flex_1()
.min_w_0()
.text_sm()
.text_ellipsis()
.whitespace_nowrap()
.child(commit.summary.clone()),
),
)
.child(
h_flex()
.gap_2()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(commit.author.clone())
.child(relative_time_secs(commit.time)),
),
)
.on_click(move |_, window, cx| on_click(window, cx))
.into_any_element()
}
pub(super) fn sync_selector(
select: &Entity<ComboboxState<SharedString>>,
cached: &mut Vec<SharedString>,
items: Vec<SharedString>,
selected: Option<SharedString>,
window: &mut Window,
cx: &mut App,
) -> bool {
let items_changed = *cached != items;
let selection_changed = selected
.as_ref()
.is_some_and(|value| select.read(cx).selected() != Some(value));
if !items_changed && !selection_changed {
return false;
}
select.update(cx, |state, cx| {
if items_changed {
state.set_options(
items
.iter()
.map(|item| (item.clone(), item.clone()))
.collect(),
cx,
);
}
if let Some(value) = selected
&& (items_changed || selection_changed)
{
state.set_selected(Some(value), window, cx);
}
});
*cached = items;
true
}
@@ -3,23 +3,21 @@ use std::path::PathBuf;
use anyhow::Error; use anyhow::Error;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AppContext as _, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, SharedString,
Render, SharedString, StatefulInteractiveElement as _, Styled as _, Task, Styled as _, Task, UniformListScrollHandle, WeakEntity, Window, div, uniform_list,
UniformListScrollHandle, WeakEntity, Window, div, px, uniform_list,
}; };
use gpui_native_kit::common::{h_flex, v_flex}; use gpui_native_kit::common::v_flex;
use gpui_native_kit::empty_state::EmptyState;
use gpui_native_kit::layout::dock::DockState; use gpui_native_kit::layout::dock::DockState;
use gpui_native_kit::scrollbar::Scrollbar; use gpui_native_kit::scrollbar::Scrollbar;
use gpui_native_kit::spinner::Spinner; use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use signed_git::{CommitList, FileCommit, Repo}; use signed_git::{CommitList, Repo};
use signed_state::RepoStore; use signed_state::RepoStore;
use utils::relative_time_secs;
use super::commit_diff::{COMMIT_ROW_HEIGHT, CommitDiffView}; use super::commit_diff::CommitDiffView;
use super::repo_display_name; use super::helpers::commit_row;
use crate::panels; use super::helpers::repo_display_name;
use crate::shared::panels;
pub struct RepoHistoryView { pub struct RepoHistoryView {
store: Entity<RepoStore>, store: Entity<RepoStore>,
@@ -144,12 +142,12 @@ impl Render for RepoHistoryView {
.child(Spinner::new("commits-loading")) .child(Spinner::new("commits-loading"))
.into_any_element() .into_any_element()
} else { } else {
empty_state("Failed to load commits") crate::shared::widgets::empty_state("Failed to load commits")
}; };
}; };
if list.commits.is_empty() { if list.commits.is_empty() {
return empty_state("No commits found"); return crate::shared::widgets::empty_state("No commits found");
} }
let commits = list.commits.clone(); let commits = list.commits.clone();
@@ -212,68 +210,3 @@ impl Render for RepoHistoryView {
.into_any_element() .into_any_element()
} }
} }
fn empty_state(message: &'static str) -> gpui::AnyElement {
v_flex()
.size_full()
.items_center()
.justify_center()
.child(EmptyState::new(message))
.into_any_element()
}
/// One commit row shared by the repository history and pull-request commit list.
pub fn commit_row(
ix: usize,
commit: &FileCommit,
on_click: impl Fn(&mut Window, &mut gpui::App) + 'static,
cx: &gpui::App,
) -> gpui::AnyElement {
h_flex()
.id(ix)
.px_4()
.h(px(COMMIT_ROW_HEIGHT))
.w_full()
.gap_3()
.border_b_1()
.border_color(cx.theme().colors.border)
.hover(|this| this.bg(cx.theme().colors.muted_hover))
.child(
v_flex()
.flex_1()
.min_w_0()
.gap_1()
.justify_center()
.child(
h_flex()
.gap_2()
.overflow_hidden()
.child(
div()
.font_family(cx.theme().mono_font)
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(commit.id.clone()),
)
.child(
div()
.flex_1()
.min_w_0()
.text_sm()
.text_ellipsis()
.whitespace_nowrap()
.child(commit.summary.clone()),
),
)
.child(
h_flex()
.gap_2()
.text_xs()
.text_color(cx.theme().colors.muted_foreground)
.child(commit.author.clone())
.child(relative_time_secs(commit.time)),
),
)
.on_click(move |_, window, cx| on_click(window, cx))
.into_any_element()
}
@@ -1,13 +1,13 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use anyhow::Error; use anyhow::Error;
use assets::CustomIconName; use assets::CustomIconName;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _, AnyElement, App, AppContext as _, ClipboardItem, Context, Entity, InteractiveElement as _,
IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, IntoElement, ParentElement as _, PathPromptOptions, Render, SharedString, Styled as _,
StatefulInteractiveElement as _, Styled as _, Subscription, Task, WeakEntity, Window, div, px, Subscription, Task, WeakEntity, Window, div, px,
}; };
use gpui_native_kit::alert::{Alert, AlertSeverity}; use gpui_native_kit::alert::{Alert, AlertSeverity};
use gpui_native_kit::avatar::Avatar; use gpui_native_kit::avatar::Avatar;
@@ -25,28 +25,32 @@ use gpui_native_kit::spinner::Spinner;
use gpui_native_kit::theme::ActiveTheme as _; use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip; use gpui_native_kit::tooltip::Tooltip;
use metadata::ProfileDirectory; use metadata::ProfileDirectory;
use nostr::nips::nip19::Nip19Coordinate; use nostr::prelude::{PublicKey, Url};
use nostr::prelude::{PublicKey, RelayUrl, ToBech32, Url};
use signed_core::{Announcement, RepoAddr}; use signed_core::{Announcement, RepoAddr};
use signed_git::{FileCommit, GitCache, Repo}; use signed_git::{FileCommit, GitCache, Repo};
use signed_state::{ use signed_state::{
Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors, Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors,
Nip34Binding, Nip34Kind, PushStepStatus, RepoListStore, RepoStore, SyncStatusDirectory, Nip34Binding, Nip34Kind, PushStepStatus, RepoStore, SyncStatusDirectory,
}; };
use utils::middle_truncate;
pub mod actions; pub mod actions;
pub mod banners; pub mod banners;
pub mod commit_diff; pub mod commit_diff;
pub mod dialogs;
pub mod files; pub mod files;
pub mod helpers;
pub mod history; pub mod history;
pub mod refs; pub mod refs;
use super::tree::{TreeItemSeed, build_tree_items, sorted_worktree_paths}; use crate::issues::IssuesView;
use crate::dialogs; use crate::shared::tree::{build_tree_items, sorted_worktree_paths};
use crate::views::issues::IssuesView; use crate::shared::widgets::repo_tab_avatar;
use banners::Banners; use banners::Banners;
use files::RepoFilesView; use files::RepoFilesView;
use helpers::{
RepoData, ShareTargets, bound_repo_label, clone_group, fork_row, load_repo_data,
nostr_clone_url, repo_display_name,
};
use history::RepoHistoryView; use history::RepoHistoryView;
use refs::RefSwitcher; use refs::RefSwitcher;
@@ -88,18 +92,6 @@ pub struct RepoDetailView {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
struct RepoData {
tree: Vec<TreeItemSeed>,
entries: Vec<PathBuf>,
readme_path: Option<PathBuf>,
readme: Option<Vec<u8>>,
worktree: Option<PathBuf>,
branches: Vec<String>,
tags: Vec<String>,
current_branch: Option<String>,
head_commit: Option<FileCommit>,
}
impl RepoDetailView { impl RepoDetailView {
pub fn new( pub fn new(
dock: WeakEntity<DockState>, dock: WeakEntity<DockState>,
@@ -795,7 +787,7 @@ impl RepoDetailView {
let deleted = probe.await; let deleted = probe.await;
this.update_in(cx, |this, _window, cx| match deleted { this.update_in(cx, |this, _window, cx| match deleted {
Ok(deleted) => dialogs::reset_remote::open(store, deleted, cx), Ok(deleted) => dialogs::reset::open(store, deleted, cx),
Err(error) => { Err(error) => {
this.error = Some(error.to_string().into()); this.error = Some(error.to_string().into());
cx.notify(); cx.notify();
@@ -816,7 +808,7 @@ impl RepoDetailView {
} }
/// Open the upstream repository, the `u` tag of this fork's announcement. /// Open the upstream repository, the `u` tag of this fork's announcement.
fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(addr) = self let Some(addr) = self
.announcement(cx) .announcement(cx)
.and_then(|announcement| announcement.upstream.as_ref()) .and_then(|announcement| announcement.upstream.as_ref())
@@ -833,7 +825,7 @@ impl RepoDetailView {
let Some(local_path) = self.store.read(cx).path.clone() else { let Some(local_path) = self.store.read(cx).path.clone() else {
return; return;
}; };
dialogs::init_repo::open(local_path, cx.entity().downgrade(), window, cx); dialogs::init::open(local_path, cx.entity().downgrade(), window, cx);
} }
fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn open_issues(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -845,7 +837,7 @@ impl RepoDetailView {
let dock_for_build = dock.clone(); let dock_for_build = dock.clone();
let store = self.store.clone(); let store = self.store.clone();
crate::panels::open( crate::shared::panels::open(
&dock, &dock,
id, id,
move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)), move |_, cx| cx.new(|cx| IssuesView::new(dock_for_build.clone(), store.clone(), cx)),
@@ -984,7 +976,7 @@ impl RepoDetailView {
vec![MenuItem::new("New issue").handler({ vec![MenuItem::new("New issue").handler({
let store = store_for_issues.clone(); let store = store_for_issues.clone();
move |_, cx| { move |_, cx| {
dialogs::new_issue::open(store.clone(), cx); crate::issues::new_issue::open(store.clone(), cx);
} }
})] })]
}) })
@@ -1684,262 +1676,3 @@ impl Render for RepoDetailView {
}) })
} }
} }
fn clone_group(label: &str, id: &'static str, command: &SharedString) -> AnyElement {
v_flex()
.gap_1()
.child(div().text_xs().child(SharedString::from(label)))
.child(ClipboardButton::new(id, command.clone()))
.into_any_element()
}
fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString {
let owner = announcement.owner;
let user = nip05
.map(str::to_owned)
.unwrap_or_else(|| owner.to_bech32().unwrap_or_else(|_| owner.to_string()));
let mut url = format!("nostr://{user}");
if let Some(hint) = announcement.relays.first().and_then(RelayUrl::domain) {
url.push('/');
url.push_str(hint);
}
url.push('/');
url.push_str(&announcement.id);
SharedString::from(url)
}
fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Option<AnyElement> {
let upstream = announcement.upstream.as_ref()?;
let (label, clickable) = match &upstream.addr {
Some(addr) => {
let name = RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|a| a.addr() == *addr)
.map(|a| {
a.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(a.id.clone()))
})
.unwrap_or_else(|| SharedString::from(addr.identifier().to_owned()));
(SharedString::from(format!("Forked from {name}")), true)
}
None => (SharedString::from(upstream.display().as_str()), false),
};
let row = h_flex()
.gap_1()
.min_w_0()
.text_sm()
.text_color(cx.theme().colors.muted_foreground)
.child(Icon::named(CustomIconName::GitBranch).size(px(14.)))
.child(div().whitespace_nowrap().text_ellipsis().child(label));
Some(if clickable {
row.id("fork-upstream")
.cursor_pointer()
.on_click(cx.listener(|this, _, window, cx| this.open_upstream(window, cx)))
.into_any_element()
} else {
row.into_any_element()
})
}
#[derive(Clone)]
struct ShareTargets {
/// NIP-19 `naddr1...` of the announcement, with its announced relays.
naddr: String,
/// Hex ID of the announcement event itself.
event_id: String,
/// NIP-34 coordinate `30617:<pubkey>:<repo-id>`.
coordinate: String,
gitworkshop: String,
ditto: String,
}
impl ShareTargets {
fn from_announcement(announcement: &Announcement) -> Self {
let addr = announcement.addr();
let coordinate = addr.to_string();
let naddr = Nip19Coordinate::new(addr.into(), announcement.relays.iter().cloned())
.to_bech32()
.unwrap_or_else(|_| coordinate.clone());
Self {
naddr: naddr.clone(),
event_id: announcement
.event_id
.to_bech32()
.unwrap_or_else(|_| announcement.event_id.to_string()),
coordinate,
gitworkshop: format!("https://gitworkshop.dev/{naddr}"),
ditto: format!("https://ditto.pub/{naddr}"),
}
}
fn menu(&self) -> Vec<MenuItem> {
let items = [
("GitWorkshop", truncate_naddr_link(&self.gitworkshop, 4)),
("Ditto", truncate_naddr_link(&self.ditto, 4)),
("Event ID", middle_truncate(&self.event_id, 10, 10)),
("Coordinate", middle_truncate(&self.coordinate, 10, 10)),
];
let values = [
self.gitworkshop.clone(),
self.ditto.clone(),
self.event_id.clone(),
self.coordinate.clone(),
];
items
.into_iter()
.zip(values)
.map(|((label, detail), value)| {
MenuItem::new(SharedString::from(format!("{label} {detail}"))).handler(
move |_, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(value.clone()));
},
)
})
.collect()
}
}
fn truncate_naddr_link(url: &str, tail: usize) -> String {
let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else {
return url.to_string();
};
if url.len() - end <= tail + 3 {
return url.to_string();
}
format!("{}...{}", &url[..end], &url[url.len() - tail..])
}
fn load_repo_data(repo: &Repo) -> Result<RepoData, Error> {
let snapshot = repo.snapshot()?;
let worktree = repo.workdir().map(Path::to_path_buf);
let tree = build_tree_items(&snapshot.entries);
Ok(RepoData {
tree,
entries: snapshot.entries,
readme_path: snapshot.readme_path,
readme: snapshot.readme,
worktree,
branches: snapshot.branches,
tags: snapshot.tags,
current_branch: snapshot.current_branch,
head_commit: snapshot.head_commit,
})
}
/// The announcement's name or ID for announced repositories, the directory name for local ones.
pub fn repo_display_name(store: &RepoStore) -> SharedString {
if store.addr().is_none() {
return store
.path
.as_ref()
.map(|path| {
SharedString::from(
path.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string()),
)
})
.unwrap_or_default();
}
store
.announcement
.as_ref()
.map(|announcement| {
announcement
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
})
.unwrap_or_default()
}
/// The owner and identifier a local repository is already bound to.
fn bound_repo_label(
binding: &Nip34Binding,
directory: &Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let profile = binding.owner.map(|pk| directory.read(cx).profile(&pk, cx));
h_flex()
.flex_shrink_0()
.gap_2()
.text_sm()
.child("Initialized by")
.text_color(cx.theme().colors.muted_foreground)
.when_some(profile, |this, profile| {
let name = profile.name();
this.child(
h_flex()
.gap_1()
.text_color(cx.theme().colors.foreground)
.child(
Avatar::new(profile.public_key())
.name(name.clone())
.picture(profile.picture())
.small(),
)
.child(name),
)
})
.when_some(binding.identifier.as_deref(), |this, ident| {
this.child(
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(SharedString::from(format!("/{ident}"))),
)
})
.into_any_element()
}
/// The profile avatar shown on the repository tab.
pub(crate) fn repo_tab_avatar(
store: &RepoStore,
directory: &Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let seed = store
.announcement
.as_ref()
.map(|announcement| format!("{}:{}", announcement.owner, announcement.id))
.or_else(|| store.addr().map(|addr| addr.to_string()))
.or_else(|| {
store
.path
.as_ref()
.map(|path| path.to_string_lossy().into_owned())
})
.unwrap_or_default();
let owner = store
.announcement
.as_ref()
.map(|announcement| directory.read(cx).profile(&announcement.owner, cx));
let (name, picture) = owner
.map(|profile| (Some(profile.name()), profile.picture()))
.unwrap_or((None, None));
let avatar = match name {
Some(name) => Avatar::new(seed).name(name),
None => Avatar::new(seed),
};
avatar.picture(picture).small().into_any_element()
}
@@ -2,6 +2,7 @@ use gpui::{App, AppContext as _, Entity, SharedString, WeakEntity, Window};
use gpui_native_kit::combobox::{Combobox, ComboboxState}; use gpui_native_kit::combobox::{Combobox, ComboboxState};
use super::RepoDetailView; use super::RepoDetailView;
use super::helpers::sync_selector;
pub(super) struct RefSwitcher { pub(super) struct RefSwitcher {
pub(super) branch_select: Entity<ComboboxState<SharedString>>, pub(super) branch_select: Entity<ComboboxState<SharedString>>,
@@ -98,42 +99,3 @@ impl RefSwitcher {
}); });
} }
} }
fn sync_selector(
select: &Entity<ComboboxState<SharedString>>,
cached: &mut Vec<SharedString>,
items: Vec<SharedString>,
selected: Option<SharedString>,
window: &mut Window,
cx: &mut App,
) -> bool {
let items_changed = *cached != items;
let selection_changed = selected
.as_ref()
.is_some_and(|value| select.read(cx).selected() != Some(value));
if !items_changed && !selection_changed {
return false;
}
select.update(cx, |state, cx| {
if items_changed {
state.set_options(
items
.iter()
.map(|item| (item.clone(), item.clone()))
.collect(),
cx,
);
}
if let Some(value) = selected
&& (items_changed || selection_changed)
{
state.set_selected(Some(value), window, cx);
}
});
*cached = items;
true
}
@@ -1,15 +1,3 @@
pub mod about;
pub mod create_repo;
pub mod exit;
pub mod grasp_servers;
pub mod import;
pub mod init_repo;
pub mod new_issue;
pub mod onboarding;
pub mod passphrase;
pub mod progress;
pub mod reset_remote;
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyView, App, Context, Entity, Global, IntoElement, ParentElement as _, Render, Styled as _, AnyView, App, Context, Entity, Global, IntoElement, ParentElement as _, Render, Styled as _,
@@ -61,16 +49,6 @@ pub fn close_global(cx: &mut App) {
host.update(cx, |host, cx| host.close(cx)); host.update(cx, |host, cx| host.close(cx));
} }
/// Opens the onboarding form.
pub fn open_onboarding(cx: &mut App) {
open_global(onboarding::OnboardingDialog::new(cx), cx);
}
/// Opens the identity import form.
pub fn open_import(cx: &mut App) {
open_global(import::ImportDialog::new(cx), cx);
}
impl Render for DialogHost { impl Render for DialogHost {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div() div()
+5
View File
@@ -0,0 +1,5 @@
pub mod dialogs;
pub mod panels;
pub mod progress;
pub mod tree;
pub mod widgets;
+98
View File
@@ -0,0 +1,98 @@
use assets::CustomIconName;
use gpui::{
AnyElement, App, Entity, IntoElement, ParentElement as _, SharedString, Styled as _, div,
};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::badge::{Badge, BadgeVariant};
use gpui_native_kit::common::Sizable as _;
use gpui_native_kit::empty_state::EmptyState;
use gpui_native_kit::icons::Icon;
use gpui_native_kit::theme::ActiveTheme as _;
use gpui_native_kit::tooltip::Tooltip;
use metadata::ProfileDirectory;
use signed_core::RepoStatus;
use signed_state::RepoStore;
/// The status pill shown on issue and pull-request rows.
pub fn status_badge(status: RepoStatus) -> Badge {
let (label, variant, icon) = match status {
RepoStatus::Open => ("Open", BadgeVariant::Neutral, CustomIconName::GitIssueOpen),
RepoStatus::Closed => (
"Closed",
BadgeVariant::Danger,
CustomIconName::GitIssueClosed,
),
RepoStatus::Draft => (
"Draft",
BadgeVariant::Warning,
CustomIconName::GitIssueOngoing,
),
RepoStatus::Applied => (
"Applied",
BadgeVariant::Success,
CustomIconName::GitIssueDone,
),
};
Badge::new(format!("status-{label}"), label)
.variant(variant)
.icon(Icon::named(icon))
.tooltip(Tooltip::text(label))
}
/// A centered empty-state message filling the available space.
pub fn empty_state(message: impl Into<SharedString>) -> AnyElement {
let message = message.into();
gpui_native_kit::common::v_flex()
.size_full()
.items_center()
.justify_center()
.child(EmptyState::new(message))
.into_any_element()
}
/// The profile avatar shown on a repository's panel tab.
pub fn repo_tab_avatar(
store: &RepoStore,
directory: &Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let seed = store
.announcement
.as_ref()
.map(|announcement| format!("{}:{}", announcement.owner, announcement.id))
.or_else(|| store.addr().map(|addr| addr.to_string()))
.or_else(|| {
store
.path
.as_ref()
.map(|path| path.to_string_lossy().into_owned())
})
.unwrap_or_default();
let owner = store
.announcement
.as_ref()
.map(|announcement| directory.read(cx).profile(&announcement.owner, cx));
let (name, picture) = owner
.map(|profile| (Some(profile.name()), profile.picture()))
.unwrap_or((None, None));
let avatar = match name {
Some(name) => Avatar::new(seed).name(name),
None => Avatar::new(seed),
};
avatar.picture(picture).small().into_any_element()
}
/// A one-line muted label used by section headers.
pub fn section_label(text: impl Into<SharedString>, cx: &App) -> AnyElement {
div()
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().colors.muted_foreground)
.child(text.into())
.into_any_element()
}
+51
View File
@@ -0,0 +1,51 @@
use std::time::{SystemTime, UNIX_EPOCH};
use gpui::{AnyElement, App, IntoElement, SharedString};
use gpui_native_kit::avatar::Avatar;
use gpui_native_kit::common::Sizable as _;
use metadata::ProfileDirectory;
use signed_state::{Nip34Binding, ResolvedLocalRepo};
pub(super) fn pick_banner() -> SharedString {
let num = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.subsec_nanos())
.unwrap_or(1)
% 3
+ 1;
format!("backgrounds/banner{num}.jpg").into()
}
/// The platform that bound a repository, `"nak"` or `"ngit"`.
pub(super) fn local_platform(binding: &Nip34Binding) -> Option<&'static str> {
let signals = &binding.signals;
if signals.nip34_json || signals.nip34_grasp_remote || signals.nip34_state_refs {
Some("nak")
} else if signals.nostr_repo_config {
Some("ngit")
} else {
None
}
}
/// The bound owner's avatar, falling back to the path's pixel avatar when unowned.
pub(super) fn local_avatar(
entry: &ResolvedLocalRepo,
directory: &gpui::Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
return Avatar::new(entry.path.to_string_lossy())
.small()
.into_any_element();
};
let profile = directory.read(cx).profile(&owner, cx);
Avatar::new(owner)
.name(profile.name())
.picture(profile.picture())
.small()
.into_any_element()
}
@@ -1,6 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use assets::CustomIconName; use assets::CustomIconName;
use gpui::{ use gpui::{
@@ -22,12 +21,16 @@ use metadata::{Profile, ProfileDirectory};
use nostr::prelude::PublicKey; use nostr::prelude::PublicKey;
use signed_core::{Announcement, RepoAddr}; use signed_core::{Announcement, RepoAddr};
use signed_state::{ use signed_state::{
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind, RepoListStore, Backend, BackendEvent, CheckoutsStore, LocalReposStore, Nip34Kind, RepoListStore,
ResolvedLocalRepo, SyncStatusDirectory, ResolvedLocalRepo, SyncStatusDirectory,
}; };
use crate::dialogs; mod helpers;
use crate::views::explore::ExploreView;
use crate::explore::ExploreView;
use crate::identity;
use crate::repo::dialogs;
use helpers::{local_avatar, local_platform, pick_banner};
pub struct SidebarView { pub struct SidebarView {
weak: WeakEntity<Self>, weak: WeakEntity<Self>,
@@ -351,7 +354,7 @@ impl SidebarView {
if row < self.announcements.len() { if row < self.announcements.len() {
let announcement = self.announcements[row].clone(); let announcement = self.announcements[row].clone();
let addr = announcement.addr(); let addr = announcement.addr();
crate::views::repo::actions::open_repo_panel( crate::repo::actions::open_repo_panel(
&self.dock, &self.dock,
&addr, &addr,
Some(&announcement), Some(&announcement),
@@ -384,12 +387,12 @@ impl SidebarView {
let id = SharedString::from(format!("repo:{}", announcement.addr())); let id = SharedString::from(format!("repo:{}", announcement.addr()));
let dock = self.dock.clone(); let dock = self.dock.clone();
let dock_for_build = dock.clone(); let dock_for_build = dock.clone();
crate::panels::open( crate::shared::panels::open(
&dock, &dock,
id, id,
move |window, cx| { move |window, cx| {
cx.new(|cx| { cx.new(|cx| {
crate::views::repo::RepoDetailView::new_local_announced( crate::repo::RepoDetailView::new_local_announced(
dock_for_build.clone(), dock_for_build.clone(),
announcement.clone(), announcement.clone(),
path.clone(), path.clone(),
@@ -407,12 +410,12 @@ impl SidebarView {
let id = SharedString::from(format!("repo-local:{}", path.display())); let id = SharedString::from(format!("repo-local:{}", path.display()));
let dock = self.dock.clone(); let dock = self.dock.clone();
let dock_for_build = dock.clone(); let dock_for_build = dock.clone();
crate::panels::open( crate::shared::panels::open(
&dock, &dock,
id, id,
move |window, cx| { move |window, cx| {
cx.new(|cx| { cx.new(|cx| {
crate::views::repo::RepoDetailView::new_local( crate::repo::RepoDetailView::new_local(
dock_for_build.clone(), dock_for_build.clone(),
path.clone(), path.clone(),
nip34.clone(), nip34.clone(),
@@ -429,7 +432,7 @@ impl SidebarView {
fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let dock = self.dock.clone(); let dock = self.dock.clone();
let dock_for_build = dock.clone(); let dock_for_build = dock.clone();
crate::panels::open( crate::shared::panels::open(
&dock, &dock,
"explore", "explore",
move |_window, cx| cx.new(|cx| ExploreView::new(dock_for_build.clone(), cx)), move |_window, cx| cx.new(|cx| ExploreView::new(dock_for_build.clone(), cx)),
@@ -517,7 +520,7 @@ impl SidebarView {
MenuItem::separator(), MenuItem::separator(),
MenuItem::new("Exit") MenuItem::new("Exit")
.destructive(true) .destructive(true)
.handler(|_, cx| dialogs::exit::open(cx)), .handler(|_, cx| identity::helpers::open_exit(cx)),
] ]
}); });
@@ -544,7 +547,7 @@ impl SidebarView {
.on_click({ .on_click({
let dock = dock.clone(); let dock = dock.clone();
move |_, window, cx| { move |_, window, cx| {
dialogs::create_repo::open(dock.clone(), window, cx); dialogs::create::open(dock.clone(), window, cx);
} }
}), }),
), ),
@@ -581,7 +584,7 @@ impl SidebarView {
move |_, window, cx| { move |_, window, cx| {
if let Err(error) = weak.update(cx, |this, cx| { if let Err(error) = weak.update(cx, |this, cx| {
this.open_panel("welcome", window, cx); this.open_panel("welcome", window, cx);
crate::dialogs::open_onboarding(cx); identity::helpers::open_onboarding(cx);
}) { }) {
log::warn!("failed to open onboarding: {error}"); log::warn!("failed to open onboarding: {error}");
} }
@@ -593,7 +596,7 @@ impl SidebarView {
.variant(ButtonVariant::Secondary) .variant(ButtonVariant::Secondary)
.on_click(move |_, _, cx| { .on_click(move |_, _, cx| {
if let Err(error) = import_weak if let Err(error) = import_weak
.update(cx, |_, cx| crate::dialogs::open_import(cx)) .update(cx, |_, cx| identity::helpers::open_import(cx))
{ {
log::warn!("failed to open import: {error}"); log::warn!("failed to open import: {error}");
} }
@@ -603,47 +606,3 @@ impl SidebarView {
) )
} }
} }
fn pick_banner() -> SharedString {
let num = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.subsec_nanos())
.unwrap_or(1)
% 3
+ 1;
format!("backgrounds/banner{num}.jpg").into()
}
/// The platform that bound a repository, `"nak"` or `"ngit"`.
fn local_platform(binding: &Nip34Binding) -> Option<&'static str> {
let signals = &binding.signals;
if signals.nip34_json || signals.nip34_grasp_remote || signals.nip34_state_refs {
Some("nak")
} else if signals.nostr_repo_config {
Some("ngit")
} else {
None
}
}
/// The bound owner's avatar, falling back to the path's pixel avatar when unowned.
fn local_avatar(
entry: &ResolvedLocalRepo,
directory: &gpui::Entity<ProfileDirectory>,
cx: &App,
) -> AnyElement {
let Some(owner) = entry.nip34.as_ref().and_then(|binding| binding.owner) else {
return Avatar::new(entry.path.to_string_lossy())
.small()
.into_any_element();
};
let profile = directory.read(cx).profile(&owner, cx);
Avatar::new(owner)
.name(profile.name())
.picture(profile.picture())
.small()
.into_any_element()
}
-8
View File
@@ -1,8 +0,0 @@
pub mod discussion;
pub mod explore;
pub mod issues;
pub mod placeholder;
pub mod repo;
pub mod sidebar;
pub mod status_list;
pub mod tree;
@@ -1,45 +0,0 @@
use nostr::prelude::Event;
use signed_core::RepoStatus;
/// Root events counted by their resolved status.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct StatusCounts {
pub total: usize,
pub open: usize,
pub closed: usize,
pub draft: usize,
pub applied: usize,
}
impl StatusCounts {
fn record(&mut self, status: RepoStatus) {
self.total += 1;
match status {
RepoStatus::Open => self.open += 1,
RepoStatus::Closed => self.closed += 1,
RepoStatus::Draft => self.draft += 1,
RepoStatus::Applied => self.applied += 1,
}
}
}
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
pub fn filter_by_status<'a>(
roots: impl IntoIterator<Item = &'a Event>,
status_of: impl Fn(&Event) -> RepoStatus,
keep: impl Fn(RepoStatus) -> bool,
) -> (Vec<usize>, StatusCounts) {
let mut counts = StatusCounts::default();
let visible = roots
.into_iter()
.enumerate()
.filter_map(|(index, root)| {
let status = status_of(root);
counts.record(status);
keep(status).then_some(index)
})
.collect();
(visible, counts)
}