fix(frontend): preserve ansi colors in terminal effects

Limit stroke and shadow effects to default-foreground terminal text so
ANSI-colored output keeps its intended visual meaning.

Also enable terminal text stroke and shadow by default in frontend and
backend fallbacks, and align the dark preset and default xterm theme
with the new green night palette.
This commit is contained in:
yinjianm
2026-03-25 06:20:33 +08:00
parent d74e84c87b
commit 7e430cb983
18 changed files with 338 additions and 75 deletions
+32 -6
View File
@@ -37,6 +37,7 @@ let observedElement: HTMLElement | null = null; // +++ Store the observed elemen
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
let selectionListenerDisposable: IDisposable | null = null; // +++ 提升声明并添加类型 +++
let scrollListenerDisposable: IDisposable | null = null;
let renderListenerDisposable: IDisposable | null = null;
let lastResizeObserverWidth = 0;
let lastResizeObserverHeight = 0;
const RESIZE_THRESHOLD = 0.5; // px
@@ -132,6 +133,25 @@ const restoreViewportSnapshot = (term: Terminal, snapshot?: TerminalViewportSnap
syncViewportTracking(term);
};
const markExplicitForegroundSpans = () => {
const hostElement = terminalRef.value;
if (!hostElement) {
return;
}
const rowSpans = hostElement.querySelectorAll<HTMLElement>('.xterm-rows span');
rowSpans.forEach((span) => {
const hasExplicitForeground =
span.className.includes('xterm-fg-') || span.style.color !== '';
if (hasExplicitForeground) {
span.dataset.explicitForeground = 'true';
} else {
delete span.dataset.explicitForeground;
}
});
};
// 防抖处理由 ResizeObserver 触发的 resize 事件
const debouncedEmitResize = debounce((term: Terminal) => {
if (term && props.isActive) { // 仅当标签仍处于活动状态时才发送防抖后的 resize
@@ -311,6 +331,7 @@ onMounted(() => {
// terminal.open() 同步执行完毕后,可以认为 Xterm 已尝试附加到 DOM
isTerminalDomReady.value = true; // +++ 直接在此处设置 DOM 准备就绪状态 +++
console.log(`[Terminal ${props.sessionId}] Xterm open() called, considering DOM ready for initial style checks.`);
markExplicitForegroundSpans();
// 适应容器大小
fitAndEmitResizeNow(terminal);
@@ -320,6 +341,10 @@ onMounted(() => {
emitWorkspaceEvent('terminal:input', { sessionId: props.sessionId, data });
});
renderListenerDisposable = terminal.onRender(() => {
markExplicitForegroundSpans();
});
scrollListenerDisposable = terminal.onScroll(() => {
if (terminal && props.isActive) {
syncViewportTracking(terminal);
@@ -665,6 +690,10 @@ onBeforeUnmount(() => {
scrollListenerDisposable.dispose();
}
if (renderListenerDisposable) {
renderListenerDisposable.dispose();
}
// 确保在卸载时移除右键监听器
removeContextMenuListener();
@@ -739,6 +768,7 @@ const applyTerminalTextStyles = () => {
} else {
hostElement.style.removeProperty('--terminal-shadow');
}
markExplicitForegroundSpans();
// console.log('[Terminal] Applied text styles. Stroke enabled:', terminalTextStrokeEnabled.value, 'Shadow enabled:', terminalTextShadowEnabled.value);
}
};
@@ -814,9 +844,7 @@ watchEffect(() => {
}
/* 文字描边和阴影样式 */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows span),
.terminal-inner-container.has-text-stroke :deep(.xterm-rows div > span), /* 更具体地针对嵌套 span */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows div) { /* 针对直接包含文本的 div */
.terminal-inner-container.has-text-stroke :deep(.xterm-rows span:not([data-explicit-foreground="true"])) {
-webkit-text-stroke-width: var(--terminal-stroke-width);
-webkit-text-stroke-color: var(--terminal-stroke-color);
text-stroke-width: var(--terminal-stroke-width);
@@ -826,9 +854,7 @@ watchEffect(() => {
-webkit-paint-order: stroke fill; /* 兼容 WebKit */
}
.terminal-inner-container.has-text-shadow :deep(.xterm-rows span),
.terminal-inner-container.has-text-shadow :deep(.xterm-rows div > span),
.terminal-inner-container.has-text-shadow :deep(.xterm-rows div) {
.terminal-inner-container.has-text-shadow :deep(.xterm-rows span:not([data-explicit-foreground="true"])) {
text-shadow: var(--terminal-shadow);
}
@@ -41,11 +41,11 @@ const {
const editableTerminalFontFamily = ref('');
const editableTerminalFontSize = ref(14);
const editableTerminalTextStrokeEnabled = ref(false);
const editableTerminalTextStrokeEnabled = ref(true);
const editableTerminalTextStrokeWidth = ref(1);
const editableTerminalTextStrokeColor = ref('#000000');
const editableTerminalTextShadowEnabled = ref(false);
const editableTerminalTextShadowEnabled = ref(true);
const editableTerminalTextShadowOffsetX = ref(0);
const editableTerminalTextShadowOffsetY = ref(0);
const editableTerminalTextShadowBlur = ref(0);
@@ -725,4 +725,4 @@ watch(() => props.isEditingTheme, (isEditing) => {
<button @click="handleSaveEditingTheme" class="px-4 md:px-5 py-2 rounded font-bold border border-button bg-button text-button-text hover:bg-button-hover hover:border-button-hover disabled:opacity-60 disabled:cursor-not-allowed text-sm md:text-base">{{ t('common.save') }}</button>
</div>
</section>
</template>
</template>
@@ -18,30 +18,30 @@ const themeParseError = ref<string | null>(null);
// 定义黑暗模式主题变量
const darkModeTheme = {
'--app-bg-color': '#212529',
'--text-color': '#e9ecef',
'--text-color-secondary': '#adb5bd',
'--border-color': '#495057',
'--link-color': '#BB86FC',
'--link-hover-color': '#D1A9FF',
'--link-active-color': '#A06CD5',
'--link-active-bg-color': 'rgba(160, 108, 213, 0.2)',
'--app-bg-color': '#161816',
'--text-color': '#d8e6d2',
'--text-color-secondary': '#8d9887',
'--border-color': '#2b332c',
'--link-color': '#37c66a',
'--link-hover-color': '#62e38e',
'--link-active-color': '#45d978',
'--link-active-bg-color': 'rgba(69, 217, 120, 0.14)',
'--nav-item-active-bg-color': 'var(--link-active-bg-color)',
'--header-bg-color': '#343a40',
'--footer-bg-color': '#343a40',
'--button-bg-color': 'var(--link-active-color)',
'--button-text-color': '#ffffff',
'--button-hover-bg-color': '#8E44AD',
'--header-bg-color': '#1b1f1b',
'--footer-bg-color': '#1b1f1b',
'--button-bg-color': '#203126',
'--button-text-color': '#9aefad',
'--button-hover-bg-color': '#294232',
'--icon-color': 'var(--text-color-secondary)',
'--icon-hover-color': 'var(--link-hover-color)',
'--split-line-color': 'var(--border-color)',
'--split-line-hover-color': 'var(--border-color)',
'--split-line-hover-color': '#3b6045',
'--input-focus-border-color': 'var(--link-active-color)',
'--input-focus-glow': 'var(--link-active-color)',
'--overlay-bg-color': 'rgba(0, 0, 0, 0.8)',
'--color-success': '#5cb85c',
'--color-error': '#d9534f',
'--color-warning': '#f0ad4e',
'--overlay-bg-color': 'rgba(0, 0, 0, 0.84)',
'--color-success': '#3fdc78',
'--color-error': '#d86a4d',
'--color-warning': '#d1a445',
'--font-family-sans-serif': 'sans-serif',
'--base-padding': '1rem',
'--base-margin': '0.5rem'
@@ -265,4 +265,4 @@ defineExpose({
</div>
<p v-if="themeParseError" class="text-error-text bg-error/10 border border-error/30 px-3 py-2 rounded text-sm mt-2">{{ themeParseError }}</p>
</section>
</template>
</template>