fix: use our own open-rs implementation instead of relying on tauri's "shell-open" (#4748)

Co-authored-by: Yerke Tulibergenov <yerke@squareup.com>
Co-authored-by: Caleb Owens <caleb@gitbutler.com>
Co-authored-by: Pavel Laptev <pawellaptew@gmail.com>
Co-authored-by: Mattias Granlund <mtsgrd@gmail.com>
Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
Co-authored-by: GitButler <gitbutler@gitbutler.com>
This commit is contained in:
Nico Domino 2024-09-07 19:28:50 +02:00 committed by GitHub
parent 6fdcf9fcc8
commit 68f0a3c288
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 429 additions and 219 deletions

73
Cargo.lock generated
View File

@ -2579,7 +2579,7 @@ dependencies = [
"gix",
"log",
"once_cell",
"open 5.3.0",
"open",
"parking_lot 0.12.3",
"pretty_assertions",
"reqwest 0.12.7",
@ -2599,6 +2599,7 @@ dependencies = [
"tracing-appender",
"tracing-forest",
"tracing-subscriber",
"url",
]
[[package]]
@ -5342,16 +5343,6 @@ version = "11.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
[[package]]
name = "open"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8"
dependencies = [
"pathdiff",
"windows-sys 0.42.0",
]
[[package]]
name = "open"
version = "5.3.0"
@ -7345,12 +7336,10 @@ dependencies = [
"indexmap 1.9.3",
"objc",
"once_cell",
"open 3.2.0",
"os_info",
"percent-encoding",
"rand 0.8.5",
"raw-window-handle",
"regex",
"reqwest 0.11.27",
"rfd",
"semver",
@ -7408,7 +7397,6 @@ dependencies = [
"png",
"proc-macro2",
"quote",
"regex",
"semver",
"serde",
"serde_json",
@ -8657,21 +8645,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@ -8745,12 +8718,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -8775,12 +8742,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -8805,12 +8766,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -8841,12 +8796,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -8871,12 +8820,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -8889,12 +8832,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -8919,12 +8856,6 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"

View File

@ -6,7 +6,7 @@
import * as events from '$lib/utils/events';
import { createKeybind } from '$lib/utils/hotkeys';
import { unsubscribe } from '$lib/utils/unsubscribe';
import { open } from '@tauri-apps/api/shell';
import { openExternalUrl } from '$lib/utils/url';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
@ -29,7 +29,7 @@
'menu://project/open-in-vscode/clicked',
async () => {
const path = `${$editor}://file${project.vscodePath}?windowId=_blank`;
open(path);
openExternalUrl(path);
}
);

View File

@ -1,10 +1,9 @@
<script lang="ts">
import { MessageRole } from '$lib/ai/types';
import Markdown from '$lib/components/Markdown.svelte';
import { autoHeight } from '$lib/utils/autoHeight';
import { getMarkdownRenderer } from '$lib/utils/markdown';
import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte';
import { marked } from 'marked';
import { createEventDispatcher } from 'svelte';
export let disableRemove = false;
@ -19,7 +18,6 @@
addExample: void;
input: string;
}>();
const markedRenderer = getMarkdownRenderer();
let textareaElement: HTMLTextAreaElement | undefined;
function focusTextareaOnMount(
@ -73,7 +71,7 @@
></textarea>
{:else}
<div class="markdown bubble-message scrollbar text-13 text-body">
{@html marked.parse(promptMessage.content, { renderer: markedRenderer })}
<Markdown content={promptMessage.content} />
</div>
{/if}
</div>

View File

@ -50,10 +50,7 @@
</div>
<button
class="empty-board__suggestions__link"
on:click={async () =>
await openExternalUrl(
'https://docs.gitbutler.com/features/virtual-branches/branch-lanes'
)}
on:click={async () => await openExternalUrl('https://docs.gitbutler.com')}
>
<div class="empty-board__suggestions__link__icon">
<Icon name="docs" />

View File

@ -4,19 +4,18 @@
import { Project } from '$lib/backend/projects';
import CommitCard from '$lib/commit/CommitCard.svelte';
import { transformAnyCommit } from '$lib/commitLines/transformers';
import Markdown from '$lib/components/Markdown.svelte';
import FileCard from '$lib/file/FileCard.svelte';
import { getGitHost } from '$lib/gitHost/interface/gitHost';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
import { RemoteBranchService } from '$lib/stores/remoteBranches';
import { getContext, getContextStoreBySymbol } from '$lib/utils/context';
import { getMarkdownRenderer } from '$lib/utils/markdown';
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { BranchData, type Branch } from '$lib/vbranches/types';
import LineGroup from '@gitbutler/ui/commitLines/LineGroup.svelte';
import { LineManagerFactory } from '@gitbutler/ui/commitLines/lineManager';
import lscache from 'lscache';
import { marked } from 'marked';
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import type { PullRequest } from '$lib/gitHost/interface/types';
@ -93,8 +92,6 @@
onMount(() => {
laneWidth = lscache.get(laneWidthKey);
});
const renderer = getMarkdownRenderer();
</script>
{#if remoteBranch || localBranch}
@ -112,7 +109,7 @@
<div class="card__header text-14 text-body text-semibold">{pr.title}</div>
{#if pr.body}
<div class="markdown card__content text-13 text-body">
{@html marked.parse(pr.body, { renderer })}
<Markdown content={pr.body} />
</div>
{/if}
</div>

View File

@ -3,6 +3,7 @@
import gbLogoSvg from '$lib/assets/gb-logo.svg?raw';
import { User } from '$lib/stores/user';
import { getContextStore } from '$lib/utils/context';
import { openExternalUrl } from '$lib/utils/url';
import Icon from '@gitbutler/ui/Icon.svelte';
import { type Snippet } from 'svelte';
@ -68,22 +69,20 @@
<div class="right-side__meta">
<div class="right-side__links">
<a
<button
class="right-side__link"
target="_blank"
href="https://docs.gitbutler.com/features/virtual-branches/branch-lanes"
onclick={async () => await openExternalUrl('https://docs.gitbutler.com/')}
>
<Icon name="docs" opacity={0.6} />
<span class="text-14 text-semibold">GitButler docs</span>
</a>
<a
</button>
<button
class="right-side__link"
target="_blank"
href="https://discord.com/invite/MmFkmaJ42D"
onclick={async () => await openExternalUrl('https://discord.com/invite/MmFkmaJ42D')}
>
<Icon name="discord" opacity={0.6} />
<span class="text-14 text-semibold">Join community</span>
</a>
</button>
</div>
<div class="wordmark">

View File

@ -0,0 +1,24 @@
<script lang="ts">
import MarkdownContent from '$lib/components/MarkdownContent.svelte';
import { options } from '$lib/utils/markdownRenderers';
import { Lexer } from 'marked';
interface Props {
content: string;
}
let { content }: Props = $props();
const lexer = new Lexer(options);
const tokens = lexer.lex(content);
</script>
<div class="markdown-content">
<MarkdownContent type="init" {tokens} />
</div>
<style>
.markdown-content {
display: inline;
}
</style>

View File

@ -0,0 +1,32 @@
<script lang="ts">
/* eslint svelte/valid-compile: "off" */
import { renderers } from '$lib/utils/markdownRenderers';
import type { Tokens, Token } from 'marked';
type Props =
| { type: 'init'; tokens: Token[] }
| Tokens.Link
| Tokens.Heading
| Tokens.Image
| Tokens.Space
| Tokens.Blockquote
| Tokens.Code
| Tokens.Codespan
| Tokens.Text;
let { type, ...rest }: Props = $props();
</script>
{#if type && renderers[type as keyof typeof renderers]}
<svelte:component this={renderers[type as keyof typeof renderers] as any} {...rest}>
{#if 'tokens' in rest}
<svelte:self tokens={rest.tokens} />
{/if}
</svelte:component>
{:else if 'tokens' in rest && rest.tokens}
{#each rest.tokens as token}
<svelte:self {...token} />
{/each}
{:else if 'raw' in rest}
{@html rest.raw?.replaceAll('\n', '<br />') ?? ''}
{/if}

View File

@ -2,18 +2,17 @@
// This is always displayed in the context of not having a cooresponding vbranch or remote
import { Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import Markdown from '$lib/components/Markdown.svelte';
import { RemotesService } from '$lib/remotes/service';
import Link from '$lib/shared/Link.svelte';
import TextBox from '$lib/shared/TextBox.svelte';
import { getContext } from '$lib/utils/context';
import { getMarkdownRenderer } from '$lib/utils/markdown';
import * as toasts from '$lib/utils/toasts';
import { remoteUrlIsHttp } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import { marked } from 'marked';
import { get } from 'svelte/store';
import type { PullRequest } from '$lib/gitHost/interface/types';
import { goto } from '$app/navigation';
@ -25,7 +24,6 @@
const remotesService = getContext(RemotesService);
const baseBranchService = getContext(BaseBranchService);
const virtualBranchService = getContext(VirtualBranchService);
const renderer = getMarkdownRenderer();
let remoteName = structuredClone(pullrequest.repoName) || '';
let createRemoteModal: Modal | undefined;
@ -108,9 +106,9 @@
{#if pullrequest.draft}
<Button size="tag" clickable={false} style="neutral" icon="draft-pr-small">Draft</Button>
{:else}
<Button size="tag" clickable={false} style="success" kind="solid" icon="pr-small"
>Open</Button
>
<Button size="tag" clickable={false} style="success" kind="solid" icon="pr-small">
Open
</Button>
{/if}
</div>
@ -130,7 +128,7 @@
</div>
{#if pullrequest.body}
<div class="markdown">
{@html marked.parse(pullrequest.body, { renderer })}
<Markdown content={pullrequest.body} />
</div>
{/if}
</div>

View File

@ -7,7 +7,7 @@
export let disabled = false;
</script>
<button class="menu-item" class:disabled {disabled} on:mousedown on:click>
<button class="menu-item" class:disabled {disabled} on:click>
{#if icon}
<Icon name={icon} />
{/if}

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
}
const { children }: Props = $props();
</script>
<blockquote>
{@render children()}
</blockquote>
<style>
blockquote {
border-left: 2px solid #ccc;
padding: 0 0 0 1rem;
}
</style>

View File

@ -0,0 +1,10 @@
<script lang="ts">
interface Props {
text: string;
lang: string;
}
const { text, lang }: Props = $props();
</script>
<pre class={`language-${lang}`}>{text}</pre>

View File

@ -0,0 +1,9 @@
<script lang="ts">
interface Props {
raw: string;
}
const { raw = '' }: Props = $props();
</script>
<code>{raw.replace(/`/g, '')}</code>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { slugify } from '$lib/utils/string';
interface Props {
depth: number;
raw: string;
text: string;
}
const { depth, raw, text }: Props = $props();
const id = $derived(slugify(text) ?? text);
</script>
{#if depth === 1}
<h1 {id}>
{text}
</h1>
{:else if depth === 2}
<h2 {id}>
{text}
</h2>
{:else if depth === 3}
<h3 {id}>
{text}
</h3>
{:else if depth === 4}
<h4 {id}>
{text}
</h4>
{:else if depth === 5}
<h5 {id}>
{text}
</h5>
{:else if depth === 6}
<h6 {id}>
{text}
</h6>
{:else}
{raw}
{/if}

View File

@ -0,0 +1,11 @@
<script lang="ts">
interface Props {
href?: string;
title?: string;
text?: string;
}
const { href = '', title = undefined, text = '' }: Props = $props();
</script>
<img src={href} {title} alt={text} />

View File

@ -0,0 +1,14 @@
<script lang="ts">
import type { Snippet } from 'svelte';
interface Props {
text: string;
children: Snippet;
}
const { children }: Props = $props();
</script>
<p>
{@render children()}
</p>

View File

@ -0,0 +1,2 @@
<br />
<br />

View File

@ -0,0 +1,11 @@
<script lang="ts">
interface Props {
text: string;
}
const { text }: Props = $props();
</script>
<span>
{text}
</span>

View File

@ -7,12 +7,12 @@
import { getContext } from '$lib/utils/context';
import { computeFileStatus } from '$lib/utils/fileStatus';
import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController';
import { LocalFile, type AnyFile } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import { join } from '@tauri-apps/api/path';
import { open as openFile } from '@tauri-apps/api/shell';
export let branchId: string | undefined;
export let target: HTMLElement | undefined;
@ -85,7 +85,7 @@
if (!project) return;
for (let file of item.files) {
const absPath = await join(project.vscodePath, file.path);
openFile(`${$editor}://file${absPath}`);
openExternalUrl(`${$editor}://file${absPath}`);
}
contextMenu.close();
} catch {

View File

@ -87,7 +87,7 @@ export class GitHubPrService implements GitHostPrService {
showToast({
title: 'Failed to fetch pull request template',
message: `Template not found at path: <code>${path}</code>.`,
message: `Template not found at path: \`${path}\`.`,
style: 'neutral'
});
}

View File

@ -4,8 +4,8 @@
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
import { editor } from '$lib/editorLink/editorLink';
import { getContext } from '$lib/utils/context';
import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController';
import { open as openFile } from '@tauri-apps/api/shell';
interface Props {
target: HTMLElement | undefined;
@ -43,10 +43,10 @@
{/if}
{#if item.lineNumber}
<ContextMenuItem
label="Open in VS Code"
on:mousedown={() => {
label="Open in VSCode"
on:click={() => {
projectPath &&
openFile(`${$editor}://file${projectPath}/${filePath}:${item.lineNumber}`);
openExternalUrl(`${$editor}://file${projectPath}/${filePath}:${item.lineNumber}`);
contextMenu.close();
}}
/>

View File

@ -1,11 +1,8 @@
<script lang="ts">
import Markdown from '$lib/components/Markdown.svelte';
import { dismissToast, toastStore } from '$lib/notifications/toasts';
import InfoMessage from '$lib/shared/InfoMessage.svelte';
import { getMarkdownRenderer } from '$lib/utils/markdown';
import { marked } from 'marked';
import { slide } from 'svelte/transition';
var renderer = getMarkdownRenderer();
</script>
<div class="toast-controller hide-native-scrollbar">
@ -24,7 +21,9 @@
</svelte:fragment>
<svelte:fragment slot="content">
{@html marked.parse(toast.message ?? '', { renderer })}
{#if toast.message}
<Markdown content={toast.message} />
{/if}
</svelte:fragment>
</InfoMessage>
</div>

View File

@ -1,5 +1,6 @@
<script lang="ts">
import SupportersBanner from './SupportersBanner.svelte';
import { openExternalUrl } from '$lib/utils/url';
import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte';
import { goto } from '$app/navigation';
@ -109,23 +110,21 @@
<section class="profile-sidebar__bottom">
<div class="social-banners">
<a
<button
class="social-banner"
href="mailto:hello@gitbutler.com?subject=Feedback or question!"
target="_blank"
on:click={async () =>
await openExternalUrl('mailto:hello@gitbutler.com?subject=Feedback or question!')}
>
<span class="text-14 text-bold">Contact us</span>
<Icon name="mail" />
</a>
<a
</button>
<button
class="social-banner"
href="https://discord.gg/MmFkmaJ42D"
target="_blank"
rel="noreferrer"
on:click={async () => await openExternalUrl('https://discord.gg/MmFkmaJ42D')}
>
<span class="text-14 text-bold">Join our Discord</span>
<Icon name="discord" />
</a>
</button>
</div>
<SupportersBanner />

View File

@ -1,13 +1,20 @@
<a class="banner" href="https://docs.gitbutler.com/community/supporters" target="_blank">
<div class="benner-content">
<h4 class="benner-label text-14 text-bold">Thank you to all GitButler early supporters</h4>
<i class="benner-arrow-wrap">
<div class="benner-arrow-tail"></div>
<script lang="ts">
import { openExternalUrl } from '$lib/utils/url';
</script>
<button
class="banner"
on:click={async () => await openExternalUrl('https://docs.gitbutler.com/community/supporters')}
>
<div class="banner-content">
<h4 class="banner-label text-14 text-bold">Thank you to all GitButler early supporters</h4>
<i class="banner-arrow-wrap">
<div class="banner-arrow-tail"></div>
<svg
viewBox="0 0 7 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="benner-arrow-head"
class="banner-arrow-head"
>
<path
d="M0.871094 1L5.00013 5.5L0.871094 10"
@ -18,7 +25,7 @@
</i>
</div>
<img class="banner-img" src="/images/banners/support.svg" alt="" />
</a>
</button>
<style>
.banner {
@ -29,22 +36,22 @@
background-color: #d7f2f1;
&:hover {
& .benner-arrow-wrap {
& .banner-arrow-wrap {
width: 12px;
}
& .benner-arrow-tail {
& .banner-arrow-tail {
width: 90%;
}
}
}
.benner-content {
.banner-content {
display: inline;
color: #000;
}
.benner-label {
.banner-label {
display: inline;
}
@ -55,7 +62,7 @@
/* ARROW */
.benner-arrow-wrap {
.banner-arrow-wrap {
position: relative;
display: inline-flex;
transform: translateY(2px);
@ -64,7 +71,7 @@
transition: width 0.2s;
}
.benner-arrow-tail {
.banner-arrow-tail {
position: absolute;
top: 50%;
transform: translateY(-50%);
@ -75,7 +82,7 @@
transition: width 0.2s;
}
.benner-arrow-head {
.banner-arrow-head {
position: absolute;
right: 0;
top: 0;

View File

@ -1,15 +1,24 @@
<script lang="ts">
export let href: string;
import { openExternalUrl } from '$lib/utils/url';
import Icon from '@gitbutler/ui/Icon.svelte';
import type iconsJson from '@gitbutler/ui/data/icons.json';
import type { Snippet } from 'svelte';
export let icon: keyof typeof iconsJson;
interface Props {
href: string;
icon: keyof typeof iconsJson;
children: Snippet;
}
const { href, icon, children }: Props = $props();
</script>
<a class="link" target="_blank" {href}>
<button class="link" onclick={async () => await openExternalUrl(href)}>
<Icon name={icon} />
<span class="text-12"><slot /></span>
</a>
<span class="text-12">
{@render children()}
</span>
</button>
<style lang="postcss">
.link {

View File

@ -1,17 +1,29 @@
<script lang="ts">
import { openExternalUrl } from '$lib/utils/url';
import Icon from '@gitbutler/ui/Icon.svelte';
import { onMount } from 'svelte';
import { onMount, type Snippet } from 'svelte';
let classes = '';
export { classes as class };
export let target: '_blank' | '_self' | '_parent' | '_top' | undefined = undefined;
export let rel: string | undefined = undefined;
export let role: 'basic' | 'primary' | 'error' = 'basic';
export let disabled = false;
export let href: string | undefined = undefined;
interface Props {
href: string;
children: Snippet;
class?: string;
target?: '_blank' | '_self' | '_parent' | '_top' | undefined;
rel?: string | undefined;
role?: 'basic' | 'primary' | 'error';
disabled?: boolean;
}
let element: HTMLAnchorElement | HTMLButtonElement | undefined;
const {
href,
target = undefined,
class: classes,
rel = undefined,
role = 'basic',
disabled = false,
children
}: Props = $props();
let element = $state<HTMLAnchorElement | HTMLButtonElement>();
onMount(() => {
if (element) {
@ -19,33 +31,31 @@
}
});
$: isExternal = href?.startsWith('http');
const isExternal = $derived(href?.startsWith('http'));
</script>
{#if href}
<a
{href}
{target}
{rel}
class="link {role} {classes}"
bind:this={element}
class:disabled
on:click={(e) => {
if (href && isExternal) {
e.preventDefault();
e.stopPropagation();
openExternalUrl(href);
}
}}
>
<slot />
{#if isExternal}
<div class="link-icon">
<Icon name="open-link" />
</div>
{/if}
</a>
{/if}
<a
{href}
{target}
{rel}
class="link {role} {classes}"
bind:this={element}
class:disabled
onclick={(e) => {
if (href && isExternal) {
e.preventDefault();
e.stopPropagation();
openExternalUrl(href);
}
}}
>
{@render children()}
{#if isExternal}
<div class="link-icon">
<Icon name="open-link" />
</div>
{/if}
</a>
<style lang="postcss">
.link {
@ -62,6 +72,7 @@
text-decoration: none;
}
}
.link-icon {
flex-shrink: 0;
}

View File

@ -1,10 +0,0 @@
import { marked } from 'marked';
export function getMarkdownRenderer() {
const renderer = new marked.Renderer({});
renderer.link = function (href, title, text) {
if (!title) title = text;
return '<a target="_blank" href="' + href + '" title="' + title + '">' + text + '</a>';
};
return renderer;
}

View File

@ -0,0 +1,32 @@
import Blockquote from '$lib/components/markdownRenderers/Blockquote.svelte';
import Code from '$lib/components/markdownRenderers/Code.svelte';
import Codespan from '$lib/components/markdownRenderers/Codespan.svelte';
import Heading from '$lib/components/markdownRenderers/Heading.svelte';
import Image from '$lib/components/markdownRenderers/Image.svelte';
import Paragraph from '$lib/components/markdownRenderers/Paragraph.svelte';
import Space from '$lib/components/markdownRenderers/Space.svelte';
import Text from '$lib/components/markdownRenderers/Text.svelte';
import Link from '$lib/shared/Link.svelte';
export const renderers = {
link: Link,
image: Image,
space: Space,
blockquote: Blockquote,
code: Code,
codespan: Codespan,
text: Text,
heading: Heading,
paragraph: Paragraph
};
export const options = {
async: false,
breaks: true,
gfm: true,
pedantic: false,
renderer: null,
silent: false,
tokenizer: null,
walkTokens: null
};

View File

@ -23,3 +23,14 @@ export function isStr(s: unknown): s is string {
export function isWhiteSpaceString(s: string) {
return s.trim() === '';
}
export function slugify(input: string) {
return String(input)
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');
}

View File

@ -1,11 +1,11 @@
import { invoke } from '$lib/backend/ipc';
import { showToast } from '$lib/notifications/toasts';
import { open } from '@tauri-apps/api/shell';
import GitUrlParse from 'git-url-parse';
import { posthog } from 'posthog-js';
export async function openExternalUrl(href: string) {
try {
await open(href);
await invoke<void>('open_url', { url: href });
} catch (e) {
if (typeof e === 'string' || e instanceof String) {
// TODO: Remove if/when we've resolved all external URL problems.

View File

@ -67,6 +67,7 @@ gitbutler-diff.workspace = true
gitbutler-operating-modes.workspace = true
gitbutler-edit-mode.workspace = true
open = "5"
url = "2.5.2"
[dependencies.tauri]
version = "1.7.0"
@ -78,7 +79,6 @@ features = [
"path-all",
"process-relaunch",
"protocol-asset",
"shell-open",
"window-maximize",
"window-start-dragging",
"window-unmaximize",

View File

@ -26,6 +26,7 @@ pub mod config;
pub mod error;
pub mod github;
pub mod modes;
pub mod open;
pub mod projects;
pub mod remotes;
pub mod repo;

View File

@ -12,8 +12,8 @@
)]
use gitbutler_tauri::{
askpass, commands, config, github, logs, menu, modes, projects, remotes, repo, secret, undo,
users, virtual_branches, zip, App, WindowState,
askpass, commands, config, github, logs, menu, modes, open, projects, remotes, repo, secret,
undo, users, virtual_branches, zip, App, WindowState,
};
use tauri::{generate_context, Manager};
use tauri_plugin_log::LogTarget;
@ -206,7 +206,8 @@ fn main() {
modes::enter_edit_mode,
modes::save_edit_and_return_to_workspace,
modes::abort_edit_and_return_to_workspace,
modes::edit_initial_index_state
modes::edit_initial_index_state,
open::open_url
])
.menu(menu::build(tauri_context.package_info()))
.on_menu_event(|event| menu::handle_event(&event))

View File

@ -1,5 +1,6 @@
use std::{env, fs};
use crate::open::open_that as open_url;
use anyhow::Context;
use gitbutler_error::{error, error::Code};
use serde_json::json;
@ -303,17 +304,13 @@ pub fn handle_event<R: Runtime>(event: &WindowMenuEvent<R>) {
'open_link: {
let result = match event.menu_item_id() {
"help/documentation" => open::that("https://docs.gitbutler.com"),
"help/github" => open::that("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => {
open::that("https://discord.com/channels/1060193121130000425/1183737922785116161")
}
"help/report-issue" => {
open::that("https://github.com/gitbutlerapp/gitbutler/issues/new")
}
"help/discord" => open::that("https://discord.com/invite/MmFkmaJ42D"),
"help/youtube" => open::that("https://www.youtube.com/@gitbutlerapp"),
"help/x" => open::that("https://x.com/gitbutler"),
"help/documentation" => open_url("https://docs.gitbutler.com"),
"help/github" => open_url("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => open_url("https://github.com/gitbutlerapp/gitbutler/releases"),
"help/report-issue" => open_url("https://github.com/gitbutlerapp/gitbutler/issues/new"),
"help/discord" => open_url("https://discord.com/invite/MmFkmaJ42D"),
"help/youtube" => open_url("https://www.youtube.com/@gitbutlerapp"),
"help/x" => open_url("https://x.com/gitbutler"),
_ => break 'open_link,
};

View File

@ -0,0 +1,75 @@
use crate::error::Error;
use anyhow::{bail, Context};
use std::env;
use tracing::instrument;
use url::Url;
pub(crate) fn open_that(path: &str) -> anyhow::Result<()> {
let target_url = Url::parse(path).with_context(|| format!("Invalid path format: '{path}'"))?;
if !["http", "https", "mailto", "vscode", "vscodium"].contains(&target_url.scheme()) {
bail!("Invalid path scheme: {}", target_url.scheme());
}
fn clean_env_vars<'a, 'b>(
var_names: &'a [&'b str],
) -> impl Iterator<Item = (&'b str, String)> + 'a {
var_names
.iter()
.filter_map(|name| env::var(name).map(|value| (*name, value)).ok())
.map(|(name, value)| {
(
name,
value
.split(':')
.filter(|path| {
!path.contains("appimage-run") && !path.contains("/tmp/.mount")
})
.collect::<Vec<_>>()
.join(":"),
)
})
}
let mut cmd_errors = Vec::new();
for mut cmd in open::commands(path) {
let cleaned_vars = clean_env_vars(&[
"APPDIR",
"GDK_PIXBUF_MODULE_FILE",
"GIO_EXTRA_MODULES",
"GIO_EXTRA_MODULES",
"GSETTINGS_SCHEMA_DIR",
"GST_PLUGIN_SYSTEM_PATH",
"GST_PLUGIN_SYSTEM_PATH_1_0",
"GTK_DATA_PREFIX",
"GTK_EXE_PREFIX",
"GTK_IM_MODULE_FILE",
"GTK_PATH",
"LD_LIBRARY_PATH",
"PATH",
"PERLLIB",
"PYTHONHOME",
"PYTHONPATH",
"QT_PLUGIN_PATH",
"XDG_DATA_DIRS",
]);
cmd.envs(cleaned_vars);
cmd.current_dir(env::temp_dir());
if cmd.status().is_ok() {
return Ok(());
} else {
cmd_errors.push(anyhow::anyhow!("Failed to execute command {:?}", cmd));
}
}
if !cmd_errors.is_empty() {
bail!("Errors occurred: {:?}", cmd_errors);
}
Ok(())
}
#[tauri::command(async)]
#[instrument(err(Debug))]
pub fn open_url(url: &str) -> Result<(), Error> {
Ok(open_that(url)?)
}

View File

@ -15,9 +15,6 @@
"readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
},
"shell": {
"open": "^((https://)|(http://)|(mailto:)|(vscode://)|(vscodium://)).+"
},
"dialog": {
"open": true
},

View File

@ -6,11 +6,6 @@
"productName": "GitButler Nightly"
},
"tauri": {
"allowlist": {
"shell": {
"open": "^((https://)|(http://)|(mailto:)|(vscode://)|(vscodium://)).+"
}
},
"bundle": {
"identifier": "com.gitbutler.app.nightly",
"icon": [

View File

@ -6,11 +6,6 @@
"productName": "GitButler"
},
"tauri": {
"allowlist": {
"shell": {
"open": "^((https://)|(http://)|(mailto:)|(vscode://)|(vscodium://)).+"
}
},
"bundle": {
"identifier": "com.gitbutler.app",
"icon": [

View File

@ -12,9 +12,6 @@
"readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
},
"shell": {
"open": "^((https://)|(http://)|(mailto:)|(vscode://)|(vscodium://)).+"
},
"dialog": {
"open": true
},