improve document and comment
This commit is contained in:
@@ -27,11 +27,7 @@ use crate::tiles::SignedTilesSkin;
|
||||
use crate::{TAB_BAR_HEIGHT, panel_handle};
|
||||
|
||||
/// What every part of the skin reads, and the dock area it belongs to.
|
||||
///
|
||||
/// The renderer is the only skin-owned object in the picture, so the settings
|
||||
/// the old `DockArea` carried live here. It is shared by reference with the
|
||||
/// per-container renderers, which are built once each and outlive any one
|
||||
/// frame.
|
||||
/// Shared by reference with the per-container renderers.
|
||||
pub(crate) struct SkinShared {
|
||||
area: WeakEntity<DockArea>,
|
||||
toggle_button_visible: Cell<bool>,
|
||||
@@ -201,10 +197,9 @@ impl DockAreaRenderer for SignedDockSkin {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// The "unknown panel" message the old `InvalidPanel` drew.
|
||||
///
|
||||
/// It answers `dump` with the state it was handed, so a layout written by
|
||||
/// a build that knows the panel survives a load and save here.
|
||||
/// The "unknown panel" message the old `InvalidPanel` drew. It answers
|
||||
/// `dump` with the state it was handed, so a layout written by a build
|
||||
/// that knows the panel survives a load and save here.
|
||||
fn build_placeholder(
|
||||
&self,
|
||||
state: &PanelState,
|
||||
@@ -246,12 +241,10 @@ impl SignedDockSkin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns the window's mouse stream into dock resizing.
|
||||
///
|
||||
/// A resize is driven by pointer moves that land anywhere in the window, not
|
||||
/// only on the handle, so it cannot be expressed as a listener on the handle
|
||||
/// itself. This element paints nothing and exists for its `paint` hook, which
|
||||
/// is the only place a window-level mouse listener can be registered.
|
||||
/// Turns the window's mouse stream into dock resizing. A resize is driven
|
||||
/// by pointer moves anywhere in the window, so this paints nothing and
|
||||
/// exists for its `paint` hook — the only place a window-level mouse
|
||||
/// listener can be registered.
|
||||
struct DockResizeTracker {
|
||||
dock: DockContext,
|
||||
shared: Rc<SkinShared>,
|
||||
@@ -317,10 +310,10 @@ impl Element for DockResizeTracker {
|
||||
if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
|
||||
return;
|
||||
}
|
||||
// Dragging a closed dock's handle reopens it, as the old dock
|
||||
// did. The live state is read rather than the render-time
|
||||
// snapshot in `dock`, which would still say closed for the
|
||||
// rest of the frame and toggle it shut again on the next move.
|
||||
// Dragging a closed dock's handle reopens it. The live
|
||||
// state is read rather than the render-time snapshot in
|
||||
// `dock`, which would still say closed for the rest of the
|
||||
// frame and toggle it shut again on the next move.
|
||||
let open = shared
|
||||
.area()
|
||||
.upgrade()
|
||||
|
||||
@@ -7,12 +7,10 @@ use gpui_component::ActiveTheme as _;
|
||||
|
||||
use crate::Panel;
|
||||
|
||||
/// Stands in for a panel this build cannot construct — one whose `panel_name`
|
||||
/// no [`PanelRegistry`](gpui_base::dock::PanelRegistry) builder answers to.
|
||||
///
|
||||
/// It reports the original [`PanelState`] from
|
||||
/// [`dump`](gpui_base::dock::Panel::dump), so a layout written by a build that
|
||||
/// knows the panel survives a load and a save here rather than losing it.
|
||||
/// Stands in for a panel this build cannot construct. It reports the
|
||||
/// original [`PanelState`] from [`dump`](gpui_base::dock::Panel::dump), so
|
||||
/// a layout written by a build that knows the panel survives a load and
|
||||
/// save here.
|
||||
pub(crate) struct InvalidPanel {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
//! The Signed dock skin.
|
||||
//!
|
||||
//! The dock engine lives upstream: `gpui_base::dock` owns the layout tree,
|
||||
//! the drags, the zoom and the persistence, and `gpui_component::dock`
|
||||
//! supplies the default appearance. This crate is the appearance the app
|
||||
//! used to vendor from gpui-component — a 44px tab bar that doubles as the
|
||||
//! window title bar, with pill tabs, window controls, title-bar dragging and
|
||||
//! previous/next tab buttons — re-implemented against upstream's renderer
|
||||
//! traits.
|
||||
//! The dock engine lives upstream (`gpui_base::dock` owns the layout tree,
|
||||
//! drags, zoom and persistence); this crate is the appearance the app used
|
||||
//! to vendor from gpui-component — a 44px tab bar that doubles as the
|
||||
//! window title bar, with pill tabs, window controls, title-bar dragging
|
||||
//! and previous/next tab buttons.
|
||||
//!
|
||||
//! Everything `gpui_component::dock` exports is re-exported here, so the app
|
||||
//! keeps importing the dock from a single place.
|
||||
|
||||
@@ -84,11 +84,9 @@ impl Render for DragPanelPreview {
|
||||
}
|
||||
|
||||
/// Where the zoom affordance goes for the group's displayed panel, or `None`
|
||||
/// when there is none to offer.
|
||||
///
|
||||
/// Two questions, and both have to be asked. [`Panel::zoom_control`] says
|
||||
/// *where* the control appears; [`gpui_base::dock::Panel::zoomable`] says
|
||||
/// whether zooming happens at all, and base refuses a zoom that fails it.
|
||||
/// when there is none to offer. Both [`Panel::zoom_control`] (where) and
|
||||
/// [`gpui_base::dock::Panel::zoomable`] (whether) must pass; base refuses a
|
||||
/// zoom that fails the latter.
|
||||
fn zoom_control(group: &TabGroupContext, cx: &App) -> Option<PanelControl> {
|
||||
let panel = group.active_panel()?;
|
||||
panel
|
||||
@@ -122,11 +120,8 @@ fn right_top_group(node: &PaneNode) -> Option<NodeId> {
|
||||
}
|
||||
}
|
||||
|
||||
/// One tab group's appearance.
|
||||
///
|
||||
/// Built per group — `DockAreaRenderer::tab_group_renderer` is called once
|
||||
/// per container — so the tab bar's scroll position and the measured
|
||||
/// title-bar geometry belong to the group they describe.
|
||||
/// One tab group's appearance. Built once per container, so the tab bar's
|
||||
/// scroll position and measured title-bar geometry belong to the group.
|
||||
pub(crate) struct SignedTabGroupSkin {
|
||||
shared: Rc<SkinShared>,
|
||||
scroll_handle: ScrollHandle,
|
||||
@@ -177,9 +172,8 @@ impl SignedTabGroupSkin {
|
||||
/// The bottom or right dock whose root tab group this group is, if any.
|
||||
///
|
||||
/// Base bars a dock's only group from being dragged or closed, so the
|
||||
/// dock cannot be emptied. A bottom/right panel is supposed to be
|
||||
/// closable and movable, though — the vendored dock allowed exactly that
|
||||
/// — so the skin recognizes the group and routes around the bar.
|
||||
/// dock cannot be emptied — but a bottom/right panel is supposed to be
|
||||
/// closable, so the skin routes around the bar for these groups.
|
||||
fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option<DockPlacement> {
|
||||
let area = self.shared.area().upgrade()?;
|
||||
let area = area.read(cx);
|
||||
@@ -270,9 +264,7 @@ impl SignedTabGroupSkin {
|
||||
}
|
||||
|
||||
/// The previous/next tab buttons shown in the tab bar's leading prefix.
|
||||
///
|
||||
/// Unlike the dock toggle button they always render, but are disabled at
|
||||
/// the ends of the tab strip (or while the panel is collapsed).
|
||||
/// Always rendered, disabled at the ends of the strip (or collapsed).
|
||||
fn render_prev_next_tab_buttons(
|
||||
&self,
|
||||
group: &TabGroupContext,
|
||||
@@ -405,10 +397,9 @@ impl SignedTabGroupSkin {
|
||||
)
|
||||
}
|
||||
|
||||
/// One tab of the pill strip.
|
||||
///
|
||||
/// While collapsed, tabs lose the active style and all interactions, and
|
||||
/// the strip becomes the way a closed bottom dock is opened again.
|
||||
/// One tab of the pill strip. While collapsed, tabs lose the active
|
||||
/// style and all interactions, and the strip becomes the way a closed
|
||||
/// bottom dock is opened again.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_tab(
|
||||
&self,
|
||||
@@ -578,10 +569,8 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
||||
if group.panels().is_empty() {
|
||||
return div().id("tab-panel");
|
||||
}
|
||||
// Closing the only panel of a bottom/right dock would leave an empty
|
||||
// dock, which base refuses through the group. The skin removes the
|
||||
// whole dock instead — the vendored dock's close took its split
|
||||
// group away just the same.
|
||||
// Closing the only panel of a bottom/right dock would leave an
|
||||
// empty dock, which base refuses; the skin removes the dock instead.
|
||||
let dock_to_remove = (group.panels().len() <= 1)
|
||||
.then(|| self.is_dock_root_group(group, cx))
|
||||
.flatten();
|
||||
@@ -601,8 +590,8 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
||||
this.on_action({
|
||||
let group = group.clone();
|
||||
move |_: &ToggleZoom, window, cx| {
|
||||
// The affordance decides the control, so a panel that
|
||||
// offers none is not zoomed *in* by the keybinding
|
||||
// The affordance decides the control, so a panel
|
||||
// offering none is not zoomed *in* by the keybinding
|
||||
// either. Zooming out is never refused: a panel that
|
||||
// stopped offering the control while zoomed would
|
||||
// otherwise strand the user with no way back.
|
||||
@@ -671,12 +660,10 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
||||
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
|
||||
let is_bottom_dock = bottom_dock_button.is_some();
|
||||
|
||||
// macOS: the traffic lights overlay the window's top-left corner. Only
|
||||
// the group whose tab bar actually sits under them must reserve the
|
||||
// space: the left dock (sidebar) normally clears them, and when it is
|
||||
// closed or absent it is the center's left-most, top-most tab group
|
||||
// that is in the corner. A bottom or right dock is never there, and
|
||||
// neither is the right panel of a center split.
|
||||
// macOS: the traffic lights overlay the window's top-left corner.
|
||||
// Only the group whose tab bar actually sits under them reserves the
|
||||
// space — the center's left-most, top-most group when the left dock
|
||||
// is closed or absent.
|
||||
let needs_traffic_light_padding = cfg!(target_os = "macos")
|
||||
&& self.shared.area().upgrade().is_some_and(|area| {
|
||||
let area = area.read(cx);
|
||||
@@ -703,11 +690,10 @@ impl TabGroupRenderer for SignedTabGroupSkin {
|
||||
self.scroll_handle.scroll_to_item(visible_ix);
|
||||
}
|
||||
|
||||
// The tab strip lays out its scrollable content at content width, so
|
||||
// the area after the last tab only spans `min_w_16` — the rest of the
|
||||
// tab bar has no element at all. Cover that dead zone with a
|
||||
// measured overlay so the whole non-interactive area can drag the
|
||||
// window. Its span is [last tab's right edge, suffix's left edge].
|
||||
// The tab strip lays out at content width, so the area after the
|
||||
// last tab has no element. Cover that dead zone (last tab's right
|
||||
// edge to suffix's left edge) with a measured overlay so the whole
|
||||
// non-interactive area can drag the window.
|
||||
let drag_overlay = match (
|
||||
self.title_bar_bounds.get(),
|
||||
self.title_bar_strip_bounds.get(),
|
||||
|
||||
@@ -101,13 +101,11 @@ impl SignedTilesSkin {
|
||||
})
|
||||
}
|
||||
|
||||
/// The trailing controls of a tile's title bar.
|
||||
///
|
||||
/// A tile has no tab bar to hang a toolbar off, so this is where its zoom,
|
||||
/// close and ellipsis menu live. The entries use click handlers rather
|
||||
/// than the [`ToggleZoom`](crate::ToggleZoom) and
|
||||
/// [`ClosePanel`](crate::ClosePanel) actions: those are dispatched to a
|
||||
/// focused tab group, and a tile is not one.
|
||||
/// The trailing controls of a tile's title bar: zoom, close and the
|
||||
/// ellipsis menu. They use click handlers rather than the
|
||||
/// [`ToggleZoom`](crate::ToggleZoom)/[`ClosePanel`](crate::ClosePanel)
|
||||
/// actions, which are dispatched to a focused tab group — a tile is
|
||||
/// not one.
|
||||
fn render_tile_controls(
|
||||
&self,
|
||||
tile: &TileContext,
|
||||
@@ -388,12 +386,9 @@ impl TilesRenderer for SignedTilesSkin {
|
||||
.size_full()
|
||||
}
|
||||
|
||||
/// The canvas scrollbar.
|
||||
///
|
||||
/// It has to be an overlay rather than one of the frame's own children:
|
||||
/// the frame is the scroll container and base appends the tiles after
|
||||
/// whatever the frame carries, so a scrollbar placed there would paint and
|
||||
/// hit-test underneath every tile.
|
||||
/// The canvas scrollbar. It must be an overlay: the frame is the scroll
|
||||
/// container and base appends the tiles after it, so a scrollbar placed
|
||||
/// inside would paint and hit-test underneath every tile.
|
||||
fn render_overlay(
|
||||
&self,
|
||||
content: Size<Pixels>,
|
||||
|
||||
@@ -71,11 +71,10 @@ pub fn grasp_list(public_key: PublicKey) -> Filter {
|
||||
/// NIP-22 comments (kind `1111`) referencing any of the given root events
|
||||
/// (issues, patches, PRs).
|
||||
///
|
||||
/// Comments are not addressed to the repository — they carry no `a` tag with
|
||||
/// the repo coordinate — so they must be fetched by their root reference
|
||||
/// instead. NIP-22 defines the uppercase `E` tag as the root of the thread
|
||||
/// (used by ngit) while some clients (including Signed itself) reference the
|
||||
/// root with a lowercase `e` tag, so both are matched.
|
||||
/// Comments carry no repository `a` tag, so they must be fetched by their
|
||||
/// root reference. NIP-22 defines the uppercase `E` tag as the thread root
|
||||
/// (used by ngit), but some clients (including Signed) use a lowercase `e`
|
||||
/// tag, so both are matched.
|
||||
///
|
||||
/// Returns two filters because `#E` and `#e` conditions would be ANDed if
|
||||
/// combined into one.
|
||||
|
||||
@@ -70,10 +70,9 @@ const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"];
|
||||
/// Walk `root` recursively and collect the paths of git repositories
|
||||
/// (directories containing a `.git` entry) below it.
|
||||
///
|
||||
/// Hidden entries and symlinks are skipped, and directories that are
|
||||
/// themselves repositories are not descended into (so nested repositories,
|
||||
/// like submodule worktrees, are not reported). Results are canonicalized,
|
||||
/// deduplicated and sorted by path.
|
||||
/// Hidden entries and symlinks are skipped; repositories are not descended
|
||||
/// into, so nested ones (e.g. submodule worktrees) are not reported.
|
||||
/// Results are canonicalized, deduplicated and sorted.
|
||||
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||
let mut repos = Vec::new();
|
||||
if !root.is_dir() {
|
||||
@@ -121,11 +120,9 @@ pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||
/// Clone a repository into `path` from the first working URL in
|
||||
/// `clone_urls` (the announcement's `clone` tag), then fetch the
|
||||
/// `refs/nostr/*` PR refs like the cache clone does. The destination must
|
||||
/// not exist yet; it is created by the clone. The first URL that works
|
||||
/// wins; when none do, the error of the last failing URL is returned.
|
||||
/// not exist yet. When no URL works, the last error is returned.
|
||||
///
|
||||
/// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache;
|
||||
/// callers open it themselves if they need a [`gix::Repository`].
|
||||
/// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache.
|
||||
pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> {
|
||||
if path.exists() {
|
||||
bail!("destination {} already exists", path.display());
|
||||
@@ -218,8 +215,8 @@ fn clone(url: &str, path: &Path) -> Result<gix::Repository> {
|
||||
/// `README.md` derived from `name`/`description`, and create the initial
|
||||
/// commit. Returns the initial commit id.
|
||||
///
|
||||
/// Uses the git CLI (like [`apply_patch`]) because it handles the plumbing
|
||||
/// (index writes, ref updates, default branch selection) natively.
|
||||
/// Uses the git CLI (like [`apply_patch`]), which handles index writes,
|
||||
/// ref updates and default branch selection natively.
|
||||
pub fn init_repository(path: &Path, name: &str, description: &str) -> Result<String> {
|
||||
std::fs::create_dir_all(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))?;
|
||||
@@ -371,11 +368,8 @@ fn git_in(dir: &Path, args: &[&str]) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Map an untrusted repository id (or display name) to a safe single path
|
||||
/// component.
|
||||
///
|
||||
/// Replaces everything outside `[A-Za-z0-9._-]` with `_`, and rejects the
|
||||
/// special components `.` and `..` so the id can't escape a directory it is
|
||||
/// joined onto.
|
||||
/// component: everything outside `[A-Za-z0-9._-]` becomes `_`, and the
|
||||
/// special components `.` and `..` are rejected.
|
||||
pub fn sanitize_path_component(id: &str) -> String {
|
||||
let sanitized: String = id
|
||||
.chars()
|
||||
@@ -517,12 +511,9 @@ fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<Fi
|
||||
}
|
||||
|
||||
/// Find the most recent commit that changed `rel` (a path relative to the
|
||||
/// worktree), like `git log -1 -- <rel>` does for non-merge commits.
|
||||
///
|
||||
/// Walks history from `HEAD` newest-first and returns the first commit whose
|
||||
/// tree entry for `rel` differs from its first parent's; a merge that only
|
||||
/// changed the file through its second parent is therefore not reported.
|
||||
/// Returns `Ok(None)` if no commit touched the file (e.g. untracked files).
|
||||
/// worktree), like `git log -1 -- <rel>`: the first commit, walking from
|
||||
/// `HEAD` newest-first, whose tree entry for `rel` differs from its first
|
||||
/// parent's. `Ok(None)` when no commit touched the file (e.g. untracked).
|
||||
pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result<Option<FileCommit>> {
|
||||
let rel = rel.to_path_buf();
|
||||
Ok(last_commits(repo, std::slice::from_ref(&rel))?
|
||||
@@ -727,10 +718,8 @@ pub struct CommitDiff {
|
||||
}
|
||||
|
||||
/// The changes of the commit `id` (short or full) in the repository at
|
||||
/// `workdir`, compared against its first parent (the empty tree for the root
|
||||
/// commit), like `git show`. Directory entries and submodules are skipped;
|
||||
/// their contents are reported as individual file changes. Files are sorted
|
||||
/// by path.
|
||||
/// `workdir`, compared against its first parent (the empty tree for the
|
||||
/// root commit), like `git show`. Files are sorted by path.
|
||||
pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result<CommitDiff> {
|
||||
commit_diff(&open_with_cache(workdir)?, id)
|
||||
}
|
||||
|
||||
@@ -245,9 +245,8 @@ impl Backend {
|
||||
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
||||
/// the given passphrase and resume the session.
|
||||
///
|
||||
/// The scrypt decryption runs off the UI thread. The returned task
|
||||
/// yields the public key on success, or the failure reason (e.g. wrong
|
||||
/// passphrase), so callers can render inline errors.
|
||||
/// The scrypt decryption runs off the UI thread. The task yields the
|
||||
/// public key, or the failure reason (e.g. wrong passphrase).
|
||||
pub fn restore_with_passphrase(
|
||||
&mut self,
|
||||
password: &str,
|
||||
@@ -286,9 +285,8 @@ impl Backend {
|
||||
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
||||
/// user's NIP-65 relay list, metadata and grasp list.
|
||||
///
|
||||
/// The heavy encryption runs off the UI thread. The returned task yields
|
||||
/// the new public key on success, or the failure reason, so callers can
|
||||
/// render progress and inline errors.
|
||||
/// The encryption runs off the UI thread; the task yields the new
|
||||
/// public key.
|
||||
pub fn create_identity(
|
||||
&mut self,
|
||||
name: &str,
|
||||
@@ -386,11 +384,8 @@ impl Backend {
|
||||
/// a push for a not-yet-existing repository while that authorization is
|
||||
/// pending (it expires after 30 minutes), like gitworkshop and ngit.
|
||||
///
|
||||
/// The git work (init, commit, push) runs on background threads. The
|
||||
/// returned task yields the published announcement on success, so
|
||||
/// callers can open the new repository right away. The announcement's
|
||||
/// `relays` tag carries the grasp servers, which are also added to the
|
||||
/// relay pool so the published events reach them.
|
||||
/// The git work runs on background threads; the task yields the
|
||||
/// published announcement.
|
||||
pub fn create_repository(
|
||||
&mut self,
|
||||
name: &str,
|
||||
@@ -435,8 +430,7 @@ impl Backend {
|
||||
let servers = grasp_servers.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// 1. Initialize the local clone (main branch + README + initial
|
||||
// commit) on a background thread.
|
||||
// Initialize the local clone (main branch + README + initial commit).
|
||||
let work = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
let name = name.clone();
|
||||
@@ -467,16 +461,14 @@ impl Backend {
|
||||
let commit_sha =
|
||||
Sha1Hash::from_str(&commit).map_err(|_| anyhow!("invalid initial commit id"))?;
|
||||
|
||||
// 2. Ensure the grasp servers are in the relay pool; the nostr
|
||||
// client queues events until each relay is connected.
|
||||
// The nostr client queues events until each relay is connected.
|
||||
this.update(cx, |this, cx| {
|
||||
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
||||
this.add_relays(urls, cx);
|
||||
})?;
|
||||
|
||||
// 3. Publish the announcement, then the state event, to the
|
||||
// grasp relays. The state event is the push authorization
|
||||
// ("purgatory"), so it must be accepted before step 4.
|
||||
// The state event is the push authorization ("purgatory"), so
|
||||
// it must be accepted before the push below.
|
||||
let announcement = GitRepositoryAnnouncement {
|
||||
id: repo_id.clone(),
|
||||
name: Some(name.clone()),
|
||||
@@ -522,9 +514,8 @@ impl Backend {
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Push the initial commit to every grasp server. A server
|
||||
// that fails to accept the push is logged, but the creation
|
||||
// only fails when no server accepted it.
|
||||
// Push to every grasp server; creation only fails when no
|
||||
// server accepted it.
|
||||
let push = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
let owner = owner.clone();
|
||||
@@ -555,14 +546,8 @@ impl Backend {
|
||||
/// state to the grasp relays, then push every branch and tag to each
|
||||
/// grasp server. Also points `origin` at the first grasp server.
|
||||
///
|
||||
/// The events must reach the grasp servers *before* the push, like
|
||||
/// [`Self::create_repository`]: GRASP servers hold the signed state
|
||||
/// event in "purgatory" and only accept a push while that
|
||||
/// authorization is pending.
|
||||
///
|
||||
/// The git work (ref listing, push) runs on background threads. The
|
||||
/// returned task yields the published announcement on success, so
|
||||
/// callers can switch the repository into its NIP-34 mode.
|
||||
/// Same ordering constraint as [`Self::create_repository`]: the state
|
||||
/// event ("purgatory") must be accepted before the push.
|
||||
pub fn publish_local_repo(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
@@ -586,9 +571,8 @@ impl Backend {
|
||||
return Task::ready(Err(anyhow!("Sign in to publish a repository")));
|
||||
};
|
||||
|
||||
// The repository identifier is derived from the name, like
|
||||
// [`Self::create_repository`]: spaces become hyphens, other
|
||||
// non-alphanumeric characters (except `/`) become hyphens.
|
||||
// The repository identifier is derived from the name as in
|
||||
// [`Self::create_repository`].
|
||||
let repo_id = identifier_from_name(&name);
|
||||
|
||||
if repo_id.is_empty() || repo_id.len() > 100 {
|
||||
@@ -607,8 +591,6 @@ impl Backend {
|
||||
let servers = grasp_servers.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// 1. Read the local repository's refs (branches, tags, HEAD)
|
||||
// and its root commit on a background thread.
|
||||
let work = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
@@ -619,16 +601,14 @@ impl Backend {
|
||||
});
|
||||
let (state, euc) = work.await?;
|
||||
|
||||
// 2. Ensure the grasp servers are in the relay pool; the nostr
|
||||
// client queues events until each relay is connected.
|
||||
// The nostr client queues events until each relay is connected.
|
||||
this.update(cx, |this, cx| {
|
||||
let urls: Vec<String> = servers.iter().map(ToString::to_string).collect();
|
||||
this.add_relays(urls, cx);
|
||||
})?;
|
||||
|
||||
// 3. Publish the announcement, then the state event, to the
|
||||
// grasp relays. The state event is the push authorization
|
||||
// ("purgatory"), so it must be accepted before step 4.
|
||||
// The state event is the push authorization ("purgatory"), so
|
||||
// it must be accepted before the push below.
|
||||
let announcement = GitRepositoryAnnouncement {
|
||||
id: repo_id.clone(),
|
||||
name: Some(name.clone()),
|
||||
@@ -672,10 +652,9 @@ impl Backend {
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Push every branch and tag to each grasp server. A server
|
||||
// that fails to accept the push is logged, but the init only
|
||||
// fails when no server accepted it. An empty repository
|
||||
// (no refs yet) has nothing to push.
|
||||
// Push every branch and tag to each grasp server; the init
|
||||
// only fails when no server accepted it. An empty repository
|
||||
// has nothing to push.
|
||||
if !refs.is_empty() {
|
||||
let push = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
@@ -697,8 +676,8 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Point `origin` at the first grasp server so later pushes
|
||||
// have a target, like the create flow.
|
||||
// Point `origin` at the first grasp server so later pushes
|
||||
// have a target.
|
||||
if let Some(base) = servers.first().and_then(grasp_base_url) {
|
||||
let url = format!("{base}/{owner}/{repo_id}.git");
|
||||
let path = path.clone();
|
||||
@@ -732,15 +711,13 @@ impl Backend {
|
||||
let relays = announcement.relays.clone();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// 1. Read the current refs of the local clone.
|
||||
let work = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
async move { signed_git::worktree_ref_state(&path) }
|
||||
});
|
||||
let state = work.await?;
|
||||
|
||||
// 2. Publish a fresh state event; grasp servers authorize a
|
||||
// push by the state they have seen.
|
||||
// Grasp servers authorize a push by the state they have seen.
|
||||
let refs = state.refs.clone();
|
||||
let head = state.head.clone();
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -749,7 +726,6 @@ impl Backend {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// 3. Push every branch and tag to the announced grasp servers.
|
||||
if !refs.is_empty() {
|
||||
let push = cx.background_spawn({
|
||||
let path = path.clone();
|
||||
@@ -1111,9 +1087,8 @@ impl Backend {
|
||||
/// `filters`, plus a negentropy sync so issues, patches and PRs stored
|
||||
/// only on those relays are not missed.
|
||||
///
|
||||
/// Best-effort: failures are logged, not surfaced, because the bootstrap
|
||||
/// relays already cover the repository. The relays stay in the pool, so
|
||||
/// events the user publishes for this repository also reach them.
|
||||
/// Best-effort: failures are logged, not surfaced. The relays stay in
|
||||
/// the pool, so later publishes for this repository also reach them.
|
||||
pub fn connect_repo_relays(
|
||||
&mut self,
|
||||
relays: Vec<RelayUrl>,
|
||||
@@ -1221,10 +1196,9 @@ impl Backend {
|
||||
/// Sign, broadcast and locally store an event. Emits
|
||||
/// [`BackendEvent::Published`] on success so stores can refresh.
|
||||
///
|
||||
/// The returned task yields the outcome of this specific action, so
|
||||
/// callers can show inline progress/errors instead of relying on
|
||||
/// the global [`BackendEvent::Error`]. The task is owned by the caller;
|
||||
/// dropping it cancels the publish.
|
||||
/// The task yields the outcome of this specific action (for inline
|
||||
/// progress/errors) and is owned by the caller; dropping it cancels
|
||||
/// the publish.
|
||||
pub fn send(
|
||||
&mut self,
|
||||
builder: EventBuilder,
|
||||
|
||||
@@ -34,7 +34,6 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
||||
.install_default()
|
||||
.ok();
|
||||
|
||||
// Initialize the nostr client and universal signer.
|
||||
let (client, signer) = cx.foreground_executor().block_on(async move {
|
||||
let path = db_path.as_ref().to_path_buf();
|
||||
new_backend(path)
|
||||
@@ -42,24 +41,18 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
||||
.expect("failed to initialize nostr backend")
|
||||
});
|
||||
|
||||
// Initialize the backend and stores.
|
||||
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
||||
Backend::set_global(entity.clone(), cx);
|
||||
|
||||
// Initialize the profile store.
|
||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||
|
||||
// Start the explore list from the local database before
|
||||
// the first window opens, relay syncs continue in the background,
|
||||
// so the list never waits for them.
|
||||
// Seed the explore list from the local database; relay syncs continue
|
||||
// in the background.
|
||||
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
||||
|
||||
// The clone cache is only meaningful on native platforms,
|
||||
// the wasm build registers an empty store so `GitStore::global` still works.
|
||||
// The clone cache is native-only; wasm registers an empty store so
|
||||
// `GitStore::global` still works.
|
||||
GitStore::set_global(PathBuf::new(), cx);
|
||||
|
||||
// Scan the default directories (Desktop, Documents) for local git
|
||||
// repositories; the sidebar lists them next to the user's NIP-34 repos.
|
||||
LocalReposStore::set_global(
|
||||
cx.new(|cx| LocalReposStore::new(default_scan_paths(), cx)),
|
||||
cx,
|
||||
@@ -71,26 +64,13 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
|
||||
/// Initialize the backend with an in-memory database on wasm.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn init(cx: &mut App) -> Entity<Backend> {
|
||||
// Initialize the nostr client and universal signer.
|
||||
let (client, signer) = new_backend().expect("failed to initialize nostr backend");
|
||||
|
||||
// Initialize the backend and stores.
|
||||
let entity = cx.new(|cx| Backend::new(client, signer, cx));
|
||||
Backend::set_global(entity.clone(), cx);
|
||||
|
||||
// Initialize the profile store.
|
||||
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
|
||||
|
||||
// Start the explore list from the local database before
|
||||
// the first window opens, relay syncs continue in the background,
|
||||
// so the list never waits for them.
|
||||
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
|
||||
|
||||
// The clone cache is only meaningful on native platforms,
|
||||
// the wasm build registers an empty store so `GitStore::global` still works.
|
||||
GitStore::set_global(PathBuf::new(), cx);
|
||||
|
||||
// No filesystem scan on wasm: there are no local git repositories.
|
||||
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
|
||||
|
||||
entity
|
||||
|
||||
@@ -586,15 +586,12 @@ impl RepoStore {
|
||||
/// (kind 1617) carrying the `git format-patch` output, which the PR
|
||||
/// references via an `e` tag (NIP-34).
|
||||
///
|
||||
/// The patch is published first and the PR is sent once the patch
|
||||
/// event's id is known, so the two always arrive together. The proposed
|
||||
/// commit is parsed from the patch's `From <commit>` header; publishing
|
||||
/// without one is refused, because the PR's `c` tag (and the patch's
|
||||
/// `commit`/`r` tags) must carry a real commit id for other NIP-34
|
||||
/// clients to verify and apply the proposal. The PR's `clone` tag
|
||||
/// carries the repository's announced mirror URLs (the commit may not be
|
||||
/// pushed there yet; the linked patch is the source of truth until a
|
||||
/// push backend exists).
|
||||
/// The patch is published first so the PR can reference its id. The
|
||||
/// proposed commit is parsed from the patch's `From <commit>` header;
|
||||
/// without one publishing is refused, because the PR's `c` tag must
|
||||
/// carry a real commit id for other NIP-34 clients to verify and apply
|
||||
/// the proposal. The `clone` tag carries the announced mirror URLs; the
|
||||
/// linked patch is the source of truth until the commit is pushed there.
|
||||
pub fn open_pull_request(
|
||||
&mut self,
|
||||
subject: Option<String>,
|
||||
@@ -783,10 +780,9 @@ impl RepoStore {
|
||||
/// the merged status.
|
||||
///
|
||||
/// Only the repository author may merge. The clone is created on demand
|
||||
/// from the announcement's clone URLs when the repository hasn't been
|
||||
/// mirrored locally yet. Patch application runs on a background thread
|
||||
/// (`git am`); failures (e.g. a patch that no longer applies) surface in
|
||||
/// [`Self::last_error`] and no status is sent.
|
||||
/// from the announcement's clone URLs when needed. Patch application
|
||||
/// (`git am`) runs on a background thread; failures (e.g. a patch that
|
||||
/// no longer applies) surface in [`Self::last_error`].
|
||||
pub fn merge_pull_request(&mut self, root: &Event, cx: &mut Context<Self>) {
|
||||
self.last_error = None;
|
||||
|
||||
@@ -876,12 +872,10 @@ fn patch_current_commit(patch: &str) -> Option<&str> {
|
||||
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
|
||||
}
|
||||
|
||||
/// Build a NIP-22 kind-1111 comment using the SDK's [`CommentBuilder`]:
|
||||
/// uppercase `E`/`K`/`P` tags scope the thread root, lowercase `e`/`k`/`p`
|
||||
/// tags the direct parent (`parent`, or the root itself for a top-level
|
||||
/// comment). An `a` tag with the repository coordinate is added so Signed's
|
||||
/// own activity subscriptions also match the comment (it is not part of
|
||||
/// NIP-22).
|
||||
/// Build a NIP-22 kind-1111 comment: uppercase `E`/`K`/`P` tags scope the
|
||||
/// thread root, lowercase `e`/`k`/`p` the direct parent (or the root for a
|
||||
/// top-level comment). An `a` tag with the repository coordinate (not part
|
||||
/// of NIP-22) is added so Signed's own activity subscriptions also match.
|
||||
fn comment_builder(
|
||||
root: &Event,
|
||||
parent: Option<&Event>,
|
||||
|
||||
@@ -22,8 +22,7 @@ impl Global for GlobalRepoListStore {}
|
||||
|
||||
/// Counts of NIP-34 activity events per repository, used to rank the
|
||||
/// explore list by popularity. Each patch event is a pushed commit (or a
|
||||
/// small commit series), which is the closest cross-repository proxy for
|
||||
/// commit count available from event data alone.
|
||||
/// small series), the closest proxy for commit count in the event data.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct RepoActivityCounts {
|
||||
/// Root `30611` issue events addressed to the repository.
|
||||
|
||||
@@ -39,12 +39,9 @@ pub(super) enum FileContent {
|
||||
|
||||
/// A markdown document loaded into a persistent [`TextViewState`].
|
||||
///
|
||||
/// The state is owned by the view rather than created per render (as the
|
||||
/// stateless `text::markdown` helper does), so it survives branch switches
|
||||
/// in the content pane. GPUI's keyed element state is dropped as soon as the
|
||||
/// element is absent for a single frame, which would otherwise re-parse the
|
||||
/// whole document on the main thread every time the pane switches between
|
||||
/// the README, a file preview, and the loading spinner.
|
||||
/// The state is owned by the view rather than created per render: GPUI
|
||||
/// drops keyed element state after one absent frame, which would re-parse
|
||||
/// the whole document on every pane switch (README / file / spinner).
|
||||
pub(super) struct MarkdownView {
|
||||
/// Source path; `None` means the repository README.
|
||||
pub(super) path: Option<SharedString>,
|
||||
@@ -53,11 +50,7 @@ pub(super) struct MarkdownView {
|
||||
|
||||
/// A code file loaded into a persistent [`InputState`], rendered as a
|
||||
/// disabled (read-only) code editor with syntax highlighting, line numbers
|
||||
/// and search.
|
||||
///
|
||||
/// Same persistence rationale as [`MarkdownView`]: the state lives as long
|
||||
/// as this view, so re-viewing the same file does not re-parse it, and
|
||||
/// parsing happens on a background task inside the editor.
|
||||
/// and search. Persistent for the same reason as [`MarkdownView`].
|
||||
pub(super) struct CodeView {
|
||||
/// Source path, relative to the worktree root.
|
||||
pub(super) path: SharedString,
|
||||
@@ -230,9 +223,7 @@ impl RepoDetailView {
|
||||
/// Load `text` into the persistent markdown TextView state.
|
||||
///
|
||||
/// The state is created empty and fed via `push_str`, which parses on a
|
||||
/// background task: switching files never blocks the main thread, and
|
||||
/// the state lives as long as this view, so re-viewing the same document
|
||||
/// does not re-parse it.
|
||||
/// background task, so switching files never blocks the main thread.
|
||||
pub(super) fn set_markdown(
|
||||
&mut self,
|
||||
path: Option<SharedString>,
|
||||
@@ -269,10 +260,8 @@ impl RepoDetailView {
|
||||
/// Load `text` into the persistent code editor state for `path`.
|
||||
///
|
||||
/// The state is created in code editor mode so the Input renders it as
|
||||
/// a syntax-highlighted, read-only editor. Like [`set_markdown`], the
|
||||
/// state lives as long as this view, so re-viewing the same file does
|
||||
/// not re-parse it; the tree-sitter parse runs on a background task
|
||||
/// inside the editor instead of blocking the main thread.
|
||||
/// a syntax-highlighted, read-only editor; the tree-sitter parse runs
|
||||
/// on a background task like [`set_markdown`]'s.
|
||||
pub(super) fn set_code(
|
||||
&mut self,
|
||||
path: SharedString,
|
||||
|
||||
@@ -276,11 +276,8 @@ pub(super) fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
||||
/// A split dropdown button built on `gpui_base::Popover`: an action element
|
||||
/// with a separate caret trigger that opens a [`PopupMenu`].
|
||||
///
|
||||
/// The action and the caret are ordinary elements supplied by the caller, so
|
||||
/// the look — icons, borders, hover states, sizes — stays fully in the
|
||||
/// application. The component only owns the popover wiring: opening on caret
|
||||
/// click, Escape/outside dismissal, focus movement into the menu, and the
|
||||
/// menu entity's lifecycle.
|
||||
/// The action and the caret are caller-supplied elements, so the look stays
|
||||
/// in the application; this component only owns the popover wiring.
|
||||
#[derive(IntoElement)]
|
||||
pub(super) struct BaseDropdownButton {
|
||||
id: ElementId,
|
||||
@@ -493,11 +490,10 @@ impl ShareTargets {
|
||||
}
|
||||
}
|
||||
|
||||
/// One row of the share menu: a small title on top of the compact label,
|
||||
/// with a copy button that flips to a check while the value is on the
|
||||
/// clipboard. Clicking the row text copies and dismisses the menu; the copy
|
||||
/// button stops propagation, so the menu stays open for further copies.
|
||||
/// Both copy `copy`, never the truncated label.
|
||||
/// One row of the share menu: a small title above the compact label, with
|
||||
/// a copy button that flips to a check while the value is on the clipboard.
|
||||
/// Clicking the row copies and dismisses the menu; the copy button stops
|
||||
/// propagation so the menu stays open. Both copy `copy`, never the label.
|
||||
pub(super) fn share_menu_row(
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
|
||||
@@ -26,10 +26,8 @@ use super::helpers::{placeholder, status_badge};
|
||||
use super::issue_detail::IssueDetailView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Height of one issue row in the virtual list: 8px vertical padding
|
||||
/// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta
|
||||
/// line (`h_6`), plus the 1px bottom border; the row totals 73px. The
|
||||
/// status chip (`size_7`, 28px) is shorter than the content.
|
||||
/// Height of one issue row in the virtual list: `py_2` padding, a 32px
|
||||
/// title line (`h_8`), a 24px meta line (`h_6`) and the 1px bottom border.
|
||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||
|
||||
/// Status filter of the issues list, chosen via the header's filter buttons.
|
||||
@@ -39,8 +37,8 @@ enum IssueFilter {
|
||||
All,
|
||||
/// Issues whose resolved status is [`RepoStatus::Open`].
|
||||
Open,
|
||||
/// Issues whose resolved status is
|
||||
/// [`RepoStatus::Closed`] or [`RepoStatus::Applied`] (both are "done" states).
|
||||
/// Issues whose resolved status is [`RepoStatus::Closed`] or
|
||||
/// [`RepoStatus::Applied`] (both are "done" states).
|
||||
Closed,
|
||||
}
|
||||
|
||||
|
||||
@@ -308,9 +308,7 @@ impl RepoDetailView {
|
||||
/// (not yet published) repository is opened straight from disk. An
|
||||
/// announced repository's local clone (if any) is loaded first without
|
||||
/// touching the network, so an unreachable server can't block the
|
||||
/// panel; a background fetch then refreshes the refs and commit list
|
||||
/// (a fetch never changes the checked-out files, so the tree and
|
||||
/// previews are left alone).
|
||||
/// panel; a background fetch then refreshes the refs and commit list.
|
||||
fn load_repo(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
@@ -505,9 +503,7 @@ impl RepoDetailView {
|
||||
}
|
||||
|
||||
/// Clone the repository into a folder chosen by the user (outside the
|
||||
/// cache), then open the new clone in the system file manager. Like
|
||||
/// ngit's clone, this resolves the announcement's `clone` URLs and
|
||||
/// clones from the first working git server.
|
||||
/// cache), then open the new clone in the system file manager.
|
||||
fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.cloning {
|
||||
return;
|
||||
@@ -722,11 +718,8 @@ impl RepoDetailView {
|
||||
|
||||
/// Walk history once for every queued path on a background task, and
|
||||
/// cache the latest commit touching each of them in [`Self::commits`]
|
||||
/// (for the file header in the content column).
|
||||
///
|
||||
/// Batching shares one walk (and its object decodes) across all paths
|
||||
/// queued while the previous walk was in flight, instead of walking the
|
||||
/// full history per file.
|
||||
/// (for the file header in the content column). Batching shares one
|
||||
/// walk across all paths queued while the previous walk was in flight.
|
||||
fn load_commits(&mut self, cx: &mut Context<Self>) {
|
||||
if self.pending_commits.is_empty() || self.loading_commits {
|
||||
return;
|
||||
@@ -1029,8 +1022,7 @@ impl RepoDetailView {
|
||||
|
||||
/// Trigger body for the branch/tag selectors: the kind icon, the
|
||||
/// selection (or placeholder) and the caret. `Combobox` replaces its
|
||||
/// default trigger entirely, which is the only way to show an icon
|
||||
/// inside the trigger label.
|
||||
/// default trigger entirely, the only way to show an icon inside it.
|
||||
fn render_ref_trigger(
|
||||
ctx: &ComboboxTriggerContext<SearchableVec<SharedString>>,
|
||||
icon: CustomIconName,
|
||||
@@ -1923,9 +1915,7 @@ impl Render for RepoDetailView {
|
||||
}
|
||||
|
||||
/// Read the worktree state of `repo` (no network): entries, README, refs
|
||||
/// and HEAD commit. The tree is built off the main thread; the seeds are
|
||||
/// plain owned strings and convert to `TreeItem`s (which hold `Rc` state)
|
||||
/// on the main thread.
|
||||
/// and HEAD commit.
|
||||
fn load_repo_data(repo: &Repository) -> Result<RepoData, Error> {
|
||||
let entries = signed_git::worktree_entries(repo)?;
|
||||
let tree = build_tree_items(&entries);
|
||||
|
||||
@@ -146,9 +146,8 @@ impl PullRequestDetailView {
|
||||
/// and commit list on a background task and populate the tree.
|
||||
///
|
||||
/// The changes come from the PR's patch set (NIP-34 `e`-linked patch
|
||||
/// events) when present; otherwise they live in the git repository
|
||||
/// (`c`, `clone` and `merge-base` tags, per NIP-34), so the clone is
|
||||
/// fetched and the `merge-base..tip` range is diffed.
|
||||
/// events) when present; otherwise from the git repository (`c`,
|
||||
/// `clone` and `merge-base` tags), diffing the `merge-base..tip` range.
|
||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.loading = true;
|
||||
self.error = None;
|
||||
|
||||
@@ -26,11 +26,8 @@ use super::helpers::{placeholder, status_badge};
|
||||
use super::pull_request_detail::PullRequestDetailView;
|
||||
use crate::image_cache::{MAX_IMAGES, image_cache};
|
||||
|
||||
/// Height of one pull request row in the virtual list: same layout as an
|
||||
/// issue row (8px vertical padding (`py_2`) on top and bottom, a 32px title
|
||||
/// line (`h_8`) and a 24px meta line (`h_6`), plus the 1px bottom border),
|
||||
/// so the row totals 73px. The status badge (`size_7`, 28px) is shorter
|
||||
/// than the content.
|
||||
/// Height of one pull request row in the virtual list; same layout as an
|
||||
/// issue row.
|
||||
const PR_ROW_HEIGHT: f32 = 73.;
|
||||
|
||||
/// Status filter of the pull request list, chosen via the header's filter
|
||||
@@ -79,9 +76,8 @@ pub struct PullRequestsView {
|
||||
/// pull request count); rebuilt on change.
|
||||
pr_len: usize,
|
||||
/// Indices into the store's `pull_requests` matching [`Self::filter`]
|
||||
/// (root PR events only; updates are revisions of the root and are not
|
||||
/// listed separately), rebuilt every render; the virtual list renders
|
||||
/// this slice.
|
||||
/// (root PR events only; updates are revisions of the root), rebuilt
|
||||
/// every render; the virtual list renders this slice.
|
||||
visible_prs: Vec<usize>,
|
||||
/// Virtual list state of the pull requests list.
|
||||
scroll_handle: VirtualListScrollHandle,
|
||||
|
||||
@@ -97,9 +97,7 @@ pub struct RepoListView {
|
||||
/// Number of rows [`Self::item_sizes`] was built for (the filtered repo count).
|
||||
repo_len: usize,
|
||||
/// Indices into the store's `announcements` matching [`Self::filter`],
|
||||
/// in display order; rebuilt when the store changes, the filter is
|
||||
/// switched, or the search text changes. The virtual list renders this
|
||||
/// slice.
|
||||
/// in display order; the virtual list renders this slice.
|
||||
visible: Vec<usize>,
|
||||
/// Search box filtering repositories by name.
|
||||
search: Entity<InputState>,
|
||||
@@ -153,10 +151,7 @@ impl RepoListView {
|
||||
}
|
||||
|
||||
/// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current
|
||||
/// store contents, [`Self::filter`] and the search query. Called when
|
||||
/// the view is created, when the store changes, when the filter is
|
||||
/// switched, and on every search keystroke, so the list is ready before
|
||||
/// the next render.
|
||||
/// store contents, [`Self::filter`] and the search query.
|
||||
fn rebuild_rows(&mut self, cx: &mut Context<Self>) {
|
||||
let filter = self.filter;
|
||||
let query = self.search.read(cx).value();
|
||||
|
||||
@@ -417,9 +417,8 @@ impl SidebarPanel {
|
||||
)
|
||||
}
|
||||
|
||||
/// Sign-in placeholder shown while logged out: the banner artwork fills the
|
||||
/// panel behind a scrim that ends in a solid black band, keeping the CTA
|
||||
/// buttons readable on a clean dark surface in both themes.
|
||||
/// Sign-in placeholder shown while logged out: banner artwork behind a
|
||||
/// scrim so the CTA buttons stay readable in both themes.
|
||||
fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||
v_flex()
|
||||
.size_full()
|
||||
|
||||
+2
-14
@@ -7,17 +7,13 @@ use gpui_component::{Theme, ThemeRegistry, theme};
|
||||
use gpui_platform::application;
|
||||
|
||||
fn main() {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
application()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
|
||||
.run(move |cx| {
|
||||
// Initialize components
|
||||
gpui_component::init(cx);
|
||||
|
||||
// Initialize theme
|
||||
theme::init(cx);
|
||||
|
||||
// Register the built-in "Signed" theme (light + dark variants)
|
||||
@@ -49,10 +45,9 @@ fn main() {
|
||||
tracing::warn!("Signed Dark theme is missing from the registry");
|
||||
}
|
||||
|
||||
// Sync the theme with the system appearance
|
||||
Theme::sync_system_appearance(None, cx);
|
||||
|
||||
// Initialize backend and stores (connects relays, restores session)
|
||||
// Connects relays and restores the session.
|
||||
std::fs::create_dir_all(paths::nostr_dir()).ok();
|
||||
signed_state::init(paths::nostr_dir(), cx);
|
||||
|
||||
@@ -60,15 +55,9 @@ fn main() {
|
||||
std::fs::create_dir_all(paths::repos_dir()).ok();
|
||||
signed_state::GitStore::set_global(paths::repos_dir().clone(), cx);
|
||||
|
||||
// Set app identity
|
||||
cx.set_app_identity("su.reya.signed", "Signed");
|
||||
|
||||
// Set up the window options
|
||||
let bounds = Bounds::centered(None, size(px(1120.0), px(750.0)), cx);
|
||||
|
||||
// The dock's tab bar acts as the window title bar: the app owns
|
||||
// title-bar dragging (via `start_window_move` on the tab bar), so
|
||||
// AppKit must not treat the top strip as a native drag region.
|
||||
let opts = WindowOptions {
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
@@ -78,7 +67,7 @@ fn main() {
|
||||
app_id: Some("Signed".to_owned()),
|
||||
titlebar: Some(TitlebarOptions {
|
||||
title: Some(SharedString::new_static("Signed")),
|
||||
// AppKit's traffic-light buttons are 14 pt tall; offset them so their vertical center matches the tab bar.
|
||||
// Center the 14pt traffic-light buttons on the tab bar.
|
||||
traffic_light_position: Some(point(
|
||||
px(9.0),
|
||||
px(TAB_BAR_HEIGHT / px(2.) - 14. / 2.),
|
||||
@@ -94,7 +83,6 @@ fn main() {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Bring the app to the foreground
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
## Local repository scan
|
||||
|
||||
- [ ] Allow the user to configure which directories are scanned for local git repositories (currently fixed to the Desktop and Documents folders).
|
||||
- [ ] Make the scanned directories configurable (currently fixed to Desktop and Documents).
|
||||
|
||||
## Create repository dialog
|
||||
|
||||
- [ ] Persist the user's preferred local repository folder (the one picked in the create-repository dialog, defaulting to Desktop) and use it as the default next time the dialog opens.
|
||||
- [ ] Remember the folder picked in the create-repository dialog and default to it next time (currently defaults to Desktop).
|
||||
|
||||
Reference in New Issue
Block a user