Skip to content

Commit

Permalink
feat(terminal): bug fixes and performance improvements
Browse files Browse the repository at this point in the history
Signed-off-by: Evan Song <[email protected]>
  • Loading branch information
ferothefox committed Oct 22, 2024
1 parent 37c112c commit 9f2c7c4
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 15 deletions.
78 changes: 69 additions & 9 deletions apps/frontend/src/components/ui/servers/LogParser.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
<template>
<div
class="parsed-log w-full overflow-hidden whitespace-nowrap text-wrap px-6 py-1 selection:bg-black selection:text-white dark:selection:bg-white dark:selection:text-black"
v-html="sanitizedLog"
></div>
class="parsed-log group relative w-full overflow-hidden px-6 py-1"
@mouseenter="showCopyButton = true"
@mouseleave="showCopyButton = false"
>
<div
ref="logContent"
class="log-content whitespace-pre-wrap selection:bg-black selection:text-white dark:selection:bg-white dark:selection:text-black"
v-html="sanitizedLog"
></div>
<Transition name="fade">
<button
v-show="showCopyButton"
class="absolute right-4 top-1/2 -translate-y-1/2 select-none rounded-md bg-bg-raised px-2 py-1 text-xs text-contrast opacity-0 transition-opacity duration-150 hover:bg-button-bg group-hover:opacity-100"
aria-label="Copy line"
@click="copyLog"
>
<span v-if="!copied">Copy</span>
<span v-else>Copied!</span>
</button>
</Transition>
</div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { ref, computed } from "vue";
import Convert from "ansi-to-html";
import DOMPurify from "dompurify";
const props = defineProps<{
log: string;
}>();
const logContent = ref<HTMLElement | null>(null);
const showCopyButton = ref(false);
const copied = ref(false);
const colors = {
30: "#101010",
31: "#EFA6A2",
Expand Down Expand Up @@ -47,18 +69,15 @@ const usernameRegex = /&lt;([^&]+)&gt;/g;
const sanitizedLog = computed(() => {
let html = convert.toHtml(props.log);
html = html.replace(
urlRegex,
(url) =>
`<a style="color:var(--color-link);text-decoration:underline;" href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`,
);
html = html.replace(
usernameRegex,
(_, username) => `<span class="minecraft-username">&lt;${username}&gt;</span>`,
);
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["span", "a"],
ALLOWED_ATTR: ["style", "href", "target", "rel", "class"],
Expand All @@ -67,22 +86,63 @@ const sanitizedLog = computed(() => {
USE_PROFILES: { html: true },
});
});
const copyLog = async () => {
if (!logContent.value) return;
try {
const textContent = logContent.value.textContent || "";
await navigator.clipboard.writeText(textContent);
copied.value = true;
setTimeout(() => {
copied.value = false;
}, 2000);
} catch (err) {
console.error("Failed to copy text:", err);
}
};
</script>

<style scoped>
.parsed-log:hover {
transition: none;
}
.parsed-log {
transition: 80ms;
}
html.light-mode .parsed-log:hover {
background-color: #ccc;
}
html.dark-mode .parsed-log:hover {
background-color: #333;
background-color: #222;
}
html.oled-mode .parsed-log:hover {
background-color: #333;
background-color: #222;
}
.minecraft-username {
font-weight: bold;
}
.fade-enter-active,
.fade-leave-active {
transition: 80ms ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
::v-deep(.log-content) {
user-select: text;
}
::v-deep(.log-content *) {
user-select: text;
}
</style>
11 changes: 7 additions & 4 deletions apps/frontend/src/components/ui/servers/PanelTerminal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div
data-pyro-terminal
:class="[
'terminal-font console relative flex h-full w-full flex-col items-center justify-between overflow-hidden rounded-t-xl pb-4 text-sm transition-transform duration-300',
'terminal-font console relative flex h-full w-full select-text flex-col items-center justify-between overflow-hidden rounded-t-xl pb-4 text-sm transition-transform duration-300',
{ 'scale-fullscreen fixed inset-0 z-50 !rounded-none': isFullScreen },
]"
tabindex="-1"
Expand Down Expand Up @@ -55,20 +55,23 @@
<div
ref="scrollContainer"
data-pyro-terminal-root
class="absolute left-0 top-0 h-full w-full select-text overflow-x-auto overflow-y-auto py-6 pb-[72px]"
class="absolute left-0 top-0 h-full w-full overflow-x-auto overflow-y-auto py-6 pb-[72px]"
@scroll="handleScroll"
>
<div data-pyro-terminal-virtual-height-watcher :style="{ height: `${totalHeight}px` }">
<ul
class="m-0 select-text list-none p-0"
class="m-0 list-none p-0"
data-pyro-terminal-virtual-list
:style="{ transform: `translateY(${offsetY}px)` }"
aria-live="polite"
role="listbox"
>
<template v-for="(item, index) in visibleItems" :key="index">
<li
ref="itemRefs"
class="relative w-full select-text list-none"
class="relative w-full list-none"
:data-pyro-terminal-recycle-tracker="index"
aria-setsize="-1"
>
<UiServersLogParser :log="item" />
</li>
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/pages/servers/manage/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ const connectWebSocket = () => {
isConnected.value = true;
isReconnecting.value = false;
isLoading.value = false;
consoleOutput.value.push("\nReady! Welcome to your Modrinth Server ༼ つ ◕_◕ ༽つ");
consoleOutput.value.push("\nPress the green start button to start your server!");
consoleOutput.value.push("Ready! Welcome to your Modrinth Server ༼ つ ◕_◕ ༽つ");
consoleOutput.value.push("Press the green start button to start your server!");
if (reconnectInterval.value) {
clearInterval(reconnectInterval.value);
reconnectInterval.value = null;
Expand Down

0 comments on commit 9f2c7c4

Please sign in to comment.