├── .envrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .tool-versions ├── .woodpecker ├── build-arch.yaml ├── build.yaml ├── docker │ └── Dockerfile ├── e2e.yaml ├── lint.yaml └── unit-test.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASE.md ├── arch ├── .gitignore ├── README.md ├── generate-srcinfo.sh ├── publish.sh └── radicle-desktop │ ├── .SRCINFO │ ├── .gitignore │ └── PKGBUILD ├── build └── .gitkeep ├── crates ├── radicle-tauri │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── capabilities │ │ └── default.json │ ├── icons │ │ ├── 128x128.png │ │ ├── 128x128@2x.png │ │ ├── 16x16.png │ │ ├── 16x16@2x.png │ │ ├── 24x24.png │ │ ├── 256x256.png │ │ ├── 256x256@2x.png │ │ ├── 32x32.png │ │ ├── 32x32@2x.png │ │ ├── 48x48.png │ │ ├── 512x512.png │ │ ├── 512x512@2x.png │ │ ├── icon.icns │ │ └── icon.ico │ ├── src │ │ ├── commands.rs │ │ ├── commands │ │ │ ├── auth.rs │ │ │ ├── cob.rs │ │ │ ├── cob │ │ │ │ ├── issue.rs │ │ │ │ └── patch.rs │ │ │ ├── diff.rs │ │ │ ├── inbox.rs │ │ │ ├── profile.rs │ │ │ ├── repo.rs │ │ │ ├── startup.rs │ │ │ └── thread.rs │ │ ├── lib.rs │ │ └── main.rs │ ├── tauri.conf.json │ └── tauri.linux.conf.json ├── radicle-types │ ├── Cargo.toml │ ├── bindings │ │ ├── cob │ │ │ ├── Author.ts │ │ │ ├── CobOptions.ts │ │ │ ├── DiffOptions.ts │ │ │ ├── Embed.ts │ │ │ ├── EmbedWithMimeType.ts │ │ │ ├── Never.ts │ │ │ ├── Operation.ts │ │ │ ├── PaginatedQuery.ts │ │ │ ├── Reaction.ts │ │ │ ├── inbox │ │ │ │ ├── ActionWithAuthor.ts │ │ │ │ ├── Issue.ts │ │ │ │ ├── NotificationCount.ts │ │ │ │ ├── NotificationItem.ts │ │ │ │ ├── NotificationsByRepo.ts │ │ │ │ ├── Patch.ts │ │ │ │ ├── RefUpdate.ts │ │ │ │ ├── SetStatusNotifications.ts │ │ │ │ └── TypedId.ts │ │ │ ├── issue │ │ │ │ ├── Action.ts │ │ │ │ ├── CloseReason.ts │ │ │ │ ├── Issue.ts │ │ │ │ ├── NewIssue.ts │ │ │ │ └── State.ts │ │ │ ├── patch │ │ │ │ ├── Action.ts │ │ │ │ ├── Edit.ts │ │ │ │ ├── Patch.ts │ │ │ │ ├── PatchCounts.ts │ │ │ │ ├── Review.ts │ │ │ │ ├── ReviewEdit.ts │ │ │ │ ├── Revision.ts │ │ │ │ ├── State.ts │ │ │ │ └── Verdict.ts │ │ │ └── thread │ │ │ │ ├── CodeLocation.ts │ │ │ │ ├── CodeRange.ts │ │ │ │ ├── Comment.ts │ │ │ │ ├── CreateReviewComment.ts │ │ │ │ ├── Embed.ts │ │ │ │ ├── NewIssueComment.ts │ │ │ │ ├── NewPatchComment.ts │ │ │ │ └── Thread.ts │ │ ├── config │ │ │ └── Config.ts │ │ ├── diff │ │ │ ├── Added.ts │ │ │ ├── Addition.ts │ │ │ ├── Copied.ts │ │ │ ├── Deleted.ts │ │ │ ├── Deletion.ts │ │ │ ├── Diff.ts │ │ │ ├── DiffContent.ts │ │ │ ├── DiffFile.ts │ │ │ ├── EofNewLine.ts │ │ │ ├── FileDiff.ts │ │ │ ├── FileMode.ts │ │ │ ├── FileStats.ts │ │ │ ├── Hunk.ts │ │ │ ├── Hunks.ts │ │ │ ├── Line.ts │ │ │ ├── Modification.ts │ │ │ ├── Modified.ts │ │ │ ├── Moved.ts │ │ │ └── Stats.ts │ │ ├── error │ │ │ └── ErrorWrapper.ts │ │ ├── repo │ │ │ ├── Commit.ts │ │ │ ├── ProjectPayload.ts │ │ │ ├── ProjectPayloadData.ts │ │ │ ├── ProjectPayloadMeta.ts │ │ │ ├── Readme.ts │ │ │ ├── RepoCount.ts │ │ │ ├── RepoInfo.ts │ │ │ ├── SupportedPayloads.ts │ │ │ ├── SyncStatus.ts │ │ │ ├── SyncedAt.ts │ │ │ └── Visibility.ts │ │ └── syntax │ │ │ ├── Label.ts │ │ │ ├── Line.ts │ │ │ └── Paint.ts │ └── src │ │ ├── cobs.rs │ │ ├── cobs │ │ ├── diff.rs │ │ ├── issue.rs │ │ ├── repo.rs │ │ ├── stream.rs │ │ ├── stream │ │ │ ├── error.rs │ │ │ └── iter.rs │ │ └── thread.rs │ │ ├── config.rs │ │ ├── diff.rs │ │ ├── domain.rs │ │ ├── domain │ │ ├── inbox.rs │ │ ├── inbox │ │ │ ├── models.rs │ │ │ ├── models │ │ │ │ └── notification.rs │ │ │ ├── service.rs │ │ │ └── traits.rs │ │ ├── patch.rs │ │ └── patch │ │ │ ├── models.rs │ │ │ ├── models │ │ │ └── patch.rs │ │ │ ├── service.rs │ │ │ └── traits.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── outbound.rs │ │ ├── outbound │ │ └── sqlite.rs │ │ ├── repo.rs │ │ ├── syntax.rs │ │ ├── test.rs │ │ ├── traits.rs │ │ └── traits │ │ ├── cobs.rs │ │ ├── issue.rs │ │ ├── patch.rs │ │ ├── repo.rs │ │ └── thread.rs └── test-http-api │ ├── Cargo.toml │ └── src │ ├── api.rs │ ├── lib.rs │ └── main.rs ├── eslint.config.js ├── flake.lock ├── flake.nix ├── index.html ├── isolation ├── index.html └── index.js ├── nix └── radicle-desktop.nix ├── package-lock.json ├── package.json ├── playwright.config.ts ├── public ├── colors.css ├── fonts │ ├── Inter-Bold.woff2 │ ├── Inter-Medium.woff2 │ ├── Inter-Regular.woff2 │ ├── Inter-SemiBold.woff2 │ ├── JetBrainsMono-Bold.woff2 │ ├── JetBrainsMono-Medium.woff2 │ ├── JetBrainsMono-Regular.woff2 │ └── JetBrainsMono-SemiBold.woff2 ├── index.css ├── prettylights.css ├── radicle.svg ├── syntax.css ├── twemoji │ └── .gitkeep └── typography.css ├── rust-toolchain ├── scripts ├── check-js ├── check-rs ├── copy-katex-assets ├── install-binaries └── install-twemoji-assets ├── src ├── App.svelte ├── components │ ├── AnnounceSwitch.svelte │ ├── AssigneeInput.svelte │ ├── Avatar.svelte │ ├── Border.svelte │ ├── Button.svelte │ ├── Changes.svelte │ ├── Changeset.svelte │ ├── CheckoutPatchButton.svelte │ ├── CheckoutRepoButton.svelte │ ├── Clipboard.svelte │ ├── CobCommitTeaser.svelte │ ├── Command.svelte │ ├── Comment.svelte │ ├── CommentToggleInput.svelte │ ├── CommitsContainer.svelte │ ├── CompactCommitAuthorship.svelte │ ├── ConfirmClear.svelte │ ├── CopyableId.svelte │ ├── Diff.svelte │ ├── DiffStatBadge.svelte │ ├── Discussion.svelte │ ├── DropdownList.svelte │ ├── DropdownListItem.svelte │ ├── EditableTitle.svelte │ ├── ExtendedTextarea.svelte │ ├── File.svelte │ ├── FileDiff.svelte │ ├── FontSizeSwitch.svelte │ ├── Header.svelte │ ├── HomeSidebar.svelte │ ├── HoverPopover.svelte │ ├── Icon.svelte │ ├── Id.svelte │ ├── InboxPopover.svelte │ ├── InfoButton.svelte │ ├── InlineTitle.svelte │ ├── IssueSecondColumn.svelte │ ├── IssueStateButton.svelte │ ├── IssueStateFilterButton.svelte │ ├── IssueTeaser.svelte │ ├── IssueTimeline.svelte │ ├── IssuesSecondColumn.svelte │ ├── Label.svelte │ ├── LabelInput.svelte │ ├── Link.svelte │ ├── Markdown.svelte │ ├── MoreBreadcrumbsButton.svelte │ ├── NakedButton.svelte │ ├── NewPatchButton.svelte │ ├── NodeBreadcrumb.svelte │ ├── NodeId.svelte │ ├── NodeStatusButton.svelte │ ├── NotificationTeaser.svelte │ ├── NotificationsByRepo.svelte │ ├── OutlineButton.svelte │ ├── PatchStateButton.svelte │ ├── PatchStateFilterButton.svelte │ ├── PatchTeaser.svelte │ ├── PatchTimeline.svelte │ ├── PatchesSecondColumn.svelte │ ├── Path.svelte │ ├── Popover.svelte │ ├── ReactionSelector.svelte │ ├── Reactions.svelte │ ├── RepoCard.svelte │ ├── RepoGuide.svelte │ ├── RepoGuide │ │ ├── clone.md │ │ └── publish.md │ ├── RepoHeader.svelte │ ├── RepoHomeSecondColumn.svelte │ ├── RepoMetadata.svelte │ ├── RepoTeaser.svelte │ ├── Review.svelte │ ├── ReviewButton.svelte │ ├── ReviewTeaser.svelte │ ├── Reviews.svelte │ ├── Revision.svelte │ ├── RevisionBadges.svelte │ ├── RevisionSelector.svelte │ ├── Settings.svelte │ ├── Sidebar.svelte │ ├── Spinner.svelte │ ├── Tab.svelte │ ├── TextInput.svelte │ ├── Textarea.svelte │ ├── ThemeSwitch.svelte │ ├── Thread.svelte │ ├── VerdictBadge.svelte │ ├── VerdictButton.svelte │ └── VisibilityBadge.svelte ├── global.d.ts ├── lib │ ├── appearance.svelte.ts │ ├── auth.svelte.ts │ ├── blockies.ts │ ├── cached.ts │ ├── checkRadicleCLI.svelte.ts │ ├── emojis.ts │ ├── events.ts │ ├── interval.ts │ ├── invoke.ts │ ├── markdown.ts │ ├── mutexExecutor.ts │ ├── notification.ts │ ├── roles.ts │ ├── router.ts │ ├── router │ │ └── definitions.ts │ ├── sleep.ts │ ├── startup.svelte.ts │ ├── syntax.ts │ ├── useLocalStorage.svelte.ts │ └── utils.ts ├── main.ts ├── views │ ├── booting │ │ ├── Auth.svelte │ │ └── CreateIdentity.svelte │ ├── home │ │ ├── Inbox.svelte │ │ ├── Repos.svelte │ │ └── router.ts │ └── repo │ │ ├── BreadcrumbCopyButton.svelte │ │ ├── CreateIssue.svelte │ │ ├── Issue.svelte │ │ ├── Issues.svelte │ │ ├── IssuesBreadcrumb.svelte │ │ ├── Layout.svelte │ │ ├── Patch.svelte │ │ ├── Patches.svelte │ │ ├── PatchesBreadcrumb.svelte │ │ ├── RepoBreadcrumb.svelte │ │ ├── RepoHome.svelte │ │ └── router.ts └── vite-env.d.ts ├── svelte.config.js ├── tests ├── e2e │ ├── clipboard.spec.ts │ ├── repo │ │ ├── issue.spec.ts │ │ └── issues.spec.ts │ ├── repos.spec.ts │ └── theme.spec.ts ├── fixtures │ └── repos │ │ └── markdown.tar.bz2 ├── support │ ├── cobs │ │ ├── issue.ts │ │ └── patch.ts │ ├── fixtures.ts │ ├── globalSetup.ts │ ├── heartwood-release │ ├── logPrefix.ts │ ├── peerManager.ts │ ├── repo.ts │ ├── router.ts │ └── support.ts └── unit │ └── notifications.test.ts ├── tsconfig.json └── vite.config.ts /.envrc: -------------------------------------------------------------------------------- 1 | watch_file rust-toolchain nix/* 2 | 3 | use flake 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /target/ 3 | node_modules/ 4 | 5 | # Tauri 6 | crates/test-http-api/target 7 | crates/radicle-tauri/target 8 | crates/radicle-tauri/gen/schemas 9 | 10 | # KaTeX files 11 | public/*.min.css 12 | public/fonts/KaTeX_**.ttf 13 | public/fonts/KaTeX_**.woff 14 | public/fonts/KaTeX_**.woff2 15 | 16 | # Twemoji Assets 17 | public/twemoji/*.svg 18 | 19 | # Editor directories and files 20 | .vscode 21 | .idea 22 | 23 | # Mac OS 24 | .DS_Store 25 | 26 | # Integration Tests 27 | tests/tmp/**/* 28 | tests/artifacts/**/* 29 | 30 | # direnv cache 31 | /.direnv/ 32 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.11.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "plugins": ["prettier-plugin-svelte"], 4 | "svelteSortOrder": "options-scripts-styles-markup", 5 | "bracketSameLine": true, 6 | "proseWrap": "never", 7 | "htmlWhitespaceSensitivity": "ignore" 8 | } 9 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 22.11.0 2 | -------------------------------------------------------------------------------- /.woodpecker/build-arch.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: pull_request 3 | - event: push 4 | branch: main 5 | - event: manual 6 | 7 | steps: 8 | build-arch: 9 | image: docker.io/library/archlinux:base-devel 10 | when: 11 | - path: "arch/*" 12 | - event: manual 13 | commands: 14 | - pacman -Sy --noconfirm git 15 | - useradd -m builder 16 | - 'echo "builder ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/builder' 17 | - chown -R builder:builder /home/builder 18 | - su builder 19 | - set -e 20 | - cd /home/builder 21 | - rm -rf radicle-bin && git clone https://aur.archlinux.org/radicle-bin.git --depth=1 22 | - makepkg --dir radicle-bin --install --syncdeps --noconfirm 23 | - cd $CI_WORKSPACE/arch/radicle-desktop 24 | # Ensure .SCRINFO is up-to-date 25 | - makepkg --printsrcinfo > .SRCINFO && git diff --exit-code .SRCINFO 26 | - | 27 | CI=true \ 28 | BUILDDIR=/home/builder/build \ 29 | PKGDEST=/home/builder/ \ 30 | SRCDEST=/home/builder/ \ 31 | makepkg --syncdeps --noconfirm --force 32 | -------------------------------------------------------------------------------- /.woodpecker/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM devraymondsh/ubuntu-rust:24.04-1.84 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | build-essential \ 5 | jq \ 6 | curl \ 7 | git \ 8 | wget \ 9 | file \ 10 | zstd \ 11 | libxdo-dev \ 12 | libssl-dev \ 13 | libayatana-appindicator3-dev \ 14 | librsvg2-dev \ 15 | libwebkit2gtk-4.1-0=2.44.0-2 \ 16 | libwebkit2gtk-4.1-dev=2.44.0-2 \ 17 | libjavascriptcoregtk-4.1-0=2.44.0-2 \ 18 | libjavascriptcoregtk-4.1-dev=2.44.0-2 \ 19 | gir1.2-javascriptcoregtk-4.1=2.44.0-2 \ 20 | gir1.2-webkit2-4.1=2.44.0-2 21 | 22 | RUN rustup component add rustfmt clippy 23 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs 24 | 25 | SHELL ["/bin/bash", "-c"] 26 | -------------------------------------------------------------------------------- /.woodpecker/e2e.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: pull_request 3 | - event: push 4 | branch: main 5 | 6 | variables: 7 | - &base_image "docker.io/sebastinez/radicle-desktop-base:latest_3" 8 | - &cache_endpoint "https://minio-api.radworks.garden/build-caches/radicle-desktop/cache" 9 | - &cache_dir "radicle-desktop/cache" 10 | 11 | steps: 12 | cache: 13 | image: *base_image 14 | pull: true 15 | environment: 16 | CACHE_ENDPOINT: *cache_endpoint 17 | CACHE_DIR: *cache_dir 18 | entrypoint: 19 | - "/bin/bash" 20 | - "-c" 21 | - | 22 | set -euo pipefail 23 | 24 | # Initialize cache status file 25 | echo "# Cache status" > .cache 26 | 27 | export ARCH=$(uname -m) 28 | export RUST_VERSION=$(rustc --version | cut -d ' ' -f 2) 29 | export RUST_HASH=$(sha256sum Cargo.lock | cut -d ' ' -f 1) 30 | export FILE_NAME="rust-e2e-""$ARCH""-""$RUST_VERSION""-""$RUST_HASH"".tar.zst" 31 | echo "FILE_NAME=$FILE_NAME" >> .cache 32 | cat .cache 33 | 34 | # Create temporary directory for cache files 35 | mkdir -p "$CACHE_DIR" 36 | 37 | echo "Checking cache..." 38 | url="$CACHE_ENDPOINT""/""$FILE_NAME" 39 | HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --head $url) 40 | 41 | if [ "$HTTP_CODE" = "200" ]; then 42 | echo "Cache hit! Downloading..." 43 | curl -s -o "$CACHE_DIR""/""$FILE_NAME" $url 44 | echo "CACHE_HIT=true" >> .cache 45 | else 46 | echo "No cache found (HTTP status: $HTTP_CODE)" 47 | echo "CACHE_HIT=false" >> .cache 48 | fi 49 | end-to-end: 50 | image: *base_image 51 | pull: true 52 | environment: 53 | CACHE_ENDPOINT: *cache_endpoint 54 | CACHE_DIR: *cache_dir 55 | entrypoint: 56 | - "/bin/bash" 57 | - "-c" 58 | - | 59 | set -euo pipefail 60 | 61 | cat .cache 62 | source .cache 63 | 64 | if [ "$CACHE_HIT" = "true" ]; then 65 | echo "Extracting cache..." 66 | tar --zstd -xf "$CACHE_DIR""/""$FILE_NAME" 67 | fi 68 | 69 | ./scripts/install-binaries; 70 | npm ci 71 | npm run build:http 72 | mkdir -p tests/artifacts; 73 | 74 | # Install and run playwright 75 | npx playwright install webkit chromium --with-deps 76 | npm run test:e2e -- --project webkit 77 | 78 | if [ "$CACHE_HIT" = "false" ]; then 79 | echo "Creating debug cache archive..." 80 | tar --zstd -cf "$CACHE_DIR""/""$FILE_NAME" target 81 | fi 82 | 83 | upload-cache: 84 | image: woodpeckerci/plugin-s3 85 | settings: 86 | endpoint: https://minio-api.radworks.garden 87 | bucket: build-caches 88 | source: radicle-desktop/*/*.{tar.zst} 89 | target: "" 90 | path_style: true 91 | access_key: 92 | from_secret: minio_access_key 93 | secret_key: 94 | from_secret: minio_secret_key 95 | -------------------------------------------------------------------------------- /.woodpecker/unit-test.yaml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: pull_request 3 | - event: push 4 | branch: main 5 | 6 | steps: 7 | unit-tests: 8 | image: docker.io/library/node:22.11.0 9 | entrypoint: 10 | - "/bin/bash" 11 | - "-c" 12 | - | 13 | npm ci 14 | npm run test:unit 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.4.1 2 | 3 | - Fixed a bug in the release pipeline to update the latest artifacts 4 | - Added WSL2 installation instructions to https://radworks.garden 5 | 6 | ### v0.4.0 7 | 8 | - Notification inbox is now accessible from any view without navigating away 9 | - App icon now shows a badge with the unread notification count and polls for updates 10 | - Added global breadcrumbs for easier navigation 11 | - Added quick copy actions for IDs and links to https://app.radicle.xyz 12 | - Syntax highlighting added for diffs in markdown code blocks (` ```diff `) 13 | - Improved dropdown UI — triggers are now visually distinct when active 14 | - Fixed AppImage bug affecting Arch, Fedora, and Red Hat users 15 | - App is now officially available for Nix users via https://radworks.garden 16 | 17 | **Shout-out to contributors 🙏✨** 18 | 19 | - youthlic `did:key:z6MktsSuE4bVYbuTtEPjmhrQWA7dMri7GUg9Qp9o8tRCsmhu` 20 | - tshepang `did:key:z6MkfPSKW7AgQqXSi8fgEJMduHpm9ABmsPYwPhMeF7PssonK` 21 | - geigerzaehler `did:key:z6Mki9XNNHeVRnYS88U59iCBzKUp2xWM3f4zvA3cXuKJFvWF` 22 | - fintohaps `did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM` 23 | - lorenz `did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz` 24 | 25 | 26 | ### v0.3.0 27 | 28 | - New onboarding guide to help users get started faster 29 | - Repo homepage now shows README and project info for quick overview 30 | - UI improvements, including clearer buttons and a new "New Patch" button 31 | - Settings now show version and commit hash for easier support 32 | - Fixed notification count and comment placement bugs 33 | 34 | 35 | ### v0.2.0 36 | 37 | - Added font size controls for enhanced readability and accessibility 38 | - Introduced contextual explainers in key areas to guide users and improve overall user experience 39 | - Enhanced contrast in both dark and light modes to improve legibility and visual clarity 40 | 41 | 42 | ### v0.1.0 43 | 44 | - First public release 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "1" 3 | 4 | members = [ 5 | "crates/radicle-tauri", 6 | "crates/radicle-types", 7 | "crates/test-http-api", 8 | ] 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radicle Desktop 2 | 3 | [![status-badge](https://woodpecker.radworks.garden/api/badges/6/status.svg)](https://woodpecker.radworks.garden/repos/6) 4 | 5 | This desktop application lets you interact with [Radicle][rad], a peer-to-peer code collaboration and publishing stack. 6 | 7 | ## Installation 8 | 9 | **Requirements:** 10 | 11 | * *Linux* or *Unix* based operating system. 12 | * Git 2.34 or later 13 | * OpenSSH 9.1 or later with `ssh-agent` 14 | 15 | ### From binaries 16 | 17 | **Debian** 18 | 19 | Add the following to your `sources.list`: 20 | 21 | ``` 22 | deb [trusted=yes] https://minio-api.radworks.garden/radworks-releases/radicle-desktop/debian unstable main 23 | ``` 24 | 25 | Run from your shell: 26 | 27 | ``` 28 | sudo apt update 29 | sudo apt install radicle-desktop 30 | ``` 31 | 32 | ### From source 33 | 34 | **Prerequisites:** 35 | 36 | - [Node.js][nod] (22.11.0 or higher) and [npm][npm] 37 | - [Rust][rus] toolchain (1.77 or higher) 38 | - [Tauri system dependencies][tau] 39 | 40 | Run the following commands to build the desktop app locally: 41 | 42 | ``` 43 | git clone https://seed.radicle.xyz/z4D5UCArafTzTQpDZNQRuqswh3ury.git radicle-desktop 44 | cd radicle-desktop 45 | npm install 46 | npm run tauri build 47 | ``` 48 | 49 | Then run one of the builds that the script outputs at the end. 50 | 51 | ## Getting in touch 52 | 53 | To get in touch with the maintainers, sign up to our [official chat on Zulip][zul]. 54 | 55 | ## License 56 | 57 | The UI is distributed under the terms of GPLv3. See [LICENSE][lic] for details. 58 | 59 | [lic]: ./LICENSE 60 | [rad]: https://radicle.xyz 61 | [nod]: https://nodejs.org 62 | [npm]: https://www.npmjs.com 63 | [rus]: https://www.rust-lang.org/ 64 | [tau]: https://v2.tauri.app/start/prerequisites/#system-dependencies 65 | [zul]: https://radicle.zulipchat.com/#narrow/stream/444463-desktop 66 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | ### Release Checklist 2 | 3 | **Note:** We release every second Thursday, before the end of the cycle. 4 | 5 | - Bump the minor version in `crates/radicle-tauri/tauri.conf.json`. 6 | - Update `CHANGELOG.md` — only include changes relevant to users. 7 | - Create a version bump patch, push to CI, and request a review. 8 | - The commit message should start with `Release` followed by `v`. 9 | - Wait for CI to pass and get peer approval. 10 | - Build the macOS app locally: `npm run tauri build`. 11 | - Make sure to clean any transient dependencies with `cargo clean && rm -rf node_modules` before building. 12 | - Upload the macOS build to [MinIO][0] in the same folder as the latest Linux build. 13 | - Publish Arch Linux package (See `arch/README.md` for more information) 14 | - Update `pkgver` in `arch/PKGBUILD` to match the release version 15 | - Update `_commit` in `arch/PKGBUILD` to the release commit created above 16 | - Regenerate `.SRCINFO` with 17 | 18 | ```bash 19 | cd arch && ./generate-srcinfo.sh 20 | ``` 21 | 22 | - Create a patch with the changes and wait for CI to pass 23 | 24 | - Create a patch on [radworks-product][1] to [update the download links][2]. 25 | - Once merged, publish the website: `git push github main`. 26 | - Publish the Arch package by pushing changes to the [Arch User Repository][4]: 27 | 28 | ```bash 29 | cd arch && ./publish.sh 30 | ``` 31 | 32 | You need to be a maintainer of the AUR package to push. 33 | - Announce the release on Zulip, following the [previous announcement format][3]. 34 | - Resolve the previous release topic on Zulip. 35 | 36 | [0]: https://minio.radworks.garden/browser/radworks-releases/radicle-desktop%2F 37 | [1]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z4Rxw3J2gX8SJgUYJ3h1KvQCAYoKS 38 | [2]: https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z4Rxw3J2gX8SJgUYJ3h1KvQCAYoKS/commits/17c25b85b123d9766c774cc414e3ffdfc7b69771 39 | [3]: https://radicle.zulipchat.com/#narrow/channel/409174-announcements/topic/radicle-desktop.20v0.2E2.2E0.20.28early.20preview.29/with/514356912 40 | [4]: https://aur.archlinux.org/packages/radicle-desktop 41 | -------------------------------------------------------------------------------- /arch/.gitignore: -------------------------------------------------------------------------------- 1 | radicle-desktop.git 2 | -------------------------------------------------------------------------------- /arch/README.md: -------------------------------------------------------------------------------- 1 | # Packaging for Arch Linux 2 | 3 | This folder contains the `PKGBUILD` script for building the `radicle-desktop` 4 | Arch Linux package. 5 | 6 | ## Arch User Repository 7 | 8 | This folder is mirrored to the Arch User Repository as 9 | [`radicle-desktop`][aur-pkg] so user can install it from there. 10 | 11 | Whenever a change is made to the `radicle-desktop` folder it can be synched to 12 | the AUR using the `./publish.sh ` script. 13 | 14 | Before you push any changes, make sure that the package builds properly, see 15 | “Build QA” below. 16 | 17 | You need to be a maintainer of the `radicle-desktop` to be allowed to push 18 | changes. To become a maintainer ask one of the existing maintainers to add you. 19 | 20 | ## Build QA 21 | 22 | There is a [Woodpecker CI workflow][workflow] to ensure that the package builds 23 | correctly on Arch Linux. 24 | 25 | You can run the workflow locally. 26 | 27 | ```bash 28 | woodpecker-cli \ 29 | exec .woodpecker/build-arch.yaml \ 30 | --repo-path . \ 31 | --volumes paccache:/var/cache/pacman,builder:/home/builder 32 | ``` 33 | 34 | Using `--volumes` is optional and improves caching between multiple runs. 35 | 36 | [aur-pkg]: https://aur.archlinux.org/packages/radicle-desktop 37 | [workflow]: ../.woodpecker/build-arch.yaml 38 | -------------------------------------------------------------------------------- /arch/generate-srcinfo.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | exec docker run \ 4 | --user 1000 \ 5 | --rm --volume "$(pwd)/radicle-desktop:/workdir:ro" \ 6 | --env BUILDDIR=/tmp \ 7 | --env PKGDEST=/tmp \ 8 | --env SRCDEST=/tmp \ 9 | --workdir /workdir \ 10 | archlinux:base-devel makepkg --printsrcinfo > radicle-desktop/.SRCINFO 11 | 12 | -------------------------------------------------------------------------------- /arch/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | push=true 6 | 7 | while [[ $# -gt 0 ]]; do 8 | case "$1" in 9 | --no-push) 10 | push=false 11 | shift 12 | ;; 13 | *) 14 | commit_message="$1" 15 | shift 16 | ;; 17 | esac 18 | done 19 | 20 | if [[ -z "${commit_message:-}" ]]; then 21 | echo "Please provide a commit message as an argument: $0 [--no-push] \"Release vX.Y.Z\"" 22 | exit 1 23 | fi 24 | 25 | if [[ ! -d radicle-desktop.git ]]; then 26 | git clone ssh://aur.archlinux.org/radicle-desktop.git radicle-desktop.git 27 | fi 28 | 29 | pushd radicle-desktop.git >/dev/null 30 | export GIT_PAGER="" 31 | git checkout master 32 | git pull 33 | cp --archive ../radicle-desktop/* ../radicle-desktop/.* . 34 | git diff 35 | git commit --all --message "$commit_message" 36 | if [[ "$push" == true ]]; then 37 | git push 38 | fi 39 | popd >/dev/null 40 | -------------------------------------------------------------------------------- /arch/radicle-desktop/.SRCINFO: -------------------------------------------------------------------------------- 1 | pkgbase = radicle-desktop 2 | pkgdesc = Radicle desktop app 3 | pkgver = 0.4.1 4 | pkgrel = 1 5 | url = https://www.radworks.garden/ 6 | arch = x86_64 7 | license = GPL-3.0-only 8 | makedepends = git 9 | makedepends = openssl 10 | makedepends = appmenu-gtk-module 11 | makedepends = libappindicator-gtk3 12 | makedepends = librsvg 13 | makedepends = rustup 14 | makedepends = npm 15 | makedepends = nodejs 16 | depends = radicle-node 17 | depends = cairo 18 | depends = desktop-file-utils 19 | depends = gdk-pixbuf2 20 | depends = glib2 21 | depends = gtk3 22 | depends = hicolor-icon-theme 23 | depends = libsoup 24 | depends = pango 25 | depends = webkit2gtk-4.1 26 | options = !strip 27 | options = !emptydirs 28 | options = !lto 29 | source = radicle-desktop::git+https://seed.radicle.xyz/z4D5UCArafTzTQpDZNQRuqswh3ury.git#commit=33dd37714b7dbf56a11339a4c6f56f11d8b85351 30 | sha256sums = SKIP 31 | 32 | pkgname = radicle-desktop 33 | -------------------------------------------------------------------------------- /arch/radicle-desktop/.gitignore: -------------------------------------------------------------------------------- 1 | /src 2 | /pkg 3 | /radicle-desktop 4 | -------------------------------------------------------------------------------- /arch/radicle-desktop/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Thomas Scholtes 2 | # 3 | # shellcheck shell=bash disable=SC2034 disable=SC2154 disable=SC2164 4 | 5 | _commit=33dd37714b7dbf56a11339a4c6f56f11d8b85351 6 | pkgname=radicle-desktop 7 | pkgver='0.4.1' 8 | pkgrel='1' 9 | pkgdesc='Radicle desktop app' 10 | arch=('x86_64') 11 | url='https://www.radworks.garden/' 12 | license=('GPL-3.0-only') 13 | depends=( 14 | 'radicle-node' 15 | # See https://v2.tauri.app/distribute/aur/#building-from-source 16 | 'cairo' 17 | 'desktop-file-utils' 18 | 'gdk-pixbuf2' 19 | 'glib2' 20 | 'gtk3' 21 | 'hicolor-icon-theme' 22 | 'libsoup' 23 | 'pango' 24 | 'webkit2gtk-4.1' 25 | ) 26 | makedepends=( 27 | # See https://v2.tauri.app/distribute/aur/#building-from-source 28 | 'git' 29 | 'openssl' 30 | 'appmenu-gtk-module' 31 | 'libappindicator-gtk3' 32 | 'librsvg' 33 | 'rustup' 34 | 'npm' 35 | 'nodejs' 36 | ) 37 | options=('!strip' '!emptydirs' '!lto') 38 | source=("$pkgname::git+https://seed.radicle.xyz/z4D5UCArafTzTQpDZNQRuqswh3ury.git#commit=$_commit") 39 | sha256sums=('SKIP') 40 | 41 | prepare() { 42 | cd "$pkgname" 43 | 44 | npm install 45 | cargo fetch --locked --target "$(rustc -vV | sed -n 's/host: //p')" 46 | } 47 | 48 | build() { 49 | cd "$pkgname" 50 | 51 | npx tauri build --bundles deb 52 | } 53 | 54 | package() { 55 | cp -a $pkgname/target/release/bundle/deb/${pkgname}_${pkgver}_*/data/* "${pkgdir}" 56 | } 57 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/build/.gitkeep -------------------------------------------------------------------------------- /crates/radicle-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /gen/schemas 4 | -------------------------------------------------------------------------------- /crates/radicle-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "radicle-tauri" 3 | version = "0.0.0" 4 | authors = ["Rudolfs Osins ", "Sebastian Martinez "] 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | rust-version = "1.77" 8 | publish = false 9 | 10 | [lib] 11 | name = "app_lib" 12 | crate-type = ["staticlib", "cdylib", "lib"] 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "2.2.0", features = ["isolation"] } 16 | 17 | [dependencies] 18 | anyhow = { version = "1.0.90" } 19 | base64 = { version = "0.22.1" } 20 | either = { version = "1.15" } 21 | log = { version = "0.4.22" } 22 | radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72" } 23 | radicle-types = { version = "0.1.0", path = "../radicle-types" } 24 | radicle-surf = { version = "0.22.1", features = ["serde"] } 25 | serde = { version = "1.0.0", features = ["derive"] } 26 | serde_json = { version = "1.0.0" } 27 | tauri = { version = "2.5.0", features = ["isolation"] } 28 | tauri-plugin-clipboard-manager = { version = "2.2.2" } 29 | tauri-plugin-dialog = { version = "2.2.1" } 30 | tauri-plugin-log = { version = "2.4.0" } 31 | tauri-plugin-shell = { version = "2.2.1" } 32 | tauri-plugin-window-state = { version = "2.2.2" } 33 | thiserror = { version = "2.0.12" } 34 | tokio = { version = "1.45.0", features = ["time"] } 35 | ts-rs = { version = "10.1.0", features = ["serde-json-impl", "no-serde-warnings"] } 36 | ssh-key = { version = "0.6.3" } 37 | zeroize = { version = "1.8.1", features = ["serde"] } 38 | 39 | [features] 40 | # by default Tauri runs in production mode 41 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 42 | default = ["custom-protocol"] 43 | # this feature is used for production builds where `devPath` points to the filesystem 44 | # DO NOT remove this 45 | custom-protocol = ["tauri/custom-protocol"] 46 | -------------------------------------------------------------------------------- /crates/radicle-tauri/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | fn main() { 5 | let head = env::var("GIT_HEAD").unwrap_or_else(|_| { 6 | Command::new("git") 7 | .args(["rev-parse", "--short", "HEAD"]) 8 | .output() 9 | .map(|output| String::from_utf8(output.stdout).expect("output from Git is UTF-8")) 10 | .unwrap_or("unknown".into()) 11 | }); 12 | println!("cargo::rustc-env=GIT_HEAD={head}"); 13 | 14 | tauri_build::build() 15 | } 16 | -------------------------------------------------------------------------------- /crates/radicle-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "clipboard-manager:allow-write-text", 8 | "clipboard-manager:default", 9 | "core:app:default", 10 | "core:event:default", 11 | "core:image:default", 12 | "core:menu:default", 13 | "core:path:default", 14 | "core:resources:default", 15 | "core:tray:default", 16 | "core:webview:default", 17 | "core:window:allow-set-badge-count", 18 | "core:window:default", 19 | "dialog:default", 20 | "log:default", 21 | "shell:allow-open" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/128x128.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/16x16.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/16x16@2x.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/24x24.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/256x256.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/256x256@2x.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/32x32.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/32x32@2x.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/48x48.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/512x512.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/512x512@2x.png -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/icon.icns -------------------------------------------------------------------------------- /crates/radicle-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/crates/radicle-tauri/icons/icon.ico -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod cob; 3 | pub mod diff; 4 | pub mod inbox; 5 | pub mod profile; 6 | pub mod repo; 7 | pub mod startup; 8 | pub mod thread; 9 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/auth.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use radicle::crypto::ssh::{self, Passphrase}; 4 | use radicle::node::Alias; 5 | use radicle::profile::env; 6 | use radicle_types::error::Error; 7 | 8 | use crate::AppState; 9 | 10 | #[tauri::command] 11 | pub fn authenticate( 12 | ctx: tauri::State, 13 | passphrase: Option, 14 | ) -> Result<(), Error> { 15 | let profile = &ctx.profile; 16 | if !profile.keystore.is_encrypted()? { 17 | return Ok(()); 18 | } 19 | match ssh::agent::Agent::connect() { 20 | Ok(mut agent) => { 21 | if agent.request_identities()?.contains(&profile.public_key) { 22 | return Ok(()); 23 | } 24 | 25 | match passphrase { 26 | Some(passphrase) => { 27 | profile.keystore.secret_key(Some(passphrase.clone()))?; 28 | register(&mut agent, profile, passphrase) 29 | } 30 | None => Err(Error::Crypto( 31 | radicle::crypto::ssh::keystore::Error::PassphraseMissing, 32 | )), 33 | } 34 | } 35 | Err(e) if e.is_not_running() => Err(Error::AgentNotRunning)?, 36 | Err(e) => Err(e)?, 37 | } 38 | } 39 | 40 | #[tauri::command] 41 | pub(crate) fn init(alias: String, passphrase: Passphrase) -> Result<(), Error> { 42 | let home = radicle::profile::home()?; 43 | let alias = Alias::from_str(&alias)?; 44 | 45 | if passphrase.is_empty() { 46 | return Err(Error::Crypto( 47 | radicle::crypto::ssh::keystore::Error::PassphraseMissing, 48 | )); 49 | } 50 | let profile = radicle::Profile::init(home, alias, Some(passphrase.clone()), env::seed())?; 51 | match ssh::agent::Agent::connect() { 52 | Ok(mut agent) => register(&mut agent, &profile, passphrase.clone())?, 53 | Err(e) if e.is_not_running() => return Err(Error::AgentNotRunning), 54 | Err(e) => Err(e)?, 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | pub fn register( 61 | agent: &mut ssh::agent::Agent, 62 | profile: &radicle::Profile, 63 | passphrase: ssh::Passphrase, 64 | ) -> Result<(), Error> { 65 | let secret = profile 66 | .keystore 67 | .secret_key(Some(passphrase)) 68 | .map_err(|e| { 69 | if e.is_crypto_err() { 70 | Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( 71 | ssh_key::Error::Crypto, 72 | )) 73 | } else { 74 | e.into() 75 | } 76 | })? 77 | .ok_or(Error::Crypto(radicle::crypto::ssh::keystore::Error::Ssh( 78 | ssh_key::Error::Crypto, 79 | )))?; 80 | 81 | agent.register(&secret)?; 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/cob.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use radicle::git; 4 | use radicle::identity; 5 | use radicle_types as types; 6 | use radicle_types::error::Error; 7 | use radicle_types::traits::thread::Thread; 8 | use tauri_plugin_clipboard_manager::ClipboardExt; 9 | use tauri_plugin_dialog::DialogExt; 10 | 11 | use crate::AppState; 12 | 13 | pub mod issue; 14 | pub mod patch; 15 | 16 | #[tauri::command] 17 | pub async fn get_embed( 18 | ctx: tauri::State<'_, AppState>, 19 | rid: identity::RepoId, 20 | name: Option, 21 | oid: git::Oid, 22 | ) -> Result { 23 | ctx.get_embed(rid, name, oid) 24 | } 25 | 26 | #[tauri::command] 27 | pub async fn save_embed_by_path( 28 | ctx: tauri::State<'_, AppState>, 29 | rid: identity::RepoId, 30 | path: PathBuf, 31 | ) -> Result { 32 | ctx.save_embed_by_path(rid, path) 33 | } 34 | 35 | #[tauri::command] 36 | pub async fn save_embed_by_clipboard( 37 | app_handle: tauri::AppHandle, 38 | ctx: tauri::State<'_, AppState>, 39 | rid: identity::RepoId, 40 | name: String, 41 | ) -> Result { 42 | let content = app_handle 43 | .clipboard() 44 | .read_image() 45 | .map(|i| i.rgba().to_vec())?; 46 | 47 | ctx.save_embed_by_bytes(rid, name, content) 48 | } 49 | 50 | #[tauri::command] 51 | pub async fn save_embed_by_bytes( 52 | ctx: tauri::State<'_, AppState>, 53 | rid: identity::RepoId, 54 | name: String, 55 | bytes: Vec, 56 | ) -> Result { 57 | ctx.save_embed_by_bytes(rid, name, bytes) 58 | } 59 | 60 | #[tauri::command] 61 | pub async fn save_embed_to_disk( 62 | app_handle: tauri::AppHandle, 63 | ctx: tauri::State<'_, AppState>, 64 | rid: identity::RepoId, 65 | oid: git::Oid, 66 | name: String, 67 | ) -> Result<(), Error> { 68 | let Some(path) = app_handle 69 | .dialog() 70 | .file() 71 | .set_file_name(name) 72 | .blocking_save_file() 73 | else { 74 | return Err(Error::SaveEmbedError); 75 | }; 76 | let path = path.into_path()?; 77 | 78 | ctx.save_embed_to_disk(rid, oid, path) 79 | } 80 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/cob/issue.rs: -------------------------------------------------------------------------------- 1 | use radicle::git; 2 | use radicle::identity; 3 | 4 | use radicle::issue::TYPENAME; 5 | use radicle_types as types; 6 | use radicle_types::error::Error; 7 | use radicle_types::traits::cobs::Cobs; 8 | use radicle_types::traits::issue::Issues; 9 | use radicle_types::traits::issue::IssuesMut; 10 | 11 | use crate::AppState; 12 | 13 | #[tauri::command] 14 | pub fn create_issue( 15 | ctx: tauri::State, 16 | rid: identity::RepoId, 17 | new: types::cobs::issue::NewIssue, 18 | opts: types::cobs::CobOptions, 19 | ) -> Result { 20 | ctx.create_issue(rid, new, opts) 21 | } 22 | 23 | #[tauri::command] 24 | pub fn edit_issue( 25 | ctx: tauri::State, 26 | rid: identity::RepoId, 27 | cob_id: git::Oid, 28 | action: types::cobs::issue::Action, 29 | opts: types::cobs::CobOptions, 30 | ) -> Result { 31 | ctx.edit_issue(rid, cob_id, action, opts) 32 | } 33 | 34 | #[tauri::command] 35 | pub(crate) fn list_issues( 36 | ctx: tauri::State, 37 | rid: identity::RepoId, 38 | status: Option, 39 | ) -> Result, Error> { 40 | ctx.list_issues(rid, status) 41 | } 42 | 43 | #[tauri::command] 44 | pub(crate) fn issue_by_id( 45 | ctx: tauri::State, 46 | rid: identity::RepoId, 47 | id: git::Oid, 48 | ) -> Result, Error> { 49 | ctx.issue_by_id(rid, id) 50 | } 51 | 52 | #[tauri::command] 53 | pub(crate) fn comment_threads_by_issue_id( 54 | ctx: tauri::State, 55 | rid: identity::RepoId, 56 | id: git::Oid, 57 | ) -> Result>, Error> { 58 | ctx.comment_threads_by_issue_id(rid, id) 59 | } 60 | 61 | #[tauri::command] 62 | pub fn activity_by_issue( 63 | ctx: tauri::State, 64 | rid: identity::RepoId, 65 | id: git::Oid, 66 | ) -> Result>, Error> { 67 | ctx.activity_by_id(rid, &TYPENAME, id) 68 | } 69 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/diff.rs: -------------------------------------------------------------------------------- 1 | use radicle::identity; 2 | use radicle_types as types; 3 | use radicle_types::error::Error; 4 | use radicle_types::traits::repo::Repo; 5 | 6 | use crate::AppState; 7 | 8 | #[tauri::command] 9 | pub async fn get_diff( 10 | ctx: tauri::State<'_, AppState>, 11 | rid: identity::RepoId, 12 | options: radicle_types::cobs::diff::DiffOptions, 13 | ) -> Result { 14 | ctx.get_diff(rid, options) 15 | } 16 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/profile.rs: -------------------------------------------------------------------------------- 1 | use radicle::node::NodeId; 2 | use radicle_types::config::Config; 3 | use radicle_types::traits::Profile; 4 | 5 | use crate::AppState; 6 | 7 | #[tauri::command] 8 | pub fn config(ctx: tauri::State) -> Config { 9 | ctx.config() 10 | } 11 | 12 | #[tauri::command] 13 | pub fn alias(ctx: tauri::State, nid: NodeId) -> Option { 14 | ctx.alias(nid) 15 | } 16 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/commands/thread.rs: -------------------------------------------------------------------------------- 1 | use radicle::identity; 2 | 3 | use radicle_types as types; 4 | use radicle_types::error::Error; 5 | use radicle_types::traits::thread::Thread; 6 | 7 | use crate::AppState; 8 | 9 | #[tauri::command] 10 | pub fn create_issue_comment( 11 | ctx: tauri::State, 12 | rid: identity::RepoId, 13 | new: types::cobs::thread::NewIssueComment, 14 | opts: types::cobs::CobOptions, 15 | ) -> Result, Error> { 16 | ctx.create_issue_comment(rid, new, opts) 17 | } 18 | 19 | #[tauri::command] 20 | pub fn create_patch_comment( 21 | ctx: tauri::State, 22 | rid: identity::RepoId, 23 | new: types::cobs::thread::NewPatchComment, 24 | opts: types::cobs::CobOptions, 25 | ) -> Result, Error> { 26 | ctx.create_patch_comment(rid, new, opts) 27 | } 28 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod commands; 2 | 3 | use radicle_types::AppState; 4 | 5 | use commands::{auth, cob, diff, inbox, profile, repo, startup, thread}; 6 | 7 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 8 | pub fn run() { 9 | #[cfg(debug_assertions)] 10 | let builder = tauri::Builder::default() 11 | .plugin(tauri_plugin_dialog::init()) 12 | .plugin( 13 | tauri_plugin_log::Builder::new() 14 | .level(log::LevelFilter::Info) 15 | .build(), 16 | ); 17 | #[cfg(not(debug_assertions))] 18 | let builder = tauri::Builder::default(); 19 | 20 | builder 21 | .plugin(tauri_plugin_shell::init()) 22 | .plugin(tauri_plugin_clipboard_manager::init()) 23 | .plugin(tauri_plugin_window_state::Builder::default().build()) 24 | .invoke_handler(tauri::generate_handler![ 25 | auth::authenticate, 26 | auth::init, 27 | cob::get_embed, 28 | cob::issue::activity_by_issue, 29 | cob::issue::comment_threads_by_issue_id, 30 | cob::issue::create_issue, 31 | cob::issue::edit_issue, 32 | cob::issue::issue_by_id, 33 | cob::issue::list_issues, 34 | cob::patch::activity_by_patch, 35 | cob::patch::edit_patch, 36 | cob::patch::list_patches, 37 | cob::patch::patch_by_id, 38 | cob::patch::edit_patch, 39 | cob::patch::review_by_patch_and_revision_and_id, 40 | cob::patch::revisions_by_patch, 41 | cob::patch::revision_by_patch_and_id, 42 | cob::patch::revisions_by_patch, 43 | cob::save_embed_by_bytes, 44 | cob::save_embed_by_clipboard, 45 | cob::save_embed_by_path, 46 | cob::save_embed_to_disk, 47 | diff::get_diff, 48 | inbox::clear_notifications, 49 | inbox::notification_count, 50 | inbox::list_notifications, 51 | profile::alias, 52 | profile::config, 53 | repo::create_repo, 54 | repo::diff_stats, 55 | repo::list_commits, 56 | repo::list_repos, 57 | repo::repo_by_id, 58 | repo::repo_count, 59 | repo::repo_readme, 60 | startup::startup, 61 | startup::version, 62 | startup::check_radicle_cli, 63 | thread::create_issue_comment, 64 | thread::create_patch_comment, 65 | ]) 66 | .run(tauri::generate_context!()) 67 | .expect("error while running tauri application"); 68 | } 69 | -------------------------------------------------------------------------------- /crates/radicle-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | app_lib::run(); 6 | } 7 | -------------------------------------------------------------------------------- /crates/radicle-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Radicle", 3 | "mainBinaryName": "radicle-desktop", 4 | "identifier": "xyz.radicle.desktop", 5 | "version": "0.4.1", 6 | "build": { 7 | "beforeDevCommand": "npm start", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../../build" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Radicle", 16 | "minWidth": 960, 17 | "minHeight": 600 18 | } 19 | ], 20 | "security": { 21 | "csp": { 22 | "default-src": "'self'", 23 | "connect-src": "ipc: http://ipc.localhost", 24 | "img-src": "'self' blob: data: https:", 25 | "style-src": "'unsafe-inline' 'self'" 26 | }, 27 | "pattern": { 28 | "use": "isolation", 29 | "options": { 30 | "dir": "../../isolation" 31 | } 32 | } 33 | } 34 | }, 35 | "bundle": { 36 | "linux": { 37 | "deb": { 38 | "depends": [ 39 | "radicle" 40 | ] 41 | } 42 | }, 43 | "active": true, 44 | "targets": "all", 45 | "icon": [ 46 | "icons/16x16.png", 47 | "icons/16x16@2x.png", 48 | "icons/24x24.png", 49 | "icons/32x32.png", 50 | "icons/32x32@2x.png", 51 | "icons/48x48.png", 52 | "icons/128x128.png", 53 | "icons/128x128@2x.png", 54 | "icons/256x256.png", 55 | "icons/256x256@2x.png", 56 | "icons/512x512.png", 57 | "icons/512x512@2x.png", 58 | "icons/icon.icns", 59 | "icons/icon.ico" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/radicle-tauri/tauri.linux.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "radicle-desktop" 3 | } 4 | -------------------------------------------------------------------------------- /crates/radicle-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "radicle-types" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = { version = "1.0.90" } 8 | axum = { version = "0.8.1", default-features = false, features = ["json"] } 9 | base64 = { version = "0.22.1" } 10 | localtime = { version = "1.3.1" } 11 | log = { version = "0.4.22" } 12 | infer = { version = "0.19.0" } 13 | mime-infer = { version = "3.0.0" } 14 | radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72", features = ["test"] } 15 | radicle-surf = { version = "0.22.1", features = ["serde"] } 16 | serde = { version = "1.0.0", features = ["derive"] } 17 | serde_json = { version = "1.0.0" } 18 | sqlite = { version = "0.32.0", features = ["bundled"] } 19 | ssh-key = { version = "0.6.3" } 20 | tauri-plugin-clipboard-manager = { version = "2.2.2" } 21 | tauri-plugin-fs = { version = "2.2.0" } 22 | tempfile = { version = "3.19.0" } 23 | thiserror = { version = "2.0.12" } 24 | tree-sitter-bash = { version = "0.23.3" } 25 | tree-sitter-c = { version = "0.23.2" } 26 | tree-sitter-css = { version = "0.23.1" } 27 | tree-sitter-diff = { version = "0.1.0" } 28 | tree-sitter-go = { version = "0.23.4" } 29 | tree-sitter-highlight = { version = "0.25.3" } 30 | tree-sitter-html = { version = "0.23.2" } 31 | tree-sitter-javascript = { version = "0.23.1" } 32 | tree-sitter-jsdoc = { version = "0.23.2" } 33 | tree-sitter-json = { version = "0.24.8" } 34 | tree-sitter-md = { version = "0.3.2" } 35 | tree-sitter-python = { version = "0.23.4" } 36 | tree-sitter-regex = { version = "0.24.3" } 37 | tree-sitter-ruby = { version = "0.23.1" } 38 | tree-sitter-rust = { version = "0.23.2" } 39 | tree-sitter-svelte-ng = { version = "1.0.2" } 40 | tree-sitter-toml-ng = { version = "0.7.0" } 41 | tree-sitter-typescript = { version = "0.23.2" } 42 | ts-rs = { version = "10.1.0", features = ["serde-json-impl", "no-serde-warnings", "format"] } 43 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/Author.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Author = { did: string; alias?: string }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/CobOptions.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type CobOptions = { announce?: boolean }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/DiffOptions.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type DiffOptions = { 4 | base: string; 5 | head: string; 6 | unified: number | null; 7 | highlight: boolean | null; 8 | }; 9 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/Embed.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Embed = { content: Array; mimeType: string | null }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/EmbedWithMimeType.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type EmbedWithMimeType = { 4 | content: Array; 5 | mimeType: string | null; 6 | }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/Never.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | /** 4 | * A type alias for the TS type `never`. 5 | */ 6 | export type Never = never; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/Operation.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "./Author"; 3 | 4 | /** 5 | * Everything that can be done in the system is represented by an `Op`. 6 | * Operations are applied to an accumulator to yield a final state. 7 | */ 8 | export type Operation = { 9 | id: string; 10 | actions: Array; 11 | author: Author; 12 | timestamp: number; 13 | }; 14 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/PaginatedQuery.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type PaginatedQuery = { cursor: number; more: boolean; content: T }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/Reaction.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "./Author"; 3 | import type { CodeLocation } from "./thread/CodeLocation"; 4 | 5 | export type Reaction = { 6 | emoji: string; 7 | authors: Array; 8 | location?: CodeLocation; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/ActionWithAuthor.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | 4 | export type ActionWithAuthor = { 5 | oid: string; 6 | timestamp: number; 7 | author: Author; 8 | } & T; 9 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/Issue.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Action } from "../issue/Action"; 3 | import type { ActionWithAuthor } from "./ActionWithAuthor"; 4 | import type { RefUpdate } from "./RefUpdate"; 5 | import type { State } from "../issue/State"; 6 | 7 | export type Issue = { 8 | rowId: string; 9 | id: string; 10 | update: RefUpdate; 11 | title: string; 12 | timestamp: number; 13 | status: State; 14 | actions: Array>; 15 | repoId: string; 16 | }; 17 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/NotificationCount.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type NotificationCount = { rid: string; name: string; count: number }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/NotificationItem.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Issue } from "./Issue"; 3 | import type { Patch } from "./Patch"; 4 | 5 | export type NotificationItem = 6 | | { "type": "issue" } & Issue 7 | | { "type": "patch" } & Patch; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/NotificationsByRepo.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { NotificationItem } from "./NotificationItem"; 3 | 4 | export type NotificationsByRepo = { 5 | rid: string; 6 | name: string; 7 | notifications: Array>; 8 | count: number; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/Patch.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Action } from "../patch/Action"; 3 | import type { ActionWithAuthor } from "./ActionWithAuthor"; 4 | import type { RefUpdate } from "./RefUpdate"; 5 | import type { State } from "../patch/State"; 6 | 7 | export type Patch = { 8 | rowId: string; 9 | id: string; 10 | update: RefUpdate; 11 | timestamp: number; 12 | title: string; 13 | status: State; 14 | actions: Array>; 15 | repoId: string; 16 | }; 17 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/RefUpdate.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type RefUpdate = 4 | | { "type": "updated"; name: string; old: string; new: string } 5 | | { "type": "created"; name: string; oid: string } 6 | | { "type": "deleted"; name: string; oid: string } 7 | | { "type": "skipped"; name: string; oid: string }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/SetStatusNotifications.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type SetStatusNotifications = 4 | | { "type": "ids"; "content": Array } 5 | | { "type": "repo"; "content": string } 6 | | { "type": "all" }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/inbox/TypedId.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | /** 4 | * The exact identifier for a particular COB. 5 | */ 6 | export type TypedId = { id: string; typeName: string }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/issue/Action.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { Embed } from "../thread/Embed"; 4 | import type { State } from "./State"; 5 | 6 | export type Action = 7 | | { "type": "assign"; assignees: Array } 8 | | { "type": "edit"; title: string } 9 | | { "type": "lifecycle"; state: State } 10 | | { "type": "label"; labels: Array } 11 | | { "type": "comment"; body: string; replyTo?: string; embeds?: Array } 12 | | { "type": "comment.edit"; id: string; body: string; embeds?: Array } 13 | | { "type": "comment.redact"; id: string } 14 | | { "type": "comment.react"; id: string; reaction: string; active: boolean }; 15 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/issue/CloseReason.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type CloseReason = "other" | "solved"; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/issue/Issue.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { Comment } from "../thread/Comment"; 4 | import type { Never } from "../Never"; 5 | import type { State } from "./State"; 6 | 7 | export type Issue = { 8 | id: string; 9 | author: Author; 10 | title: string; 11 | state: State; 12 | assignees: Array; 13 | body: Comment; 14 | commentCount: number; 15 | labels: Array; 16 | timestamp: number; 17 | }; 18 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/issue/NewIssue.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Embed } from "../thread/Embed"; 3 | 4 | export type NewIssue = { 5 | title: string; 6 | description: string; 7 | labels?: Array; 8 | assignees?: Array; 9 | embeds?: Array; 10 | }; 11 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/issue/State.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { CloseReason } from "./CloseReason"; 3 | 4 | export type State = { "status": "closed"; reason: CloseReason } | { 5 | "status": "open"; 6 | }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Action.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { CodeLocation } from "../thread/CodeLocation"; 4 | import type { Embed } from "../thread/Embed"; 5 | import type { Verdict } from "./Verdict"; 6 | 7 | export type Action = 8 | | { "type": "edit"; title: string; target: string } 9 | | { "type": "label"; labels: Array } 10 | | { "type": "lifecycle"; state: { status: "draft" | "open" | "archived" } } 11 | | { "type": "assign"; assignees: Array } 12 | | { "type": "merge"; revision: string; commit: string } 13 | | { 14 | "type": "review"; 15 | revision: string; 16 | summary?: string; 17 | verdict?: Verdict; 18 | labels?: Array; 19 | } 20 | | { 21 | "type": "review.edit"; 22 | review: string; 23 | summary?: string; 24 | verdict?: Verdict; 25 | labels?: Array; 26 | } 27 | | { "type": "review.redact"; review: string } 28 | | { 29 | "type": "review.comment"; 30 | review: string; 31 | body: string; 32 | location?: CodeLocation; 33 | replyTo?: string; 34 | embeds?: Array; 35 | } 36 | | { 37 | "type": "review.comment.edit"; 38 | review: string; 39 | comment: string; 40 | body: string; 41 | embeds?: Array; 42 | } 43 | | { "type": "review.comment.redact"; review: string; comment: string } 44 | | { 45 | "type": "review.comment.react"; 46 | review: string; 47 | comment: string; 48 | reaction: string; 49 | active: boolean; 50 | } 51 | | { "type": "review.comment.resolve"; review: string; comment: string } 52 | | { "type": "review.comment.unresolve"; review: string; comment: string } 53 | | { 54 | "type": "revision"; 55 | description: string; 56 | base: string; 57 | oid: string; 58 | resolves?: Array<[string, string]>; 59 | } 60 | | { 61 | "type": "revision.edit"; 62 | revision: string; 63 | description: string; 64 | embeds?: Array; 65 | } 66 | | { 67 | "type": "revision.react"; 68 | revision: string; 69 | location?: CodeLocation; 70 | reaction: string; 71 | active: boolean; 72 | } 73 | | { "type": "revision.redact"; revision: string } 74 | | { 75 | "type": "revision.comment"; 76 | revision: string; 77 | location?: CodeLocation; 78 | body: string; 79 | replyTo?: string; 80 | embeds?: Array; 81 | } 82 | | { 83 | "type": "revision.comment.edit"; 84 | revision: string; 85 | comment: string; 86 | body: string; 87 | embeds?: Array; 88 | } 89 | | { "type": "revision.comment.redact"; revision: string; comment: string } 90 | | { 91 | "type": "revision.comment.react"; 92 | revision: string; 93 | comment: string; 94 | reaction: string; 95 | active: boolean; 96 | }; 97 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Edit.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { Embed } from "../thread/Embed"; 4 | 5 | export type Edit = { 6 | author: Author; 7 | timestamp: number; 8 | body: string; 9 | embeds?: Array; 10 | }; 11 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Patch.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { State } from "./State"; 4 | 5 | export type Patch = { 6 | id: string; 7 | author: Author; 8 | title: string; 9 | base: string; 10 | head: string; 11 | state: State; 12 | assignees: Array; 13 | labels: Array; 14 | timestamp: number; 15 | revisionCount: number; 16 | }; 17 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/PatchCounts.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type PatchCounts = { 4 | open: number; 5 | draft: number; 6 | archived: number; 7 | merged: number; 8 | }; 9 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Review.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { CodeLocation } from "../thread/CodeLocation"; 4 | import type { Comment } from "../thread/Comment"; 5 | import type { Verdict } from "./Verdict"; 6 | 7 | export type Review = { 8 | id: string; 9 | author: Author; 10 | verdict?: Verdict; 11 | summary?: string; 12 | comments: Array>; 13 | timestamp: number; 14 | labels: Array; 15 | }; 16 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/ReviewEdit.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Verdict } from "./Verdict"; 3 | 4 | export type ReviewEdit = { 5 | reviewId: string; 6 | verdict?: Verdict; 7 | summary?: string; 8 | labels?: Array; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Revision.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { CodeLocation } from "../thread/CodeLocation"; 4 | import type { Comment } from "../thread/Comment"; 5 | import type { Edit } from "./Edit"; 6 | import type { Reaction } from "../Reaction"; 7 | import type { Review } from "./Review"; 8 | 9 | export type Revision = { 10 | id: string; 11 | author: Author; 12 | description: Array; 13 | base: string; 14 | head: string; 15 | reviews?: Array; 16 | timestamp: number; 17 | discussion?: Array>; 18 | reactions?: Array; 19 | }; 20 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/State.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type State = 4 | | { "status": "draft" } 5 | | { "status": "open"; conflicts?: Array<[string, string]> } 6 | | { "status": "archived" } 7 | | { "status": "merged"; revision: string; commit: string }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/patch/Verdict.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Verdict = "accept" | "reject"; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/CodeLocation.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { CodeRange } from "./CodeRange"; 3 | 4 | export type CodeLocation = { 5 | commit: string; 6 | path: string; 7 | old: CodeRange | null; 8 | new: CodeRange | null; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/CodeRange.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type CodeRange = { 4 | "type": "lines"; 5 | range: { start: number; end: number }; 6 | } | { "type": "chars"; line: number; range: { start: number; end: number } }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/Comment.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../Author"; 3 | import type { Edit } from "../patch/Edit"; 4 | import type { Embed } from "./Embed"; 5 | import type { Never } from "../Never"; 6 | import type { Reaction } from "../Reaction"; 7 | 8 | export type Comment = { 9 | id: string; 10 | author: Author; 11 | edits: Array; 12 | reactions: Array; 13 | replyTo: string | null; 14 | location: T | null; 15 | embeds?: Array; 16 | resolved: boolean; 17 | }; 18 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/CreateReviewComment.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { CodeLocation } from "./CodeLocation"; 3 | import type { Embed } from "./Embed"; 4 | 5 | export type CreateReviewComment = { 6 | reviewId: string; 7 | body: string; 8 | replyTo?: string; 9 | location?: CodeLocation; 10 | embeds?: Array; 11 | }; 12 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/Embed.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Embed = { name: string; content: string }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/NewIssueComment.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Embed } from "./Embed"; 3 | 4 | export type NewIssueComment = { 5 | id: string; 6 | body: string; 7 | replyTo?: string; 8 | embeds?: Array; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/NewPatchComment.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { CodeLocation } from "./CodeLocation"; 3 | import type { Embed } from "./Embed"; 4 | 5 | export type NewPatchComment = { 6 | id: string; 7 | revision: string; 8 | body: string; 9 | replyTo?: string; 10 | location?: CodeLocation; 11 | embeds?: Array; 12 | }; 13 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/cob/thread/Thread.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Comment } from "./Comment"; 3 | import type { Never } from "../Never"; 4 | 5 | export type Thread = { 6 | root: Comment; 7 | replies: Array>; 8 | }; 9 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/config/Config.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | /** 4 | * Service configuration. 5 | */ 6 | export type Config = { 7 | /** 8 | * Node Public Key in NID format. 9 | */ 10 | publicKey: string; 11 | /** 12 | * Node alias. 13 | */ 14 | alias: string; 15 | /** 16 | * Default seeding policy. 17 | */ 18 | seedingPolicy: { default: "allow"; scope: "followed" | "all" } | { 19 | default: "block"; 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Added.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { DiffContent } from "./DiffContent"; 3 | import type { DiffFile } from "./DiffFile"; 4 | 5 | export type Added = { path: string; diff: DiffContent; new: DiffFile }; 6 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Addition.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Line } from "../syntax/Line"; 3 | 4 | export type Addition = { line: string; lineNo: number; highlight: Line | null }; 5 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Copied.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { DiffContent } from "./DiffContent"; 3 | import type { DiffFile } from "./DiffFile"; 4 | 5 | export type Copied = { 6 | oldPath: string; 7 | newPath: string; 8 | old: DiffFile; 9 | new: DiffFile; 10 | diff: DiffContent; 11 | }; 12 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Deleted.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { DiffContent } from "./DiffContent"; 3 | import type { DiffFile } from "./DiffFile"; 4 | 5 | export type Deleted = { path: string; diff: DiffContent; old: DiffFile }; 6 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Deletion.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Line } from "../syntax/Line"; 3 | 4 | export type Deletion = { line: string; lineNo: number; highlight: Line | null }; 5 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Diff.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { FileDiff } from "./FileDiff"; 3 | import type { Stats } from "./Stats"; 4 | 5 | export type Diff = { files: Array; stats: Stats }; 6 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/DiffContent.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { EofNewLine } from "./EofNewLine"; 3 | import type { FileStats } from "./FileStats"; 4 | import type { Hunks } from "./Hunks"; 5 | 6 | export type DiffContent = { "type": "binary" } | { 7 | "type": "plain"; 8 | hunks: Hunks; 9 | stats: FileStats; 10 | eof: EofNewLine; 11 | } | { "type": "empty" }; 12 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/DiffFile.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { FileMode } from "./FileMode"; 3 | 4 | export type DiffFile = { oid: string; mode: FileMode }; 5 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/EofNewLine.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type EofNewLine = 4 | | "oldMissing" 5 | | "newMissing" 6 | | "bothMissing" 7 | | "noneMissing"; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/FileDiff.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Added } from "./Added"; 3 | import type { Copied } from "./Copied"; 4 | import type { Deleted } from "./Deleted"; 5 | import type { Modified } from "./Modified"; 6 | import type { Moved } from "./Moved"; 7 | 8 | export type FileDiff = 9 | | { "status": "added" } & Added 10 | | { "status": "deleted" } & Deleted 11 | | { "status": "modified" } & Modified 12 | | { "status": "moved" } & Moved 13 | | { "status": "copied" } & Copied; 14 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/FileMode.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type FileMode = "blob" | "blobExecutable" | "tree" | "link" | "commit"; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/FileStats.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type FileStats = { additions: number; deletions: number }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Hunk.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Modification } from "./Modification"; 3 | 4 | export type Hunk = { 5 | header: string; 6 | lines: Array; 7 | old: { start: number; end: number }; 8 | new: { start: number; end: number }; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Hunks.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Hunk } from "./Hunk"; 3 | 4 | export type Hunks = Array; 5 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Line.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Line = Array; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Modification.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Addition } from "./Addition"; 3 | import type { Deletion } from "./Deletion"; 4 | import type { Line } from "../syntax/Line"; 5 | 6 | export type Modification = 7 | | { "type": "addition" } & Addition 8 | | { "type": "deletion" } & Deletion 9 | | { 10 | "type": "context"; 11 | line: string; 12 | lineNoOld: number; 13 | lineNoNew: number; 14 | highlight: Line | null; 15 | }; 16 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Modified.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { DiffContent } from "./DiffContent"; 3 | import type { DiffFile } from "./DiffFile"; 4 | 5 | export type Modified = { 6 | path: string; 7 | diff: DiffContent; 8 | old: DiffFile; 9 | new: DiffFile; 10 | }; 11 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Moved.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { DiffContent } from "./DiffContent"; 3 | import type { DiffFile } from "./DiffFile"; 4 | 5 | export type Moved = { 6 | oldPath: string; 7 | old: DiffFile; 8 | newPath: string; 9 | new: DiffFile; 10 | diff: DiffContent; 11 | }; 12 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/diff/Stats.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Stats = { 4 | filesChanged: number; 5 | insertions: number; 6 | deletions: number; 7 | }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/error/ErrorWrapper.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type ErrorWrapper = { code: string; message?: string }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/Commit.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Commit = { 4 | id: string; 5 | author: { name: string; email: string; time: number }; 6 | committer: { name: string; email: string; time: number }; 7 | message: string; 8 | summary: string; 9 | parents: Array; 10 | }; 11 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/ProjectPayload.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { ProjectPayloadData } from "./ProjectPayloadData"; 3 | import type { ProjectPayloadMeta } from "./ProjectPayloadMeta"; 4 | 5 | export type ProjectPayload = { 6 | data: ProjectPayloadData; 7 | meta: ProjectPayloadMeta; 8 | }; 9 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/ProjectPayloadData.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type ProjectPayloadData = { 4 | defaultBranch: string; 5 | description: string; 6 | name: string; 7 | }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/ProjectPayloadMeta.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type ProjectPayloadMeta = { 4 | head: string; 5 | issues: { open: number; closed: number }; 6 | patches: { open: number; draft: number; archived: number; merged: number }; 7 | }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/Readme.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type Readme = { path: string; content: string; binary: boolean }; 4 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/RepoCount.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | export type RepoCount = { 4 | total: number; 5 | contributor: number; 6 | delegate: number; 7 | private: number; 8 | seeding: number; 9 | }; 10 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/RepoInfo.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../cob/Author"; 3 | import type { SupportedPayloads } from "./SupportedPayloads"; 4 | import type { Visibility } from "./Visibility"; 5 | 6 | export type RepoInfo = { 7 | payloads: SupportedPayloads; 8 | delegates: Array; 9 | threshold: number; 10 | visibility: Visibility; 11 | rid: string; 12 | seeding: number; 13 | lastCommitTimestamp: number; 14 | }; 15 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/SupportedPayloads.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { ProjectPayload } from "./ProjectPayload"; 3 | 4 | export type SupportedPayloads = { "xyz.radicle.project"?: ProjectPayload }; 5 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/SyncStatus.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { SyncedAt } from "./SyncedAt"; 3 | 4 | export type SyncStatus = { 5 | "status": "synced"; 6 | /** 7 | * At what ref was the remote synced at. 8 | */ 9 | at: SyncedAt; 10 | } | { 11 | "status": "outOfSync"; 12 | /** 13 | * Local head of our `rad/sigrefs`. 14 | */ 15 | local: SyncedAt; 16 | /** 17 | * Remote head of our `rad/sigrefs`. 18 | */ 19 | remote: SyncedAt; 20 | }; 21 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/SyncedAt.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | 3 | /** 4 | * Holds an oid and timestamp. 5 | */ 6 | export type SyncedAt = { oid: string; timestamp: number }; 7 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/repo/Visibility.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Author } from "../cob/Author"; 3 | 4 | export type Visibility = { "type": "public" } | { 5 | "type": "private"; 6 | allow?: Array; 7 | }; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/syntax/Label.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Paint } from "./Paint"; 3 | 4 | /** 5 | * A styled string that does not contain any `'\n'`. 6 | */ 7 | export type Label = Paint; 8 | -------------------------------------------------------------------------------- /crates/radicle-types/bindings/syntax/Line.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. 2 | import type { Label } from "./Label"; 3 | 4 | /** 5 | * A line of text that has styling and can be displayed. 6 | */ 7 | export type Line = { items: Array(a) { 29 | let x = B::from_radicle_action(r, &aliases); 30 | Some(x) 31 | } else { 32 | log::error!("Not able to deserialize the action"); 33 | 34 | None 35 | } 36 | }) 37 | .collect::>(); 38 | 39 | Some(crate::cobs::Operation { 40 | id: op.id, 41 | actions, 42 | author: crate::cobs::Author::new(&op.author.into(), &aliases), 43 | timestamp: op.timestamp, 44 | }) 45 | }) 46 | .collect::>(); 47 | 48 | Ok::<_, Error>(ops) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/test-http-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-http-api" 3 | description = "HTTP Test API" 4 | homepage = "https://radicle.xyz" 5 | version = "0.1.0" 6 | edition = "2021" 7 | 8 | [dependencies] 9 | anyhow = { version = "1.0.90" } 10 | axum = { version = "0.8.1", default-features = false, features = ["json", "query", "tokio", "http1"] } 11 | hyper = { version = "1.6", default-features = false } 12 | lexopt = { version = "0.3.0" } 13 | radicle = { git = "https://ash.radicle.garden/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git", package = "radicle", rev = "7c902b6905724345ba850eb6cca8f8becc9a9c72" } 14 | radicle-surf = { version = "0.22.1", default-features = false, features = ["serde"] } 15 | radicle-types = { path = "../radicle-types" } 16 | serde = { version = "1.0.0", features = ["derive"] } 17 | serde_json = { version = "1.0.0", features = ["preserve_order"] } 18 | thiserror = { version = "2.0.12" } 19 | tokio = { version = "1.45", default-features = false, features = ["macros", "rt-multi-thread"] } 20 | tower-http = { version = "0.6.2", default-features = false, features = ["cors", "set-header"] } 21 | -------------------------------------------------------------------------------- /crates/test-http-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::Arc; 3 | 4 | use axum::Router; 5 | use tokio::net::TcpListener; 6 | 7 | use radicle::cob::cache::COBS_DB_FILE; 8 | use radicle::Profile; 9 | 10 | use radicle_types::domain::patch::service::Service as PatchService; 11 | 12 | mod api; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Options { 16 | pub listen: SocketAddr, 17 | } 18 | 19 | pub async fn run(options: Options) -> anyhow::Result<()> { 20 | let profile = Profile::load()?; 21 | let listener = TcpListener::bind(options.listen).await?; 22 | let app = router(profile)?.into_make_service_with_connect_info::(); 23 | 24 | axum::serve(listener, app) 25 | .await 26 | .map_err(anyhow::Error::from) 27 | } 28 | 29 | fn router(profile: Profile) -> anyhow::Result { 30 | let profile = Arc::new(profile); 31 | 32 | let patch_db = 33 | radicle_types::outbound::sqlite::Sqlite::reader(profile.cobs().join(COBS_DB_FILE))?; 34 | let patch_service = PatchService::new(patch_db); 35 | 36 | let ctx = api::Context::new(profile, Arc::new(patch_service)); 37 | 38 | Ok(api::router(ctx)) 39 | } 40 | -------------------------------------------------------------------------------- /crates/test-http-api/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use test_http_api as api; 4 | 5 | #[tokio::main] 6 | async fn main() -> anyhow::Result<()> { 7 | let options = parse_options()?; 8 | match api::run(options).await { 9 | Ok(()) => {} 10 | Err(_) => { 11 | process::exit(1); 12 | } 13 | } 14 | Ok(()) 15 | } 16 | 17 | fn parse_options() -> Result { 18 | use lexopt::prelude::*; 19 | 20 | let mut parser = lexopt::Parser::from_env(); 21 | let mut listen = None; 22 | 23 | while let Some(arg) = parser.next()? { 24 | match arg { 25 | Long("listen") => { 26 | let addr = parser.value()?.parse()?; 27 | listen = Some(addr); 28 | } 29 | _ => return Err(arg.unexpected()), 30 | } 31 | } 32 | Ok(api::Options { 33 | listen: listen.unwrap_or_else(|| ([0, 0, 0, 0], 8081).into()), 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Radicle Desktop App"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | heartwood = { 11 | url = "git+https://seed.radicle.xyz/z3gqcJUoA1n9HaHKufZs5FCSGazv5.git?ref=refs/namespaces/z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/refs/tags/v1.1.0"; 12 | }; 13 | }; 14 | 15 | outputs = { 16 | self, 17 | nixpkgs, 18 | flake-utils, 19 | rust-overlay, 20 | heartwood, 21 | ... 22 | }: 23 | (flake-utils.lib.eachDefaultSystem (system: let 24 | pkgs = import nixpkgs { 25 | inherit system; 26 | overlays = [ 27 | (import rust-overlay) 28 | ]; 29 | }; 30 | in { 31 | 32 | checks = { 33 | radicle-desktop = self.packages.${system}.radicle-desktop.overrideAttrs ({ doCheck = true; }); 34 | }; 35 | 36 | devShells.default = pkgs.mkShell { 37 | name = "radicle-desktop-env"; 38 | inputsFrom = [ self.checks.${system}.radicle-desktop ]; 39 | nativeBuildInputs = with pkgs; [ 40 | cargo-watch 41 | cargo-nextest 42 | ripgrep 43 | rust-analyzer 44 | ]; 45 | env = self.checks.${system}.radicle-desktop.env // { 46 | }; 47 | }; 48 | 49 | packages = { 50 | default = self.packages.${system}.radicle-desktop; 51 | twemoji-assets = pkgs.fetchFromGitHub { 52 | owner = "twitter"; 53 | repo = "twemoji"; 54 | rev = "v14.0.2"; 55 | hash = "sha256-YoOnZ5uVukzi/6bLi22Y8U5TpplPzB7ji42l+/ys5xI="; 56 | }; 57 | 58 | radicle-desktop = pkgs.callPackage ./nix/radicle-desktop.nix { 59 | inherit heartwood; 60 | inherit (self.packages.${system}) twemoji-assets; 61 | } 62 | // (if self ? rev || self ? dirtyRev then { 63 | GIT_HEAD = if self ? rev then self.rev else self.dirtyRev; 64 | } else {}); 65 | }; 66 | })); 67 | } 68 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | Radicle 14 | 20 | 26 | 32 | 38 | 44 | 50 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /isolation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Isolation Secure Script 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /isolation/index.js: -------------------------------------------------------------------------------- 1 | window.__TAURI_ISOLATION_HOOK__ = payload => { 2 | return payload; 3 | }; 4 | -------------------------------------------------------------------------------- /nix/radicle-desktop.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | importNpmLock, 4 | rust-bin, 5 | makeRustPlatform, 6 | cargo-tauri, 7 | nodejs, 8 | pkg-config, 9 | wrapGAppsHook4, 10 | glib, 11 | gtk3, 12 | libsoup_3, 13 | openssl, 14 | webkitgtk_4_1, 15 | git, 16 | openssh, 17 | system, 18 | playwright-driver, 19 | heartwood, 20 | twemoji-assets, 21 | GIT_HEAD ? null, 22 | }: 23 | let 24 | rTc = rust-bin.fromRustupToolchainFile ./../rust-toolchain; 25 | rustPlatform = makeRustPlatform { 26 | cargo = rTc; 27 | rustc = rTc; 28 | }; 29 | in 30 | rustPlatform.buildRustPackage rec { 31 | pname = "radicle-desktop"; 32 | inherit (with builtins; (fromJSON (readFile ./../package.json))) version; 33 | 34 | src = ./..; 35 | 36 | cargoDeps = rustPlatform.importCargoLock { 37 | lockFile = ./../Cargo.lock; 38 | outputHashes = { 39 | "radicle-0.14.0" = "sha256-F7pJ+yLhlRXg03A+pNXwsqNSOG3qJs6bEO9YUUXs4f0="; 40 | }; 41 | }; 42 | 43 | npmDeps = importNpmLock { 44 | inherit version; 45 | pname = pname + "-npm-deps"; 46 | npmRoot = ./..; 47 | }; 48 | 49 | nativeBuildInputs = [ 50 | cargo-tauri.hook 51 | nodejs 52 | importNpmLock.npmConfigHook 53 | pkg-config 54 | wrapGAppsHook4 55 | ]; 56 | 57 | buildInputs = [ 58 | glib 59 | gtk3 60 | libsoup_3 61 | openssl 62 | webkitgtk_4_1 63 | ]; 64 | 65 | postPatch = '' 66 | patchShebangs scripts/copy-katex-assets scripts/check-js scripts/check-rs 67 | mkdir -p public/twemoji 68 | cp -t public/twemoji -r -- ${twemoji-assets}/assets/svg/* 69 | : >scripts/install-twemoji-assets 70 | ''; 71 | 72 | doCheck = false; 73 | nativeCheckInputs = [ 74 | git 75 | openssh 76 | ]; 77 | 78 | env = 79 | { 80 | HW_RELEASE = "nix-" + (heartwood.shortRev or "unknown-ref"); 81 | PLAYWRIGHT_BROWSERS_PATH = playwright-driver.browsers; 82 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = 1; 83 | PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true; 84 | RUST_SRC_PATH = "${rTc}/lib/rustlib/src/rust/library"; 85 | } 86 | // (lib.optionalAttrs (GIT_HEAD != null) { 87 | inherit GIT_HEAD; 88 | }); 89 | 90 | preCheck = '' 91 | export RAD_HOME="$PWD/_rad-home" 92 | export RAD_PASSPHRASE="" 93 | rad auth --alias test 94 | bins="tests/tmp/bin/heartwood/$HW_RELEASE" 95 | mkdir -p "$bins" 96 | cp -t "$bins" -- ${heartwood.packages.${system}.radicle}/bin/* 97 | printf "$HW_RELEASE" >tests/support/heartwood-release 98 | ''; 99 | 100 | checkPhase = '' 101 | npm run build:http 102 | npm run test:unit 103 | scripts/check-js 104 | scripts/check-rs 105 | ''; 106 | 107 | passthru.env = env; 108 | meta = { 109 | description = "Radicle Desktop App"; 110 | license = lib.licenses.gpl3; 111 | maintainers = [ ]; 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | import { devices } from "@playwright/test"; 3 | 4 | const config: PlaywrightTestConfig = { 5 | outputDir: "./tests/artifacts", 6 | testDir: "./tests/e2e", 7 | globalSetup: "./tests/support/globalSetup.ts", 8 | timeout: 30_000, 9 | expect: { 10 | timeout: 8000, 11 | }, 12 | fullyParallel: true, 13 | workers: process.env.CI ? 1 : undefined, 14 | forbidOnly: !!process.env.CI, 15 | retries: process.env.CI ? 2 : 0, 16 | reporter: "list", 17 | use: { 18 | colorScheme: "dark", 19 | actionTimeout: 5000, 20 | baseURL: "http://localhost:3001", 21 | trace: "retain-on-failure", 22 | }, 23 | 24 | projects: [ 25 | { 26 | name: "webkit", 27 | use: { 28 | ...devices["Desktop Safari"], 29 | }, 30 | }, 31 | ], 32 | 33 | webServer: [ 34 | { 35 | command: 36 | "VITE_AUTH_LONG_DELAY=1000 npm run start -- --strictPort --port 3001", 37 | port: 3001, 38 | }, 39 | ], 40 | }; 41 | 42 | export default config; 43 | -------------------------------------------------------------------------------- /public/fonts/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/Inter-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/Inter-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/Inter-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/Inter-SemiBold.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/JetBrainsMono-Bold.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/JetBrainsMono-Medium.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/JetBrainsMono-Regular.woff2 -------------------------------------------------------------------------------- /public/fonts/JetBrainsMono-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/fonts/JetBrainsMono-SemiBold.woff2 -------------------------------------------------------------------------------- /public/syntax.css: -------------------------------------------------------------------------------- 1 | .global-syntax.operator, 2 | .global-syntax.keyword\.repeat, 3 | .global-syntax.keyword { 4 | color: var(--color-prettylights-syntax-keyword); 5 | } 6 | .global-syntax.type { 7 | color: var(--color-prettylights-syntax-entity); 8 | } 9 | .global-syntax.property, 10 | .global-syntax.variable\.parameter { 11 | color: var(--color-prettylights-syntax-variable); 12 | } 13 | .global-syntax.punctuation\.delimiter { 14 | } 15 | .global-syntax.punctuation\.bracket { 16 | } 17 | .global-syntax.attribute { 18 | } 19 | .global-syntax.number, 20 | .global-syntax.constant, 21 | .global-syntax.type\.builtin, 22 | .global-syntax.constant\.builtin, 23 | .global-syntax.variable\.builtin, 24 | .global-syntax.function { 25 | color: var(--color-prettylights-syntax-constant); 26 | } 27 | .global-syntax.comment, 28 | .global-syntax.comment\.documentation { 29 | color: var(--color-prettylights-syntax-comment); 30 | } 31 | .global-syntax.string { 32 | color: var(--color-prettylights-syntax-string); 33 | } 34 | .global-syntax.string.special { 35 | } 36 | .global-syntax.function\.method { 37 | color: var(--color-prettylights-syntax-entity); 38 | } 39 | .global-syntax.type.builtin { 40 | } 41 | .global-syntax.punctuation.bracket { 42 | } 43 | .global-syntax.punctuation.delimiter { 44 | } 45 | .global-syntax.punctuation.special { 46 | } 47 | .global-syntax.text.literal { 48 | } 49 | .global-syntax.text.title { 50 | } 51 | .global-syntax.variable { 52 | color: var(--color-prettylights-syntax-storage-modifier-import); 53 | } 54 | .global-syntax.attribute { 55 | } 56 | .global-syntax.label { 57 | } 58 | .global-syntax.type { 59 | } 60 | .global-syntax.variable.parameter { 61 | } 62 | .global-syntax.constructor { 63 | color: var(--color-prettylights-syntax-entity); 64 | } 65 | .global-syntax.tag\.delimiter { 66 | color: var(--color-prettylights-syntax-keyword); 67 | } 68 | -------------------------------------------------------------------------------- /public/twemoji/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/public/twemoji/.gitkeep -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.84 2 | -------------------------------------------------------------------------------- /scripts/check-js: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | npx tsc --noEmit 5 | npx svelte-check --tsconfig tsconfig.json --fail-on-warnings --compiler-warnings options_missing_custom_element:ignore 6 | npx eslint --cache --cache-location node_modules/.cache/eslint --max-warnings 0 . 7 | npx prettier "**/*.@(ts|js|svelte|json|css|html|yml)" "!crates/**/*" --ignore-path .gitignore --check --cache 8 | -------------------------------------------------------------------------------- /scripts/check-rs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cargo fmt --check 5 | cargo clippy --workspace -- -Dwarnings 6 | cargo check --workspace 7 | cargo test --workspace 8 | -------------------------------------------------------------------------------- /scripts/copy-katex-assets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeuo pipefail 3 | 4 | echo "Copying katex assets into bundle directory" 5 | 6 | cp -r node_modules/katex/dist/katex.min.css public/katex.min.css 7 | cp -r node_modules/katex/dist/fonts/* public/fonts/ 8 | -------------------------------------------------------------------------------- /scripts/install-binaries: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") 5 | RELEASE=$(cat "$REPO_ROOT/tests/support/heartwood-release") 6 | BINARY_PATH=$REPO_ROOT/tests/tmp/bin 7 | OS=$(uname) 8 | 9 | install() { 10 | if test -d "$BINARY_PATH/$1/$2"; then 11 | echo ✅ "Folder $BINARY_PATH/$1/$2 exists already skipping download." 12 | else 13 | mkdir -p "$BINARY_PATH/$1/$2" 14 | case "$OS" in 15 | Darwin) 16 | ARCH="aarch64-apple-darwin" 17 | ;; 18 | Linux) 19 | ARCH="x86_64-unknown-linux-musl" 20 | ;; 21 | *) 22 | echo "There are no precompiled binaries for your OS: $OS, compile $1 manually and make sure it's in PATH." && exit 1 23 | ;; 24 | esac 25 | case "$1" in 26 | heartwood) 27 | FETCH_URL="https://files.radicle.xyz/releases/$2/radicle-$ARCH.tar.xz" 28 | FILENAME="radicle-$2-$ARCH" 29 | ;; 30 | *) 31 | echo "No precompiled binary found with the name $1." && exit 1 32 | ;; 33 | esac 34 | 35 | echo Downloading "$1" v"$2" from "$FETCH_URL into /tests/tmp/bin/$1/$2" 36 | curl --fail -s "$FETCH_URL" | tar -xJ --strip-components=2 -C "$BINARY_PATH/$1/$2" "$FILENAME/bin/" || (echo "Download failed" && exit 1) 37 | fi 38 | } 39 | 40 | show_usage() { 41 | echo 42 | echo "Installs binaries required for running e2e test suite." 43 | echo 44 | echo "USAGE:" 45 | echo " install-binaries [-h]" 46 | echo 47 | echo "OPTIONS:" 48 | echo " -h --help Print this Help." 49 | echo 50 | } 51 | 52 | while [ $# -ne 0 ]; do 53 | case "$1" in 54 | --help | -h) 55 | show_usage 56 | exit 57 | ;; 58 | esac 59 | done 60 | 61 | install "heartwood" "$RELEASE" 62 | 63 | echo 64 | -------------------------------------------------------------------------------- /scripts/install-twemoji-assets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -Eeou pipefail 3 | 4 | version="$(node -e 'console.log(require("twemoji/package.json").version)')" 5 | 6 | echo "Installing Twemoji SVG assets v${version}" 7 | 8 | curl -sSL "https://github.com/twitter/twemoji/archive/refs/tags/v${version}.tar.gz" \ 9 | | tar -x -z -C public/twemoji/ --strip-components=3 "twemoji-${version}/assets/svg" 10 | -------------------------------------------------------------------------------- /src/components/AnnounceSwitch.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 32 | 33 | 39 | 40 |
41 | 50 | 51 | 60 |
61 | -------------------------------------------------------------------------------- /src/components/Avatar.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 29 | 30 | avatar 35 | -------------------------------------------------------------------------------- /src/components/Changeset.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 27 | 28 |
29 | {#each diff.files as file} 30 |
31 | 32 |
33 | {/each} 34 |
35 | -------------------------------------------------------------------------------- /src/components/CheckoutPatchButton.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 33 | {#snippet toggle(onclick)} 34 | 41 | {/snippet} 42 | {#snippet popover()} 43 | 51 | 52 | To checkout this patch in your working copy, run: 53 | 54 | 55 | 56 | {/snippet} 57 | 58 | -------------------------------------------------------------------------------- /src/components/CheckoutRepoButton.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | {#snippet toggle(onclick)} 22 | 29 | {/snippet} 30 | 31 | {#snippet popover()} 32 | 40 | 41 | To checkout a working copy of this repo, run: 42 | 43 | 44 | 45 | {/snippet} 46 | 47 | -------------------------------------------------------------------------------- /src/components/Clipboard.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 27 | 28 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Command.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | 24 |
25 | clipboard.copy()} 28 | styleOverflow="hidden" 29 | styleBackgroundColor="var(--color-background-float)" 30 | styleCursor="pointer" 31 | styleJustifyContent="space-between" 32 | stylePadding="0.25rem 0.5rem" 33 | {styleWidth} 34 | variant="ghost"> 35 | 36 | $ {command} 37 | 38 | 39 | 40 |
41 | -------------------------------------------------------------------------------- /src/components/CommentToggleInput.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | 45 | {#if state !== "collapsed"} 46 | { 55 | if (onclose !== undefined) { 56 | onclose(); 57 | } else { 58 | state = "collapsed"; 59 | } 60 | }} 61 | submit={async ({ comment, embeds }) => { 62 | try { 63 | state = "submit"; 64 | await submit(comment, Array.from(embeds.values())); 65 | } finally { 66 | state = "collapsed"; 67 | } 68 | }} /> 69 | {:else} 70 | { 77 | e.preventDefault(); 78 | e.stopPropagation(); 79 | 80 | state = "expanded"; 81 | if (onexpand !== undefined) { 82 | onexpand(); 83 | } 84 | }}> 85 |
86 | {placeholder} 87 |
88 |
89 | {/if} 90 | -------------------------------------------------------------------------------- /src/components/CommitsContainer.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | 48 |
49 |
50 | { 54 | expanded = !expanded; 55 | }}> 56 | 57 | 58 | {@render leftHeader()} 59 |
60 |
61 | 62 | {#if expanded} 63 |
64 |
65 | {@render children()} 66 |
67 | {/if} 68 |
69 | -------------------------------------------------------------------------------- /src/components/ConfirmClear.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#if closed} 18 | (closed = false)}> 22 | 23 | 24 | {:else} 25 |
26 |
27 | 31 | (closed = true)}> 32 | Cancel 33 | 34 |
35 |
36 | {/if} 37 | -------------------------------------------------------------------------------- /src/components/CopyableId.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | 32 | 33 |
clipboard.copy()} 37 | class:inline 38 | class="copyable-id global-flex txt-small txt-monospace"> 39 | {#if children} 40 | {@render children()} 41 | {:else} 42 | {id} 43 | {/if} 44 | 45 |
46 | -------------------------------------------------------------------------------- /src/components/DiffStatBadge.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | 34 |
35 |
+{stats.insertions}
36 |
-{stats.deletions}
37 |
38 | -------------------------------------------------------------------------------- /src/components/DropdownList.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /src/components/DropdownListItem.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 64 | 65 | 66 |
{ 77 | if (disabled || !onclick) { 78 | return; 79 | } 80 | onclick(); 81 | }}> 82 | {@render children()} 83 |
84 | -------------------------------------------------------------------------------- /src/components/FontSizeSwitch.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 14 | 15 |
16 | 24 | 25 | 28 | 29 | 37 |
38 | -------------------------------------------------------------------------------- /src/components/HoverPopover.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | 43 |
44 |
setVisible(true)} 48 | onmouseleave={() => setVisible(false)}> 49 | {@render toggle()} 50 | 51 | {#if visible} 52 |
53 |
57 | {@render popover()} 58 |
59 |
60 | {/if} 61 |
62 |
63 | -------------------------------------------------------------------------------- /src/components/InlineTitle.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 27 | 28 | 35 | {@html dompurify.sanitize(formatInlineTitle(escape(content)))} 36 | 37 | -------------------------------------------------------------------------------- /src/components/IssueStateFilterButton.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | {#snippet iconSnippet(status: IssueStatus)} 29 |
32 | 33 |
34 | {/snippet} 35 | 36 | {#snippet counterSnippet(status: IssueStatus)} 37 |
38 | {#if status === "all"} 39 | {counters.open + counters.closed} 40 | {:else} 41 | {counters[status]} 42 | {/if} 43 |
44 | {/snippet} 45 | 46 | 50 | {#snippet toggle(onclick)} 51 | 56 | {@render iconSnippet(status)} 57 | {capitalize(status)} 58 | {@render counterSnippet(status)} 59 | 60 | 61 | {/snippet} 62 | 63 | {#snippet popover()} 64 | 65 | 66 | {#snippet item(state)} 67 | { 72 | changeFilter(state); 73 | closeFocused(); 74 | }}> 75 | {@render iconSnippet(state)} 76 | {capitalize(state)} 77 | {@render counterSnippet(state)} 78 | 79 | {/snippet} 80 | 81 | 82 | {/snippet} 83 | 84 | -------------------------------------------------------------------------------- /src/components/Label.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
{label}
11 |
12 | -------------------------------------------------------------------------------- /src/components/Link.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 46 | 47 |
53 | {@render children()} 54 | 55 | -------------------------------------------------------------------------------- /src/components/MoreBreadcrumbsButton.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 22 | {#snippet toggle(onclick)} 23 | 28 | 29 | 30 | {/snippet} 31 | 32 | {#snippet popover()} 33 | 34 |
38 | {@render children()} 39 |
40 |
41 | {/snippet} 42 |
43 | -------------------------------------------------------------------------------- /src/components/NewPatchButton.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 23 | {#snippet toggle(onclick)} 24 | {#if outline} 25 | 30 | New patch 31 | 32 | {:else} 33 | 40 | {/if} 41 | {/snippet} 42 | 43 | {#snippet popover()} 44 |
45 | 53 |
54 |
55 | Create a new patch 56 |
57 |
62 | Create a new branch in your working copy, commit your changes, and 63 | run: 64 | 67 |
68 |
69 | 70 |
71 |
72 | Don't have a working copy yet? 73 |
74 |
79 | To checkout a working copy of this repo, run: 80 | 81 |
82 |
83 |
84 |
85 | {/snippet} 86 |
87 | -------------------------------------------------------------------------------- /src/components/NodeBreadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#if $activeRouteStore.resource === "home"} 22 | 23 | 27 | {:else} 28 | 29 | 30 | 31 | {/if} 32 | -------------------------------------------------------------------------------- /src/components/NodeId.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 |
47 | 48 | {#if alias} 49 | 50 | {alias} 51 | 52 | {:else} 53 | 54 | {truncateId(publicKey)} 55 | 56 | {/if} 57 |
58 | -------------------------------------------------------------------------------- /src/components/NodeStatusButton.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | {#snippet toggle(onclick)} 18 | 19 | {#if $nodeRunning} 20 | 21 | Online 22 | {:else} 23 | 24 | Offline 25 | {/if} 26 | 27 | {/snippet} 28 | {#snippet popover()} 29 | 35 |
36 | {#if $nodeRunning} 37 | Your node is up and running, your changes will be synced 38 | automatically. 39 | {:else} 40 | Your node is not running, changes you make are safe but won't be 41 | announced. 42 | {/if} 43 |
44 |
45 | {/snippet} 46 |
47 | -------------------------------------------------------------------------------- /src/components/PatchStateFilterButton.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | {#snippet iconSnippet(status: PatchStatus | undefined)} 30 |
31 | 35 |
36 | {/snippet} 37 | 38 | {#snippet counterSnippet(status: PatchStatus | undefined)} 39 |
40 | {#if status} 41 | {counters[status]} 42 | {:else} 43 | {counters.draft + counters.open + counters.archived + counters.merged} 44 | {/if} 45 |
46 | {/snippet} 47 | 48 | 52 | {#snippet toggle(onclick)} 53 | 58 | {@render iconSnippet(status)} 59 | {status ? capitalize(status) : "All"} 60 | {@render counterSnippet(status)} 61 | 62 | 63 | {/snippet} 64 | 65 | {#snippet popover()} 66 | 67 | 69 | {#snippet item(state)} 70 | { 75 | await select(state); 76 | closeFocused(); 77 | }}> 78 | {@render iconSnippet(state)} 79 | {state ? capitalize(state) : "All"} 80 | {@render counterSnippet(state)} 81 | 82 | {/snippet} 83 | 84 | 85 | {/snippet} 86 | 87 | -------------------------------------------------------------------------------- /src/components/Path.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 26 | 27 | 28 | {fullPath 29 | .match(/^.*\/|/) 30 | ?.values() 31 | .next().value} 32 | 33 | {fullPath.split("/").slice(-1)} 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/ReactionSelector.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 52 | 53 | 58 | {#snippet toggle(onclick)} 59 | 60 | {/snippet} 61 | {#snippet popover()} 62 | 63 |
64 | {#each availableReactions as reaction} 65 | {@const lookedUpReaction = reactions?.find( 66 | ({ emoji }) => emoji === reaction, 67 | )} 68 | 75 | {/each} 76 |
77 |
78 | {/snippet} 79 |
80 | -------------------------------------------------------------------------------- /src/components/Reactions.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 |
34 | {#each reactions as { emoji, authors }} 35 |
36 | {#if handleReaction} 37 | 38 |
{ 43 | if (handleReaction) { 44 | await handleReaction(authors, emoji); 45 | } 46 | }}> 47 | {@html emojiToTwemoji(emoji, ["21a9"])} 48 | {authors.length} 49 |
50 | {:else} 51 |
52 | {@html emojiToTwemoji(emoji, ["21a9"])} 53 | {authors.length} 54 |
55 | {/if} 56 |
57 | {/each} 58 |
59 | -------------------------------------------------------------------------------- /src/components/RepoCard.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | 37 | 45 |
46 | 47 | 48 |
49 | {#if project.data.description} 50 | {project.data.description} 51 | {:else} 52 | No description. 53 | {/if} 54 |
55 | 61 | 62 | 75 |
76 |
77 | -------------------------------------------------------------------------------- /src/components/RepoGuide.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | {#snippet tabSnippet(name: typeof tab.value, content: string)} 32 | { 35 | tab.value = name; 36 | }}> 37 | {content} 38 | 39 | {/snippet} 40 | 41 | 49 | {@render tabSnippet("clone", "Clone a repo from the network")} 50 | {@render tabSnippet("publish", "Publish existing repo")} 51 | 52 | 53 | 60 |
61 | {#if tab.value === "clone"} 62 | 63 | {:else if tab.value === "publish"} 64 | 65 | {/if} 66 |
67 |
68 | -------------------------------------------------------------------------------- /src/components/RepoGuide/clone.md: -------------------------------------------------------------------------------- 1 | #### 1. Find a repo on the Radicle network 2 | 3 | You can search for Radicle repos by name or description at [search.radicle.xyz](https://search.radicle.xyz). 4 | 5 | To clone a repo, you’ll need its Repository Identifier (RID) — a unique string that begins with `rad:`. 6 | 7 | #### 2. Start your node 8 | 9 | If you node is Offline, you should start it by running: 10 | 11 | ```sh 12 | rad node start 13 | ``` 14 | 15 | #### 3. Clone the repo 16 | 17 | To clone a repo, use the `rad clone` command followed by the RID of the repo you want to clone. 18 | 19 | ```sh 20 | rad clone 21 | ``` 22 | -------------------------------------------------------------------------------- /src/components/RepoGuide/publish.md: -------------------------------------------------------------------------------- 1 | #### Publish existing repo on Radicle 2 | 3 | Navigate to your existing Git repo and publish it to Radicle by following the setup prompts: 4 | 5 | - **Repository Name:** Enter a name for your repo. 6 | - **Description:** Provide a brief summary of what your repo does. 7 | - **Default Branch:** Typically **main** or **master**. 8 | - **Visibility:** Choose **public** to share with others or **private** to not publish it to the network yet. 9 | 10 | ```sh 11 | cd path/to/your/repo 12 | rad init 13 | ``` 14 | 15 | That's it! Your repo is now on the Radicle network. 🚀 16 | -------------------------------------------------------------------------------- /src/components/RepoHeader.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | 26 |
27 |
28 |
31 | {project.data.name[0]} 32 |
33 | 36 | {project.data.name} 37 | 38 |
39 |
40 | {#if repo.visibility.type === "private"} 41 |
45 |
46 | 47 |
48 |
49 | {/if} 50 | {#if repo.delegates.find(x => x.did === selfDid)} 51 |
55 |
56 | 57 |
58 |
59 | {/if} 60 |
61 |
66 | 67 | {repo.seeding} 68 |
69 |
70 |
71 |
72 | -------------------------------------------------------------------------------- /src/components/RepoTeaser.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | 26 |
27 | 28 | 29 | {name} 30 | 31 |
32 | 33 | {seeding} 34 |
35 |
36 | -------------------------------------------------------------------------------- /src/components/RevisionBadges.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {#if revision.id === revisions.slice(-1)[0].id} 15 | 19 | Latest 20 | 21 | {/if} 22 | {#if revision.id === revisions[0].id} 23 | 27 | Initial 28 | 29 | {/if} 30 | -------------------------------------------------------------------------------- /src/components/Settings.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 38 | {#snippet toggle(onclick)} 39 | 45 | 46 | {#if !compact} 47 | Settings 48 | {/if} 49 | 50 | {/snippet} 51 | {#snippet popover()} 52 | 53 |
59 |
64 | Version 65 |
66 | 67 |
71 | Theme 72 |
73 |
77 | Announce changes 78 |
79 |
83 | Font size 84 |
85 |
86 |
87 | {/snippet} 88 |
89 | -------------------------------------------------------------------------------- /src/components/Tab.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 73 | 74 | 75 |
84 |
85 | {@render children()} 86 |
87 |
88 | -------------------------------------------------------------------------------- /src/components/ThemeSwitch.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | 35 | 36 | 42 | 43 |
44 | 54 | 55 | 65 |
66 | -------------------------------------------------------------------------------- /src/components/VerdictBadge.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 48 | 49 | 55 | 56 | {verdict ? capitalize(`${verdict}ed`) : "None"} 57 | {@render children?.()} 58 | 59 | -------------------------------------------------------------------------------- /src/components/VerdictButton.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 51 | 52 | 57 | {#snippet toggle(onclick)} 58 | 63 | {/snippet} 64 | {#snippet popover()} 65 | 66 | 67 | {#snippet item(verdict)} 68 | { 75 | await onSelect(verdict); 76 | closeFocused(); 77 | }}> 78 | 83 | 84 | {verdict ? capitalize(`${verdict}ed`) : "None"} 85 | 86 | 87 | {/snippet} 88 | 89 | 90 | {/snippet} 91 | 92 | -------------------------------------------------------------------------------- /src/components/VisibilityBadge.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 30 | 31 | 35 | 36 | {capitalize(type)} 37 | 38 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | // eslint-disable-next-line @typescript-eslint/naming-convention 4 | __TAURI_INTERNALS__: Record; 5 | } 6 | } 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /src/lib/appearance.svelte.ts: -------------------------------------------------------------------------------- 1 | export const fontSettings = $state({ size: loadFontSize() }); 2 | const step = 2; 3 | const minFontSize = 14; 4 | const maxFontSize = 24; 5 | 6 | export function increaseFontSize() { 7 | if (fontSettings.size + step <= maxFontSize) { 8 | setFontSize(fontSettings.size + step); 9 | } 10 | } 11 | 12 | export function decreaseFontSize() { 13 | if (fontSettings.size - step >= minFontSize) { 14 | setFontSize(fontSettings.size - step); 15 | } 16 | } 17 | 18 | export function resetFontSize() { 19 | setFontSize(16); 20 | } 21 | 22 | function loadFontSize(): number { 23 | const storedFontSize = localStorage ? localStorage.getItem("fontSize") : "16"; 24 | 25 | if (storedFontSize === null) { 26 | return 16; 27 | } else { 28 | return parseInt(storedFontSize); 29 | } 30 | } 31 | 32 | function setFontSize(size: number) { 33 | fontSettings.size = size; 34 | localStorage.setItem("fontSize", size.toString()); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/auth.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorWrapper } from "@bindings/error/ErrorWrapper"; 2 | 3 | import * as router from "@app/lib/router"; 4 | import { dynamicInterval } from "@app/lib/interval"; 5 | import { get } from "svelte/store"; 6 | import { invoke } from "@app/lib/invoke"; 7 | 8 | export const startup = $state<{ error?: ErrorWrapper }>({ error: undefined }); 9 | 10 | let lock = false; 11 | 12 | export async function checkAuth() { 13 | try { 14 | if (lock) { 15 | return; 16 | } 17 | lock = true; 18 | await invoke("authenticate", { passphrase: "" }); 19 | dynamicInterval( 20 | "auth", 21 | checkAuth, 22 | import.meta.env.VITE_AUTH_LONG_DELAY || 30_000, 23 | ); 24 | if (get(router.activeRouteStore).resource === "booting") { 25 | void router.push({ resource: "home", activeTab: "all" }); 26 | } 27 | } catch (err) { 28 | const error = err as ErrorWrapper; 29 | startup.error = error; 30 | if (get(router.activeRouteStore).resource !== "booting") { 31 | void router.push({ resource: "booting" }); 32 | } 33 | dynamicInterval("auth", checkAuth, 5_000); 34 | } finally { 35 | lock = false; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/cached.ts: -------------------------------------------------------------------------------- 1 | import { LRUCache } from "lru-cache"; 2 | 3 | export function cached( 4 | f: (...args: Args) => Promise, 5 | makeKey: (...args: Args) => string, 6 | options?: LRUCache.Options, 7 | ): (...args: Args) => Promise { 8 | const cache = new LRUCache(options || { max: 500 }); 9 | return async function (...args: Args): Promise { 10 | const key = makeKey(...args); 11 | const cached = cache.get(key); 12 | 13 | if (cached === undefined) { 14 | const value = await f(...args); 15 | cache.set(key, { value }); 16 | return value; 17 | } else { 18 | return cached.value; 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/checkRadicleCLI.svelte.ts: -------------------------------------------------------------------------------- 1 | import { dynamicInterval } from "@app/lib/interval"; 2 | import { invoke } from "@app/lib/invoke"; 3 | 4 | let lock = false; 5 | 6 | let installed = $state(false); 7 | 8 | export const radicleInstalled = () => installed; 9 | 10 | export async function checkRadicleCLI() { 11 | try { 12 | if (lock) { 13 | return; 14 | } 15 | lock = true; 16 | await invoke("check_radicle_cli"); 17 | dynamicInterval( 18 | "checkRadicleCLI", 19 | checkRadicleCLI, 20 | import.meta.env.VITE_CHECK_RADICLE_LONG_DELAY || 30_000, 21 | ); 22 | installed = true; 23 | } catch { 24 | dynamicInterval("checkRadicleCLI", checkRadicleCLI, 1_000); 25 | installed = false; 26 | } finally { 27 | lock = false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/emojis.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | const emojis: { [key: string]: string } = { 4 | 100: "💯", 5 | question: "❓", 6 | exclamation: "❗", 7 | sunrise: "🌅", 8 | rainbow: "🌈", 9 | ocean: "🌊", 10 | volcano: "🌋", 11 | seedling: "🌱", 12 | maple_leaf: "🍁", 13 | wood: "🪵", 14 | evergreen_tree: "🌲", 15 | gift: "🎁", 16 | santa: "🎅", 17 | tada: "🎉", 18 | art: "🎨", 19 | dart: "🎯", 20 | bug: "🐛", 21 | wave: "👋", 22 | ok_hand: "👌", 23 | building_construction: "🏗️", 24 | "+1": "👍", 25 | thumbsup: "👍", 26 | "-1": "👎", 27 | thumbsdown: "👎", 28 | clap: "👏", 29 | open_hands: "👐", 30 | ghost: "👻", 31 | alien: "👽", 32 | skull: "💀", 33 | boom: "💥", 34 | poop: "💩", 35 | muscle: "💪", 36 | mage: "🧙‍♀️", 37 | bow: "🙇‍♂️", 38 | see_no_evil: "🙈", 39 | hear_no_evil: "🙉", 40 | speak_no_evil: "🙊", 41 | pray: "🙏", 42 | rocket: "🚀", 43 | construction: "🚧", 44 | rotating_light: "🚨", 45 | no_entry_sign: "🚫", 46 | clown_face: "🤡", 47 | }; 48 | 49 | export default emojis; 50 | -------------------------------------------------------------------------------- /src/lib/events.ts: -------------------------------------------------------------------------------- 1 | import type { SyncStatus } from "@bindings/repo/SyncStatus"; 2 | 3 | import { SvelteMap } from "svelte/reactivity"; 4 | import { writable } from "svelte/store"; 5 | 6 | export const nodeRunning = writable(false); 7 | export const syncStatus = writable>( 8 | new SvelteMap(), 9 | ); 10 | -------------------------------------------------------------------------------- /src/lib/interval.ts: -------------------------------------------------------------------------------- 1 | const dynamicIntervals = new Map>(); 2 | 3 | export function dynamicInterval( 4 | key: string, 5 | callback: () => void, 6 | period: number, 7 | ) { 8 | // Clear an existing interval for this key, if any. 9 | if (dynamicIntervals.has(key)) { 10 | clearTimeout(dynamicIntervals.get(key)); 11 | } 12 | 13 | // Set up a new dynamic interval. 14 | const id = setTimeout(() => { 15 | callback(); 16 | dynamicInterval(key, callback, period); 17 | }, period); 18 | 19 | dynamicIntervals.set(key, id); 20 | } 21 | 22 | export function resetDynamicInterval(key: string) { 23 | dynamicIntervals.delete(key); 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/roles.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyFromDid } from "@app/lib/utils"; 2 | 3 | export function isDelegate( 4 | publicKey: string | undefined, 5 | delegates: string[], 6 | ): true | undefined { 7 | if (!publicKey) { 8 | return undefined; 9 | } 10 | return ( 11 | delegates.some(delegate => publicKeyFromDid(delegate) === publicKey) || 12 | undefined 13 | ); 14 | } 15 | 16 | export function isDelegateOrAuthor( 17 | publicKey: string | undefined, 18 | delegates: string[], 19 | author: string, 20 | ): true | undefined { 21 | return ( 22 | isDelegate(publicKey, delegates) || 23 | publicKey === publicKeyFromDid(author) || 24 | undefined 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/router/definitions.ts: -------------------------------------------------------------------------------- 1 | import type { LoadedHomeRoute, HomeRoute } from "@app/views/home/router"; 2 | import type { LoadedRepoRoute, RepoRoute } from "@app/views/repo/router"; 3 | 4 | import { loadHome } from "@app/views/home/router"; 5 | import { 6 | loadCreateIssue, 7 | loadIssue, 8 | loadIssues, 9 | loadPatch, 10 | loadPatches, 11 | loadRepoHome, 12 | } from "@app/views/repo/router"; 13 | 14 | interface BootingRoute { 15 | resource: "booting"; 16 | } 17 | 18 | export type Route = BootingRoute | HomeRoute | RepoRoute; 19 | export type LoadedRoute = BootingRoute | LoadedHomeRoute | LoadedRepoRoute; 20 | 21 | export async function loadRoute( 22 | route: Route, 23 | _previousLoaded: LoadedRoute, 24 | ): Promise { 25 | if (route.resource === "home") { 26 | return loadHome(route); 27 | } else if (route.resource === "repo.home") { 28 | return loadRepoHome(route); 29 | } else if (route.resource === "repo.issue") { 30 | return loadIssue(route); 31 | } else if (route.resource === "repo.createIssue") { 32 | return loadCreateIssue(route); 33 | } else if (route.resource === "repo.issues") { 34 | return loadIssues(route); 35 | } else if (route.resource === "repo.patch") { 36 | return loadPatch(route); 37 | } else if (route.resource === "repo.patches") { 38 | return loadPatches(route); 39 | } 40 | return route; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(timeMs: number): Promise { 2 | return new Promise(resolve => { 3 | setTimeout(resolve, timeMs); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/startup.svelte.ts: -------------------------------------------------------------------------------- 1 | import type { SyncStatus } from "@bindings/repo/SyncStatus"; 2 | 3 | import once from "lodash/once"; 4 | import { SvelteMap } from "svelte/reactivity"; 5 | import { listen } from "@tauri-apps/api/event"; 6 | 7 | import { nodeRunning, syncStatus } from "./events"; 8 | 9 | // Will be called once in the startup of the app 10 | export const createEventEmittersOnce = once(async () => { 11 | const unlistenEvents = await listen("event", () => { 12 | // Add handler for incoming events 13 | }); 14 | 15 | const unlistenSyncStatus = await listen>( 16 | "sync_status", 17 | event => { 18 | syncStatus.set(new SvelteMap(Object.entries(event.payload))); 19 | }, 20 | ); 21 | 22 | const unlistenNodeEvents = await listen("node_running", event => { 23 | nodeRunning.set(event.payload); 24 | }); 25 | 26 | return [unlistenEvents, unlistenSyncStatus, unlistenNodeEvents]; 27 | }); 28 | -------------------------------------------------------------------------------- /src/lib/useLocalStorage.svelte.ts: -------------------------------------------------------------------------------- 1 | import { z, type SafeParseReturnType } from "zod"; 2 | 3 | export default function useLocalStorage< 4 | S extends z.infer, 5 | T extends z.ZodType = z.ZodType, 6 | >( 7 | key: string, 8 | schema: T, 9 | initialValue: z.infer, 10 | disableLocalStorage = false, 11 | ) { 12 | const stored = !disableLocalStorage ? localStorage.getItem(key) : null; 13 | 14 | const parseFromJson = ( 15 | content: string, 16 | ): SafeParseReturnType => { 17 | return z 18 | .string() 19 | .transform((_, ctx) => { 20 | try { 21 | return JSON.parse(content); 22 | } catch { 23 | ctx.addIssue({ 24 | code: z.ZodIssueCode.custom, 25 | message: "invalid json", 26 | }); 27 | return z.never; 28 | } 29 | }) 30 | .pipe(schema) 31 | .safeParse(content); 32 | }; 33 | 34 | let value = $state(initialValue); 35 | 36 | if (stored) { 37 | try { 38 | const parsed = parseFromJson(stored); 39 | if (parsed.success) { 40 | value = parsed.data; 41 | } else { 42 | console.error("Invalid stored data:", parsed.error); 43 | } 44 | } catch (error) { 45 | console.error("Error parsing stored data:", error); 46 | } 47 | } 48 | 49 | return { 50 | get value() { 51 | return value; 52 | }, 53 | set value(v: S) { 54 | value = v; 55 | if (!disableLocalStorage) 56 | localStorage.setItem(key, JSON.stringify(value)); 57 | }, 58 | clear() { 59 | value = initialValue; 60 | if (!disableLocalStorage) localStorage.removeItem(key); 61 | }, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { mount } from "svelte"; 2 | import { hotkeyKeyUX, hotkeyMacCompat, startKeyUX } from "keyux"; 3 | import App from "./App.svelte"; 4 | 5 | const app = mount(App, { target: document.body }); 6 | 7 | const mac = hotkeyMacCompat(); 8 | startKeyUX(window, [hotkeyKeyUX([mac])]); 9 | 10 | export default app; 11 | -------------------------------------------------------------------------------- /src/views/home/router.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@bindings/config/Config"; 2 | import type { RepoInfo } from "@bindings/repo/RepoInfo"; 3 | import type { RepoCount } from "@bindings/repo/RepoCount"; 4 | 5 | import { invoke } from "@app/lib/invoke"; 6 | 7 | export type HomeReposTab = "all" | "delegate" | "private" | "contributor"; 8 | 9 | export interface HomeRoute { 10 | resource: "home"; 11 | activeTab: HomeReposTab; 12 | } 13 | 14 | export interface LoadedHomeRoute { 15 | resource: "home"; 16 | params: { 17 | activeTab: HomeReposTab; 18 | repoCount: RepoCount; 19 | repos: RepoInfo[]; 20 | config: Config; 21 | notificationCount: number; 22 | }; 23 | } 24 | 25 | export async function loadHome(route: HomeRoute): Promise { 26 | let show = "all"; 27 | 28 | if (route.resource === "home") { 29 | if (route.activeTab === "delegate") { 30 | show = "delegate"; 31 | } else if (route.activeTab === "contributor") { 32 | show = "contributor"; 33 | } else if (route.activeTab === "private") { 34 | show = "private"; 35 | } 36 | } 37 | 38 | const [config, repoCount, repos, notificationCount] = await Promise.all([ 39 | invoke("config"), 40 | invoke("repo_count"), 41 | invoke("list_repos", { show }), 42 | invoke("notification_count"), 43 | ]); 44 | return { 45 | resource: "home", 46 | params: { 47 | activeTab: route.activeTab, 48 | repoCount, 49 | repos, 50 | config, 51 | notificationCount, 52 | }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/views/repo/IssuesBreadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if $activeRouteStore.resource === "repo.issues"} 17 | Issues 18 | {:else} 19 | Issues 20 | {/if} 21 | -------------------------------------------------------------------------------- /src/views/repo/PatchesBreadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if $activeRouteStore.resource === "repo.patches"} 17 | Patches 18 | {:else} 19 | Patches 20 | {/if} 21 | -------------------------------------------------------------------------------- /src/views/repo/RepoBreadcrumb.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#if $activeRouteStore.resource === "repo.home"} 18 | {name} 19 | 20 | {:else} 21 | 22 | {name} 23 | 24 | {/if} 25 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /tests/e2e/clipboard.spec.ts: -------------------------------------------------------------------------------- 1 | import { chromium } from "playwright"; 2 | 3 | import { expect, markdownRid, test } from "@tests/support/fixtures.js"; 4 | import { formatRepositoryId } from "@app/lib/utils"; 5 | 6 | // We explicitly run all clipboard tests withing the context of a single test 7 | // so that we don't run into race conditions, because there is no way to isolate 8 | // the clipboard in Playwright yet. 9 | test("copy to clipboard", async () => { 10 | const browser = await chromium.launch(); 11 | const context = await browser.newContext(); 12 | await context.grantPermissions(["clipboard-read", "clipboard-write"]); 13 | const page = await context.newPage(); 14 | 15 | await page.goto("/repos"); 16 | 17 | // Reset system clipboard to a known state. 18 | await page.evaluate("navigator.clipboard.writeText('')"); 19 | 20 | // Repo ID. 21 | { 22 | await page.getByText(formatRepositoryId(markdownRid)).click(); 23 | const clipboardContent = await page.evaluate( 24 | "navigator.clipboard.readText()", 25 | ); 26 | expect(clipboardContent).toBe(markdownRid); 27 | } 28 | 29 | // Clear the system clipboard contents so developers don't wonder why there's 30 | // random stuff in their clipboard after running tests. 31 | await page.evaluate("navigator.clipboard.writeText('')"); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/e2e/repo/issues.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, cobRid, expect } from "@tests/support/fixtures.js"; 2 | 3 | test("navigate issues listing", async ({ page }) => { 4 | await page.goto(`/repos/${cobRid}/issues?show=all`); 5 | await page.getByRole("link", { name: "Closed" }).click(); 6 | await expect(page.locator(".issue-teaser")).toHaveCount(2); 7 | await expect(page).toHaveURL(`/repos/${cobRid}/issues?status=closed`); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/e2e/repos.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@tests/support/fixtures.js"; 2 | 3 | test("navigate to repo issues", async ({ page }) => { 4 | await page.goto("/repos"); 5 | await page.getByRole("button", { name: "cobs" }).click(); 6 | await page.getByRole("link", { name: "icon-issue Issues" }).click(); 7 | await page.getByText("This title has **markdown**").click(); 8 | await expect( 9 | page.getByText("This title has **markdown**").nth(1), 10 | ).toBeVisible(); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/e2e/theme.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@tests/support/fixtures.js"; 2 | 3 | test("default theme", async ({ page }) => { 4 | await page.goto("/repos"); 5 | 6 | await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); 7 | }); 8 | 9 | test("theme persistence", async ({ page }) => { 10 | await page.goto("/repos"); 11 | await expect(page.getByRole("button", { name: "markdown" })).toBeVisible(); 12 | await page.getByRole("button", { name: "Settings" }).click(); 13 | 14 | await page 15 | .getByRole("button", { name: "icon-sun Light", exact: true }) 16 | .click(); 17 | await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); 18 | 19 | await page.reload(); 20 | 21 | await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); 22 | }); 23 | 24 | test("change theme", async ({ page }) => { 25 | await page.goto("/repos"); 26 | await expect(page.getByRole("button", { name: "markdown" })).toBeVisible(); 27 | await page.getByRole("button", { name: "Settings" }).click(); 28 | 29 | await page 30 | .getByRole("button", { name: "icon-sun Light", exact: true }) 31 | .click(); 32 | await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); 33 | 34 | await page 35 | .getByRole("button", { name: "icon-moon Dark", exact: true }) 36 | .click(); 37 | await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/fixtures/repos/markdown.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radicle-dev/radicle-desktop/c3aa74f4c96855eeb8c74e6aa15d82a403f00f38/tests/fixtures/repos/markdown.tar.bz2 -------------------------------------------------------------------------------- /tests/support/cobs/issue.ts: -------------------------------------------------------------------------------- 1 | import type { RadiclePeer } from "@tests/support/peerManager.js"; 2 | import type { Options } from "execa"; 3 | 4 | export async function create( 5 | peer: RadiclePeer, 6 | title: string, 7 | description: string, 8 | labels: string[], 9 | options: Options, 10 | ): Promise { 11 | const issueOptions: string[] = [ 12 | "issue", 13 | "open", 14 | "--title", 15 | title, 16 | "--description", 17 | description, 18 | ...labels.map(label => ["--label", label]).flat(), 19 | ]; 20 | const { stdout } = await peer.rad(issueOptions, options); 21 | const match = stdout.match(/Issue {3}([a-zA-Z0-9]*)/); 22 | if (!match) { 23 | throw new Error("Not able to parse issue id"); 24 | } 25 | return match[1]; 26 | } 27 | -------------------------------------------------------------------------------- /tests/support/cobs/patch.ts: -------------------------------------------------------------------------------- 1 | import type { RadiclePeer } from "@tests/support/peerManager.js"; 2 | import type { Options } from "execa"; 3 | 4 | export async function create( 5 | peer: RadiclePeer, 6 | commitLines: string[], 7 | branch: string, 8 | changeFn: () => Promise, 9 | messages: string[], 10 | options: Options, 11 | ): Promise { 12 | if (branch) { 13 | await peer.git(["reset", "--hard"], options); 14 | await peer.git(["switch", "main"], options); 15 | await peer.git(["switch", "-c", branch], options); 16 | } 17 | await changeFn(); 18 | await peer.git(["add", "."], options); 19 | await peer.git( 20 | ["commit"].concat(...commitLines.map(line => ["-m", line])), 21 | options, 22 | ); 23 | const cmd = [ 24 | "push", 25 | ...messages.map(msg => ["-o", `patch.message=${msg}`]).flat(), 26 | "rad", 27 | "HEAD:refs/patches", 28 | ]; 29 | const { stderr } = await peer.git(cmd, options); 30 | const match = stderr.match(/✓ Patch ([a-zA-Z0-9]*) opened/); 31 | if (!match) { 32 | throw new Error("Not able to parse patch id"); 33 | } 34 | return match[1]; 35 | } 36 | 37 | export async function merge( 38 | peer: RadiclePeer, 39 | targetBranch: string, 40 | featureBranch: string, 41 | options: Options, 42 | ): Promise { 43 | await peer.git(["switch", targetBranch], options); 44 | await peer.git(["merge", featureBranch], options); 45 | await peer.git(["push", "rad", targetBranch], options); 46 | } 47 | -------------------------------------------------------------------------------- /tests/support/heartwood-release: -------------------------------------------------------------------------------- 1 | 1.1.0-pre.4 -------------------------------------------------------------------------------- /tests/support/logPrefix.ts: -------------------------------------------------------------------------------- 1 | import type { ColorName } from "chalk"; 2 | 3 | import chalk from "chalk"; 4 | 5 | const PADDING_WIDTH = 12; 6 | 7 | // The order here is important, we want successive prefixes to have 8 | // high contrast. 9 | const availableColors: ColorName[] = [ 10 | "blue", 11 | "yellowBright", 12 | "greenBright", 13 | "gray", 14 | "green", 15 | "blueBright", 16 | "redBright", 17 | "white", 18 | "yellow", 19 | "red", 20 | "magenta", 21 | "cyan", 22 | ]; 23 | 24 | let nextColorIndex = 0; 25 | 26 | const assignedColors: Record = {}; 27 | 28 | export function logPrefix(label: string): string { 29 | if (assignedColors[label] === undefined) { 30 | const color = availableColors[nextColorIndex]; 31 | nextColorIndex = (nextColorIndex + 1) % availableColors.length; 32 | assignedColors[label] = color; 33 | } 34 | 35 | // We reset colors at the beginning of each line to avoid styles from previous 36 | // lines messing up prefix colors. This is noticable in rust stack traces 37 | // where the `in` and `with` keywords have a white background color. 38 | return chalk.reset[assignedColors[label]]( 39 | `${label.padEnd(PADDING_WIDTH)} | `, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /tests/support/repo.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | import type { RadiclePeer } from "@tests/support/peerManager"; 3 | 4 | import * as Path from "node:path"; 5 | 6 | export async function changeBranch(peer: string, branch: string, page: Page) { 7 | await page.getByTitle("Change branch").click(); 8 | const peerLocator = page.getByLabel("peer-item").filter({ hasText: peer }); 9 | await peerLocator.getByTitle("Expand peer").click(); 10 | await page.getByRole("button", { name: branch }).click(); 11 | } 12 | 13 | // Create a repo using the rad CLI. 14 | export async function createRepo( 15 | peer: RadiclePeer, 16 | { 17 | name, 18 | description = "", 19 | defaultBranch = "main", 20 | visibility = "public", 21 | }: { 22 | name: string; 23 | description?: string; 24 | defaultBranch?: string; 25 | visibility?: "public" | "private"; 26 | }, 27 | ): Promise<{ rid: string; repoFolder: string; defaultBranch: string }> { 28 | const repoFolder = Path.join(peer.checkoutPath, name); 29 | 30 | await peer.git(["init", name, "--initial-branch", defaultBranch], { 31 | cwd: peer.checkoutPath, 32 | }); 33 | await peer.git(["commit", "--allow-empty", "--message", "initial commit"], { 34 | cwd: repoFolder, 35 | }); 36 | await peer.rad( 37 | [ 38 | "init", 39 | "--name", 40 | name, 41 | "--default-branch", 42 | defaultBranch, 43 | "--description", 44 | description, 45 | `--${visibility}`, 46 | ], 47 | { 48 | cwd: repoFolder, 49 | }, 50 | ); 51 | 52 | const { stdout: rid } = await peer.rad(["inspect"], { 53 | cwd: repoFolder, 54 | }); 55 | 56 | return { rid, repoFolder, defaultBranch }; 57 | } 58 | 59 | export function extractPatchId(cmdOutput: { stderr: string }) { 60 | const match = cmdOutput.stderr.match(/[0-9a-f]{40}/); 61 | if (match) { 62 | return match[0]; 63 | } else { 64 | throw new Error("Could not get patch id"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/support/router.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from "@playwright/test"; 2 | import { expect } from "@tests/support/fixtures.js"; 3 | 4 | // Reloads the current page and verifies that the URL stays correct 5 | export const expectUrlPersistsReload = async (page: Page) => { 6 | const url = page.url(); 7 | await page.reload(); 8 | await expect(page).toHaveURL(url); 9 | }; 10 | 11 | // Navigates back, checks the URL and navigates forward back to the initial page 12 | export const expectBackAndForwardNavigationWorks = async ( 13 | beforeURL: string, 14 | page: Page, 15 | ) => { 16 | const currentURL = page.url(); 17 | 18 | await page.goBack(); 19 | await page 20 | .getByRole("progressbar", { name: "Page loading" }) 21 | .waitFor({ state: "hidden" }); 22 | await expect(page).toHaveURL(beforeURL); 23 | await page.goForward(); 24 | 25 | await page 26 | .getByRole("progressbar", { name: "Page loading" }) 27 | .waitFor({ state: "hidden" }); 28 | await expect(page).toHaveURL(currentURL); 29 | }; 30 | -------------------------------------------------------------------------------- /tests/support/support.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from "execa"; 2 | 3 | import { execa } from "execa"; 4 | import * as Crypto from "node:crypto"; 5 | import { fileURLToPath } from "node:url"; 6 | import * as Path from "node:path"; 7 | import * as Fs from "node:fs/promises"; 8 | 9 | // Generate string of 12 random characters with 8 bits of entropy. 10 | export function randomTag(): string { 11 | return Crypto.randomBytes(8).toString("hex"); 12 | } 13 | 14 | export function createOptions(repoFolder: string, days: number): Options { 15 | return { 16 | cwd: repoFolder, 17 | // eslint-disable-next-line @typescript-eslint/naming-convention 18 | env: { RAD_LOCAL_TIME: (1671211684 + days * 86400).toString() }, 19 | }; 20 | } 21 | 22 | const filename = fileURLToPath(import.meta.url); 23 | export const supportDir = Path.dirname(filename); 24 | export const tmpDir = Path.resolve(supportDir, "..", "./tmp"); 25 | export const fixturesDir = Path.resolve(supportDir, "..", "./fixtures"); 26 | const workspacePaths = [Path.join(tmpDir, "peers"), Path.join(tmpDir, "repos")]; 27 | 28 | export const heartwoodRelease = await Fs.readFile( 29 | `${supportDir}/heartwood-release`, 30 | "utf8", 31 | ); 32 | 33 | // Assert that binaries are installed and are the correct version. 34 | export async function assertBinariesInstalled( 35 | binary: string, 36 | expectedVersion: string, 37 | expectedPath: string, 38 | ): Promise { 39 | const { stdout: which } = await execa("which", [binary]); 40 | if (Path.dirname(which) !== expectedPath) { 41 | throw new Error( 42 | `${binary} path doesn't match used ${binary} binary: ${expectedPath} !== ${which}`, 43 | ); 44 | } 45 | const { stdout: version } = await execa(binary, ["--version"]); 46 | if (!version.includes(expectedVersion)) { 47 | throw new Error( 48 | `${binary} version ${version} does not satisfy ${expectedVersion}`, 49 | ); 50 | } 51 | } 52 | 53 | export async function removeWorkspace(): Promise { 54 | for (const path of workspacePaths) { 55 | await Fs.rm(path, { 56 | recursive: true, 57 | force: true, 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "include": ["src", "tests", "./*.js", "./*.ts"], 4 | "exclude": ["node_modules/*", "isolation/*"], 5 | "compilerOptions": { 6 | "noEmit": true, 7 | "target": "es2021", 8 | "module": "es2022", 9 | "types": ["vite/client"], 10 | "sourceMap": true, 11 | "baseUrl": "./", 12 | "moduleResolution": "bundler", 13 | "strict": true, 14 | "resolveJsonModule": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "useDefineForClassFields": true, 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true, 20 | "skipLibCheck": true, 21 | "paths": { 22 | "@app/*": ["./src/*"], 23 | "@bindings/*": ["./crates/radicle-types/bindings/*"], 24 | "@tests/*": ["./tests/*"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { svelte } from "@sveltejs/vite-plugin-svelte"; 3 | import path from "node:path"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | test: { 8 | environment: "happy-dom", 9 | include: ["tests/unit/**/*.test.ts"], 10 | reporters: "verbose", 11 | }, 12 | plugins: [ 13 | svelte({ 14 | // Reference: https://github.com/sveltejs/vite-plugin-svelte/issues/270#issuecomment-1033190138 15 | dynamicCompileOptions({ filename }) { 16 | if (path.basename(filename) === "Clipboard.svelte") { 17 | return { customElement: true }; 18 | } 19 | }, 20 | }), 21 | ], 22 | build: { 23 | outDir: "build", 24 | }, 25 | // prevent vite from obscuring rust errors 26 | clearScreen: false, 27 | server: { 28 | port: 1420, 29 | strictPort: true, 30 | watch: { 31 | ignored: ["**/crates/radicle-tauri/**"], 32 | }, 33 | }, 34 | resolve: { 35 | alias: { 36 | "@app": path.resolve("./src"), 37 | "@bindings": path.resolve("./crates/radicle-types/bindings/"), 38 | }, 39 | }, 40 | }); 41 | --------------------------------------------------------------------------------