mirror of
https://github.com/unraid/api.git
synced 2026-01-07 00:59:48 -06:00
629 lines
19 KiB
Vue
629 lines
19 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
|
import { useApolloClient, useQuery } from '@vue/apollo-composable';
|
|
import { vInfiniteScroll } from '@vueuse/components';
|
|
|
|
import { ArrowDownTrayIcon, ArrowPathIcon } from '@heroicons/vue/24/outline';
|
|
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@unraid/ui';
|
|
import { useContentHighlighting } from '~/composables/useContentHighlighting';
|
|
|
|
import type { LogFileContentQuery, LogFileContentQueryVariables } from '~/composables/gql/graphql';
|
|
|
|
import { useThemeStore } from '~/store/theme';
|
|
import { GET_LOG_FILE_CONTENT } from './log.query';
|
|
import { LOG_FILE_SUBSCRIPTION } from './log.subscription';
|
|
|
|
// Get theme information
|
|
const themeStore = useThemeStore();
|
|
const isDarkMode = computed(() => themeStore.darkMode);
|
|
|
|
// Use shared highlighting logic
|
|
const { highlightContent } = useContentHighlighting();
|
|
|
|
const props = defineProps<{
|
|
logFilePath: string;
|
|
lineCount: number;
|
|
autoScroll: boolean;
|
|
highlightLanguage?: string; // Optional prop to specify the language for highlighting
|
|
clientFilter?: string; // Optional client-side filter to apply to log content
|
|
}>();
|
|
|
|
const DEFAULT_CHUNK_SIZE = 100;
|
|
const scrollViewportRef = ref<HTMLElement | null>(null);
|
|
const state = reactive({
|
|
loadedContentChunks: [] as { content: string; startLine: number }[],
|
|
currentStartLine: undefined as number | undefined,
|
|
isLoadingMore: false,
|
|
isRefreshing: false,
|
|
isAtTop: false,
|
|
canLoadMore: false,
|
|
initialLoadComplete: false,
|
|
isDownloading: false,
|
|
isSubscriptionActive: false,
|
|
});
|
|
|
|
// Get Apollo client for direct queries
|
|
const { client } = useApolloClient();
|
|
|
|
// Fetch log content
|
|
const {
|
|
result: logContentResult,
|
|
loading: loadingLogContent,
|
|
error: logContentError,
|
|
refetch: refetchLogContent,
|
|
subscribeToMore,
|
|
} = useQuery<LogFileContentQuery, LogFileContentQueryVariables>(
|
|
GET_LOG_FILE_CONTENT,
|
|
() => ({
|
|
path: props.logFilePath,
|
|
lines: props.lineCount || DEFAULT_CHUNK_SIZE,
|
|
startLine: state.currentStartLine,
|
|
}),
|
|
() => ({
|
|
enabled: !!props.logFilePath,
|
|
fetchPolicy: 'network-only',
|
|
})
|
|
);
|
|
|
|
// Force-scroll to bottom after DOM updates
|
|
const forceScrollToBottom = () => {
|
|
nextTick(() => {
|
|
if (scrollViewportRef.value) {
|
|
scrollViewportRef.value.scrollTop = scrollViewportRef.value.scrollHeight;
|
|
}
|
|
});
|
|
};
|
|
|
|
// MutationObserver to detect changes in log content
|
|
let observer: MutationObserver | null = null;
|
|
onMounted(() => {
|
|
if (scrollViewportRef.value) {
|
|
observer = new MutationObserver(() => {
|
|
if (props.autoScroll) {
|
|
forceScrollToBottom();
|
|
}
|
|
});
|
|
observer.observe(scrollViewportRef.value as unknown as Node, { childList: true, subtree: true });
|
|
}
|
|
|
|
// Start the log subscription
|
|
startLogSubscription();
|
|
});
|
|
|
|
// Cleanup observer on unmount
|
|
onUnmounted(() => {
|
|
observer?.disconnect();
|
|
});
|
|
|
|
// Handle log content updates
|
|
watch(
|
|
logContentResult,
|
|
(newResult) => {
|
|
if (!newResult?.logFile) return;
|
|
|
|
const { content, startLine } = newResult.logFile;
|
|
const effectiveStartLine = startLine || 1;
|
|
|
|
if (state.isLoadingMore) {
|
|
// Loading more historical content - prepend to existing chunks
|
|
state.loadedContentChunks.unshift({ content, startLine: effectiveStartLine });
|
|
state.isLoadingMore = false;
|
|
|
|
nextTick(() => (state.canLoadMore = true));
|
|
} else if (state.isRefreshing) {
|
|
// Refreshing - replace all content and reset state
|
|
state.loadedContentChunks = [{ content, startLine: effectiveStartLine }];
|
|
state.isRefreshing = false;
|
|
state.currentStartLine = undefined;
|
|
state.isAtTop = false;
|
|
state.initialLoadComplete = true;
|
|
|
|
nextTick(() => {
|
|
forceScrollToBottom();
|
|
setTimeout(() => (state.canLoadMore = true), 300);
|
|
});
|
|
} else {
|
|
// Initial load - replace all content
|
|
state.loadedContentChunks = [{ content, startLine: effectiveStartLine }];
|
|
|
|
nextTick(() => {
|
|
forceScrollToBottom();
|
|
state.initialLoadComplete = true;
|
|
setTimeout(() => (state.canLoadMore = true), 300);
|
|
});
|
|
}
|
|
|
|
state.isAtTop = effectiveStartLine === 1;
|
|
if (state.isAtTop) {
|
|
state.canLoadMore = false;
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Function to highlight log content using shared composable
|
|
const highlightLog = (content: string): string => {
|
|
return highlightContent(content, props.highlightLanguage);
|
|
};
|
|
|
|
// Apply client-side filtering
|
|
const filteredContent = computed(() => {
|
|
// Join chunks ensuring proper newline handling
|
|
const rawContent = state.loadedContentChunks
|
|
.map((chunk) => chunk.content)
|
|
.filter(content => content) // Remove empty chunks
|
|
.join(''); // Content should already have proper newlines
|
|
|
|
// Apply client-side filter if provided
|
|
if (props.clientFilter && props.clientFilter.trim()) {
|
|
const filterLower = props.clientFilter.toLowerCase();
|
|
const lines = rawContent.split('\n');
|
|
const filtered = lines.filter(line => line.toLowerCase().includes(filterLower));
|
|
return filtered.join('\n');
|
|
}
|
|
|
|
return rawContent;
|
|
});
|
|
|
|
// Computed properties
|
|
const logContent = computed(() => {
|
|
return highlightLog(filteredContent.value);
|
|
});
|
|
|
|
const totalLines = computed(() => logContentResult.value?.logFile?.totalLines || 0);
|
|
const shouldLoadMore = computed(() => state.canLoadMore && !state.isLoadingMore && !state.isAtTop);
|
|
|
|
// Load older log content
|
|
const loadMoreContent = async () => {
|
|
if (state.isLoadingMore || state.isAtTop || !state.canLoadMore) return;
|
|
|
|
state.isLoadingMore = true;
|
|
state.canLoadMore = false;
|
|
|
|
const firstChunk = state.loadedContentChunks[0];
|
|
if (firstChunk) {
|
|
const newStartLine = Math.max(1, firstChunk.startLine - DEFAULT_CHUNK_SIZE);
|
|
state.currentStartLine = newStartLine;
|
|
|
|
const prevScrollHeight = scrollViewportRef.value?.scrollHeight || 0;
|
|
|
|
await refetchLogContent();
|
|
|
|
nextTick(() => {
|
|
if (scrollViewportRef.value) {
|
|
scrollViewportRef.value.scrollTop += scrollViewportRef.value.scrollHeight - prevScrollHeight;
|
|
}
|
|
});
|
|
|
|
if (newStartLine === 1) {
|
|
state.isAtTop = true;
|
|
state.canLoadMore = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Download log file
|
|
const downloadLogFile = async () => {
|
|
if (!props.logFilePath || state.isDownloading) return;
|
|
|
|
try {
|
|
state.isDownloading = true;
|
|
|
|
// Get the filename from the path
|
|
const fileName = props.logFilePath.split('/').pop() || 'log.txt';
|
|
|
|
// Query for the entire log file content
|
|
const result = await client.query({
|
|
query: GET_LOG_FILE_CONTENT,
|
|
variables: {
|
|
path: props.logFilePath,
|
|
// Don't specify lines or startLine to get the entire file
|
|
},
|
|
fetchPolicy: 'network-only',
|
|
});
|
|
|
|
if (!result.data?.logFile?.content) {
|
|
throw new Error('Failed to fetch log content');
|
|
}
|
|
|
|
// Create a blob with the content
|
|
const blob = new Blob([result.data.logFile.content], { type: 'text/plain' });
|
|
|
|
// Create a download link
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = fileName;
|
|
|
|
// Trigger the download
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
|
|
// Clean up
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
} catch (error) {
|
|
console.error('Error downloading log file:', error);
|
|
alert(`Error downloading log file: ${error instanceof Error ? error.message : String(error)}`);
|
|
} finally {
|
|
state.isDownloading = false;
|
|
}
|
|
};
|
|
|
|
// Helper function to start log subscription
|
|
const startLogSubscription = () => {
|
|
if (!props.logFilePath) return;
|
|
|
|
try {
|
|
subscribeToMore({
|
|
document: LOG_FILE_SUBSCRIPTION,
|
|
variables: { path: props.logFilePath },
|
|
updateQuery: (prev, { subscriptionData }) => {
|
|
if (!subscriptionData.data || !prev) return prev;
|
|
|
|
// Set subscription as active when we receive data
|
|
state.isSubscriptionActive = true;
|
|
|
|
const existingContent = prev.logFile?.content || '';
|
|
const newContent = subscriptionData.data.logFile.content;
|
|
|
|
// Update the local state with the new content
|
|
if (newContent && state.loadedContentChunks.length > 0) {
|
|
const lastChunk = state.loadedContentChunks[state.loadedContentChunks.length - 1];
|
|
// Ensure there's a newline between the existing content and new content if needed
|
|
if (lastChunk.content && !lastChunk.content.endsWith('\n') && newContent) {
|
|
lastChunk.content += '\n' + newContent;
|
|
} else {
|
|
lastChunk.content += newContent;
|
|
}
|
|
|
|
// Force scroll to bottom if auto-scroll is enabled
|
|
if (props.autoScroll) {
|
|
nextTick(() => forceScrollToBottom());
|
|
}
|
|
}
|
|
|
|
return {
|
|
...prev,
|
|
logFile: {
|
|
...prev.logFile,
|
|
content: existingContent + newContent,
|
|
totalLines: (prev.logFile?.totalLines || 0) + (newContent.split('\n').length - 1),
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
// Set subscription as active
|
|
state.isSubscriptionActive = true;
|
|
} catch (error) {
|
|
console.error('Error starting log subscription:', error);
|
|
state.isSubscriptionActive = false;
|
|
}
|
|
};
|
|
|
|
// Refresh logs with full reset
|
|
const refreshLogContent = async () => {
|
|
// Set refresh flag to indicate we're refreshing
|
|
state.isRefreshing = true;
|
|
state.isLoadingMore = false;
|
|
state.canLoadMore = false;
|
|
|
|
// Refetch with explicit variables to ensure we get the latest logs
|
|
await refetchLogContent({
|
|
path: props.logFilePath,
|
|
lines: props.lineCount || DEFAULT_CHUNK_SIZE,
|
|
startLine: undefined, // Explicitly pass undefined to get the latest lines
|
|
});
|
|
|
|
// Restart the subscription with the same variables used for refetch
|
|
startLogSubscription();
|
|
};
|
|
|
|
watch(() => props.logFilePath, refreshLogContent);
|
|
defineExpose({ refreshLogContent });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col h-full max-h-full overflow-hidden">
|
|
<div
|
|
class="flex justify-between px-4 py-2 bg-muted text-xs text-muted-foreground shrink-0 items-center"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<span>Total lines: {{ totalLines }}</span>
|
|
<TooltipProvider v-if="state.isSubscriptionActive">
|
|
<Tooltip :delay-duration="300">
|
|
<TooltipTrigger as-child>
|
|
<div
|
|
class="w-2 h-2 rounded-full bg-green-500 animate-pulse cursor-help"
|
|
aria-hidden="true"
|
|
/>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Watching log file</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
<span>{{ state.isAtTop ? 'Showing all available lines' : 'Scroll up to load more' }}</span>
|
|
<div class="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
:disabled="loadingLogContent || state.isDownloading"
|
|
@click="downloadLogFile"
|
|
>
|
|
<ArrowDownTrayIcon
|
|
class="h-3 w-3 mr-1"
|
|
:class="{ 'animate-pulse': state.isDownloading }"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="text-sm">{{ state.isDownloading ? 'Downloading...' : 'Download' }}</span>
|
|
</Button>
|
|
<Button variant="outline" :disabled="loadingLogContent" @click="refreshLogContent">
|
|
<ArrowPathIcon class="h-3 w-3 mr-1" aria-hidden="true" />
|
|
<span class="text-sm">Refresh</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="loadingLogContent && !state.isLoadingMore"
|
|
class="flex items-center justify-center flex-1 p-4 text-muted-foreground"
|
|
>
|
|
Loading log content...
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="logContentError"
|
|
class="flex items-center justify-center flex-1 p-4 text-destructive"
|
|
>
|
|
Error loading log content: {{ logContentError.message }}
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
ref="scrollViewportRef"
|
|
v-infinite-scroll="[
|
|
loadMoreContent,
|
|
{ direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore },
|
|
]"
|
|
class="flex-1 overflow-y-auto"
|
|
:class="{ 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
|
|
>
|
|
<!-- Loading indicator for loading more content -->
|
|
<div
|
|
v-if="state.isLoadingMore"
|
|
class="sticky top-0 z-10 bg-muted/80 backdrop-blur-xs border-b border-border rounded-md mx-2 mt-2"
|
|
>
|
|
<div class="flex items-center justify-center p-2 text-xs text-primary-foreground">
|
|
<ArrowPathIcon class="h-3 w-3 mr-2 animate-spin" aria-hidden="true" />
|
|
Loading more lines...
|
|
</div>
|
|
</div>
|
|
|
|
<pre
|
|
class="font-mono whitespace-pre p-4 m-0 text-xs leading-6 hljs"
|
|
:class="{ 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
|
|
v-html="logContent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Define CSS variables for both light and dark themes */
|
|
:root {
|
|
/* Light theme colors (default) - adjusted for better readability */
|
|
--log-background: transparent;
|
|
--log-keyword-color: hsl(var(--destructive) / 0.9); /* Slightly dimmed */
|
|
--log-string-color: hsl(var(--primary) / 0.7); /* Dimmed primary color */
|
|
--log-comment-color: hsl(var(--muted-foreground));
|
|
--log-number-color: hsl(var(--accent-foreground) / 0.8); /* Slightly dimmed */
|
|
--log-timestamp-color: hsl(210, 90%, 40%); /* Darker blue for timestamps */
|
|
--log-ip-color: hsl(32, 90%, 40%); /* Darker orange for IPs */
|
|
--log-error-color: hsl(var(--destructive) / 0.9); /* Slightly dimmed */
|
|
--log-warning-color: hsl(40, 90%, 40%); /* Darker yellow for warnings */
|
|
--log-success-color: hsl(142, 70%, 35%); /* Darker green for success */
|
|
--log-error-bg: hsl(var(--destructive) / 0.08); /* Lighter background */
|
|
--log-warning-bg: hsl(40, 90%, 50% / 0.08); /* Lighter background */
|
|
--log-success-bg: hsl(142, 70%, 40% / 0.08); /* Lighter background */
|
|
}
|
|
|
|
/* Dark theme colors - use slightly different color combinations for better visibility */
|
|
.theme-dark {
|
|
--log-background: transparent;
|
|
--log-keyword-color: hsl(var(--destructive) / 0.9);
|
|
--log-string-color: hsl(var(--primary) / 0.9);
|
|
--log-comment-color: hsl(var(--muted-foreground) / 0.9);
|
|
--log-number-color: hsl(var(--accent-foreground) / 0.9);
|
|
--log-timestamp-color: hsl(210, 100%, 66%); /* Brighter blue for timestamps in dark mode */
|
|
--log-ip-color: hsl(32, 100%, 56%); /* Brighter orange for IPs in dark mode */
|
|
--log-error-color: hsl(350, 100%, 66%); /* Brighter red for errors in dark mode */
|
|
--log-warning-color: hsl(50, 100%, 60%); /* Brighter yellow for warnings in dark mode */
|
|
--log-success-color: hsl(120, 100%, 45%); /* Brighter green for success in dark mode */
|
|
--log-error-bg: hsl(350, 100%, 40% / 0.15);
|
|
--log-warning-bg: hsl(50, 100%, 50% / 0.15);
|
|
--log-success-bg: hsl(120, 100%, 40% / 0.15);
|
|
}
|
|
|
|
/* Add some basic styling for the highlighted logs */
|
|
.hljs {
|
|
background: var(--log-background);
|
|
}
|
|
|
|
/* Style for error messages */
|
|
.hljs .hljs-keyword,
|
|
.hljs .hljs-selector-tag,
|
|
.hljs .hljs-literal,
|
|
.hljs .hljs-section,
|
|
.hljs .hljs-link {
|
|
color: var(--log-keyword-color);
|
|
}
|
|
|
|
/* Style for warnings */
|
|
.hljs .hljs-string,
|
|
.hljs .hljs-title,
|
|
.hljs .hljs-name,
|
|
.hljs .hljs-type,
|
|
.hljs .hljs-attribute,
|
|
.hljs .hljs-symbol,
|
|
.hljs .hljs-bullet,
|
|
.hljs .hljs-built_in,
|
|
.hljs .hljs-addition,
|
|
.hljs .hljs-variable,
|
|
.hljs .hljs-template-tag,
|
|
.hljs .hljs-template-variable {
|
|
color: var(--log-string-color);
|
|
}
|
|
|
|
/* Style for info messages */
|
|
.hljs .hljs-comment,
|
|
.hljs .hljs-quote,
|
|
.hljs .hljs-deletion,
|
|
.hljs .hljs-meta {
|
|
color: var(--log-comment-color);
|
|
}
|
|
|
|
/* Style for timestamps and IDs */
|
|
.hljs .hljs-number,
|
|
.hljs .hljs-regexp,
|
|
.hljs .hljs-literal,
|
|
.hljs .hljs-variable,
|
|
.hljs .hljs-template-variable,
|
|
.hljs .hljs-tag .hljs-attr,
|
|
.hljs .hljs-tag .hljs-string,
|
|
.hljs .hljs-attr,
|
|
.hljs .hljs-string {
|
|
color: var(--log-number-color);
|
|
}
|
|
|
|
/* Style for success messages */
|
|
.hljs .hljs-function .hljs-keyword,
|
|
.hljs .hljs-class .hljs-keyword {
|
|
color: var(--log-success-color);
|
|
}
|
|
|
|
/* Custom log pattern styles */
|
|
.hljs-timestamp {
|
|
color: var(--log-timestamp-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
.hljs-ip {
|
|
color: var(--log-ip-color);
|
|
}
|
|
|
|
/* Error line and keyword styling */
|
|
.hljs-error {
|
|
display: inline-block;
|
|
width: 100%;
|
|
padding-left: 4px;
|
|
margin-left: -4px;
|
|
}
|
|
|
|
.theme-light .hljs-error {
|
|
background-color: hsl(var(--destructive) / 0.05);
|
|
border-left: 2px solid hsl(var(--destructive) / 0.7);
|
|
}
|
|
|
|
.theme-dark .hljs-error {
|
|
background-color: var(--log-error-bg);
|
|
}
|
|
|
|
.hljs-error-keyword {
|
|
color: var(--log-error-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Warning line and keyword styling */
|
|
.hljs-warning {
|
|
display: inline-block;
|
|
width: 100%;
|
|
padding-left: 4px;
|
|
margin-left: -4px;
|
|
}
|
|
|
|
.theme-light .hljs-warning {
|
|
background-color: hsl(40, 90%, 50% / 0.05);
|
|
border-left: 2px solid hsl(40, 90%, 40% / 0.7);
|
|
}
|
|
|
|
.theme-dark .hljs-warning {
|
|
background-color: var(--log-warning-bg);
|
|
}
|
|
|
|
.hljs-warning-keyword {
|
|
color: var(--log-warning-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Success line and keyword styling */
|
|
.hljs-success {
|
|
display: inline-block;
|
|
width: 100%;
|
|
padding-left: 4px;
|
|
margin-left: -4px;
|
|
}
|
|
|
|
.theme-light .hljs-success {
|
|
background-color: hsl(142, 70%, 40% / 0.05);
|
|
border-left: 2px solid hsl(142, 70%, 35% / 0.7);
|
|
}
|
|
|
|
.theme-dark .hljs-success {
|
|
background-color: var(--log-success-bg);
|
|
}
|
|
|
|
.hljs-success-keyword {
|
|
color: var(--log-success-color);
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* ANSI color styles for ansi_up output - using format: ansi-{color}-fg/bg */
|
|
/* Foreground colors */
|
|
:deep(.ansi-black-fg) { color: #000; }
|
|
:deep(.ansi-red-fg) { color: #c91b00; }
|
|
:deep(.ansi-green-fg) { color: #00c200; }
|
|
:deep(.ansi-yellow-fg) { color: #c7c400; }
|
|
:deep(.ansi-blue-fg) { color: #0225c7; }
|
|
:deep(.ansi-magenta-fg) { color: #c930c7; }
|
|
:deep(.ansi-cyan-fg) { color: #00c5c7; }
|
|
:deep(.ansi-white-fg) { color: #c7c7c7; }
|
|
|
|
/* Bright foreground colors */
|
|
:deep(.ansi-bright-black-fg) { color: #676767; }
|
|
:deep(.ansi-bright-red-fg) { color: #ff6d67; }
|
|
:deep(.ansi-bright-green-fg) { color: #5ff967; }
|
|
:deep(.ansi-bright-yellow-fg) { color: #fefb67; }
|
|
:deep(.ansi-bright-blue-fg) { color: #6871ff; }
|
|
:deep(.ansi-bright-magenta-fg) { color: #ff76ff; }
|
|
:deep(.ansi-bright-cyan-fg) { color: #5ffdff; }
|
|
:deep(.ansi-bright-white-fg) { color: #fff; }
|
|
|
|
/* Background colors */
|
|
:deep(.ansi-black-bg) { background-color: #000; }
|
|
:deep(.ansi-red-bg) { background-color: #c91b00; }
|
|
:deep(.ansi-green-bg) { background-color: #00c200; }
|
|
:deep(.ansi-yellow-bg) { background-color: #c7c400; }
|
|
:deep(.ansi-blue-bg) { background-color: #0225c7; }
|
|
:deep(.ansi-magenta-bg) { background-color: #c930c7; }
|
|
:deep(.ansi-cyan-bg) { background-color: #00c5c7; }
|
|
:deep(.ansi-white-bg) { background-color: #c7c7c7; }
|
|
|
|
/* Bright background colors */
|
|
:deep(.ansi-bright-black-bg) { background-color: #676767; }
|
|
:deep(.ansi-bright-red-bg) { background-color: #ff6d67; }
|
|
:deep(.ansi-bright-green-bg) { background-color: #5ff967; }
|
|
:deep(.ansi-bright-yellow-bg) { background-color: #fefb67; }
|
|
:deep(.ansi-bright-blue-bg) { background-color: #6871ff; }
|
|
:deep(.ansi-bright-magenta-bg) { background-color: #ff76ff; }
|
|
:deep(.ansi-bright-cyan-bg) { background-color: #5ffdff; }
|
|
:deep(.ansi-bright-white-bg) { background-color: #fff; }
|
|
|
|
/* Additional ansi_up classes */
|
|
:deep(.ansi-bold) { font-weight: bold; }
|
|
:deep(.ansi-italic) { font-style: italic; }
|
|
:deep(.ansi-underline) { text-decoration: underline; }
|
|
:deep(.ansi-strike) { text-decoration: line-through; }
|
|
</style>
|