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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
import gbLogoSvg from '$lib/assets/gb-logo.svg?raw'; import gbLogoSvg from '$lib/assets/gb-logo.svg?raw';
import { User } from '$lib/stores/user'; import { User } from '$lib/stores/user';
import { getContextStore } from '$lib/utils/context'; import { getContextStore } from '$lib/utils/context';
import { openExternalUrl } from '$lib/utils/url';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
@ -68,22 +69,20 @@
<div class="right-side__meta"> <div class="right-side__meta">
<div class="right-side__links"> <div class="right-side__links">
<a <button
class="right-side__link" class="right-side__link"
target="_blank" onclick={async () => await openExternalUrl('https://docs.gitbutler.com/')}
href="https://docs.gitbutler.com/features/virtual-branches/branch-lanes"
> >
<Icon name="docs" opacity={0.6} /> <Icon name="docs" opacity={0.6} />
<span class="text-14 text-semibold">GitButler docs</span> <span class="text-14 text-semibold">GitButler docs</span>
</a> </button>
<a <button
class="right-side__link" class="right-side__link"
target="_blank" onclick={async () => await openExternalUrl('https://discord.com/invite/MmFkmaJ42D')}
href="https://discord.com/invite/MmFkmaJ42D"
> >
<Icon name="discord" opacity={0.6} /> <Icon name="discord" opacity={0.6} />
<span class="text-14 text-semibold">Join community</span> <span class="text-14 text-semibold">Join community</span>
</a> </button>
</div> </div>
<div class="wordmark"> <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 // This is always displayed in the context of not having a cooresponding vbranch or remote
import { Project } from '$lib/backend/projects'; import { Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService'; import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import Markdown from '$lib/components/Markdown.svelte';
import { RemotesService } from '$lib/remotes/service'; import { RemotesService } from '$lib/remotes/service';
import Link from '$lib/shared/Link.svelte'; import Link from '$lib/shared/Link.svelte';
import TextBox from '$lib/shared/TextBox.svelte'; import TextBox from '$lib/shared/TextBox.svelte';
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import { getMarkdownRenderer } from '$lib/utils/markdown';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { remoteUrlIsHttp } from '$lib/utils/url'; import { remoteUrlIsHttp } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { VirtualBranchService } from '$lib/vbranches/virtualBranch'; import { VirtualBranchService } from '$lib/vbranches/virtualBranch';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import { marked } from 'marked';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import type { PullRequest } from '$lib/gitHost/interface/types'; import type { PullRequest } from '$lib/gitHost/interface/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
@ -25,7 +24,6 @@
const remotesService = getContext(RemotesService); const remotesService = getContext(RemotesService);
const baseBranchService = getContext(BaseBranchService); const baseBranchService = getContext(BaseBranchService);
const virtualBranchService = getContext(VirtualBranchService); const virtualBranchService = getContext(VirtualBranchService);
const renderer = getMarkdownRenderer();
let remoteName = structuredClone(pullrequest.repoName) || ''; let remoteName = structuredClone(pullrequest.repoName) || '';
let createRemoteModal: Modal | undefined; let createRemoteModal: Modal | undefined;
@ -108,9 +106,9 @@
{#if pullrequest.draft} {#if pullrequest.draft}
<Button size="tag" clickable={false} style="neutral" icon="draft-pr-small">Draft</Button> <Button size="tag" clickable={false} style="neutral" icon="draft-pr-small">Draft</Button>
{:else} {:else}
<Button size="tag" clickable={false} style="success" kind="solid" icon="pr-small" <Button size="tag" clickable={false} style="success" kind="solid" icon="pr-small">
>Open</Button Open
> </Button>
{/if} {/if}
</div> </div>
@ -130,7 +128,7 @@
</div> </div>
{#if pullrequest.body} {#if pullrequest.body}
<div class="markdown"> <div class="markdown">
{@html marked.parse(pullrequest.body, { renderer })} <Markdown content={pullrequest.body} />
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -7,7 +7,7 @@
export let disabled = false; export let disabled = false;
</script> </script>
<button class="menu-item" class:disabled {disabled} on:mousedown on:click> <button class="menu-item" class:disabled {disabled} on:click>
{#if icon} {#if icon}
<Icon name={icon} /> <Icon name={icon} />
{/if} {/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 { getContext } from '$lib/utils/context';
import { computeFileStatus } from '$lib/utils/fileStatus'; import { computeFileStatus } from '$lib/utils/fileStatus';
import * as toasts from '$lib/utils/toasts'; import * as toasts from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { LocalFile, type AnyFile } from '$lib/vbranches/types'; import { LocalFile, type AnyFile } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte'; import Modal from '@gitbutler/ui/Modal.svelte';
import { join } from '@tauri-apps/api/path'; import { join } from '@tauri-apps/api/path';
import { open as openFile } from '@tauri-apps/api/shell';
export let branchId: string | undefined; export let branchId: string | undefined;
export let target: HTMLElement | undefined; export let target: HTMLElement | undefined;
@ -85,7 +85,7 @@
if (!project) return; if (!project) return;
for (let file of item.files) { for (let file of item.files) {
const absPath = await join(project.vscodePath, file.path); const absPath = await join(project.vscodePath, file.path);
openFile(`${$editor}://file${absPath}`); openExternalUrl(`${$editor}://file${absPath}`);
} }
contextMenu.close(); contextMenu.close();
} catch { } catch {

View File

@ -87,7 +87,7 @@ export class GitHubPrService implements GitHostPrService {
showToast({ showToast({
title: 'Failed to fetch pull request template', 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' style: 'neutral'
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,29 @@
<script lang="ts"> <script lang="ts">
import { openExternalUrl } from '$lib/utils/url'; import { openExternalUrl } from '$lib/utils/url';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { onMount } from 'svelte'; import { onMount, type Snippet } from 'svelte';
let classes = ''; interface Props {
export { classes as class }; href: string;
export let target: '_blank' | '_self' | '_parent' | '_top' | undefined = undefined; children: Snippet;
export let rel: string | undefined = undefined; class?: string;
export let role: 'basic' | 'primary' | 'error' = 'basic'; target?: '_blank' | '_self' | '_parent' | '_top' | undefined;
export let disabled = false; rel?: string | undefined;
export let href: string | undefined = 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(() => { onMount(() => {
if (element) { if (element) {
@ -19,33 +31,31 @@
} }
}); });
$: isExternal = href?.startsWith('http'); const isExternal = $derived(href?.startsWith('http'));
</script> </script>
{#if href} <a
<a {href}
{href} {target}
{target} {rel}
{rel} class="link {role} {classes}"
class="link {role} {classes}" bind:this={element}
bind:this={element} class:disabled
class:disabled onclick={(e) => {
on:click={(e) => { if (href && isExternal) {
if (href && isExternal) { e.preventDefault();
e.preventDefault(); e.stopPropagation();
e.stopPropagation(); openExternalUrl(href);
openExternalUrl(href); }
} }}
}} >
> {@render children()}
<slot /> {#if isExternal}
{#if isExternal} <div class="link-icon">
<div class="link-icon"> <Icon name="open-link" />
<Icon name="open-link" /> </div>
</div> {/if}
{/if} </a>
</a>
{/if}
<style lang="postcss"> <style lang="postcss">
.link { .link {
@ -62,6 +72,7 @@
text-decoration: none; text-decoration: none;
} }
} }
.link-icon { .link-icon {
flex-shrink: 0; 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) { export function isWhiteSpaceString(s: string) {
return s.trim() === ''; 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 { showToast } from '$lib/notifications/toasts';
import { open } from '@tauri-apps/api/shell';
import GitUrlParse from 'git-url-parse'; import GitUrlParse from 'git-url-parse';
import { posthog } from 'posthog-js'; import { posthog } from 'posthog-js';
export async function openExternalUrl(href: string) { export async function openExternalUrl(href: string) {
try { try {
await open(href); await invoke<void>('open_url', { url: href });
} catch (e) { } catch (e) {
if (typeof e === 'string' || e instanceof String) { if (typeof e === 'string' || e instanceof String) {
// TODO: Remove if/when we've resolved all external URL problems. // 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-operating-modes.workspace = true
gitbutler-edit-mode.workspace = true gitbutler-edit-mode.workspace = true
open = "5" open = "5"
url = "2.5.2"
[dependencies.tauri] [dependencies.tauri]
version = "1.7.0" version = "1.7.0"
@ -78,7 +79,6 @@ features = [
"path-all", "path-all",
"process-relaunch", "process-relaunch",
"protocol-asset", "protocol-asset",
"shell-open",
"window-maximize", "window-maximize",
"window-start-dragging", "window-start-dragging",
"window-unmaximize", "window-unmaximize",

View File

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

View File

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

View File

@ -1,5 +1,6 @@
use std::{env, fs}; use std::{env, fs};
use crate::open::open_that as open_url;
use anyhow::Context; use anyhow::Context;
use gitbutler_error::{error, error::Code}; use gitbutler_error::{error, error::Code};
use serde_json::json; use serde_json::json;
@ -303,17 +304,13 @@ pub fn handle_event<R: Runtime>(event: &WindowMenuEvent<R>) {
'open_link: { 'open_link: {
let result = match event.menu_item_id() { let result = match event.menu_item_id() {
"help/documentation" => open::that("https://docs.gitbutler.com"), "help/documentation" => open_url("https://docs.gitbutler.com"),
"help/github" => open::that("https://github.com/gitbutlerapp/gitbutler"), "help/github" => open_url("https://github.com/gitbutlerapp/gitbutler"),
"help/release-notes" => { "help/release-notes" => open_url("https://github.com/gitbutlerapp/gitbutler/releases"),
open::that("https://discord.com/channels/1060193121130000425/1183737922785116161") "help/report-issue" => open_url("https://github.com/gitbutlerapp/gitbutler/issues/new"),
} "help/discord" => open_url("https://discord.com/invite/MmFkmaJ42D"),
"help/report-issue" => { "help/youtube" => open_url("https://www.youtube.com/@gitbutlerapp"),
open::that("https://github.com/gitbutlerapp/gitbutler/issues/new") "help/x" => open_url("https://x.com/gitbutler"),
}
"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"),
_ => break 'open_link, _ => 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, "readFile": true,
"scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"] "scope": ["$APPCACHE/archives/*", "$RESOURCE/_up_/scripts/*"]
}, },
"shell": {
"open": "^((https://)|(http://)|(mailto:)|(vscode://)|(vscodium://)).+"
},
"dialog": { "dialog": {
"open": true "open": true
}, },

View File

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

View File

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

View File

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