import { TextAttributes, RGBA } from "@opentui/core"
import { For, type JSX, createMemo } from "solid-js"
import { useTheme, tint } from "@tui/context/theme"
import { logo, go, marks } from "@/cli/logo"
const SHADOW_MARKER = new RegExp(`[${marks}]`)
type LogoType = {
left: string[]
right: string[]
charMap?: Record<string, string>
charOpacity?: Record<string, number>
}
const typedLogo = logo as LogoType
type TextSegment = { text: string; muted: boolean; link: boolean }
function parseSegments(line: string): TextSegment[] {
const segments: TextSegment[] = []
let s = line
while (s.length > 0) {
if (s.startsWith("{muted}")) {
s = s.slice(7)
const end = s.indexOf("{/muted}")
if (end === -1) break
segments.push({ text: s.slice(0, end), muted: true, link: false })
s = s.slice(end + 8)
continue
}
if (s.startsWith("{link}")) {
s = s.slice(6)
const end = s.indexOf("{/link}")
if (end === -1) break
segments.push({ text: s.slice(0, end), muted: false, link: true })
s = s.slice(end + 7)
continue
}
const a = s.indexOf("{muted}")
const b = s.indexOf("{link}")
const candidates = [a, b].filter((x) => x >= 0)
const n = candidates.length ? Math.min(...candidates) : s.length
const plain = s.slice(0, n)
if (plain) {
segments.push({ text: plain, muted: false, link: false })
}
s = s.slice(n)
}
return segments
}
export function Logo(props: { shape?: LogoType; column?: "left" | "right" } = {}) {
const { theme } = useTheme()
const white = RGBA.fromInts(255, 255, 255)
const activeShape = props.shape ?? typedLogo
function getGradientColor(position: number, totalWidth: number): RGBA {
const ratio = Math.max(0, Math.min(1, position / totalWidth))
const r = Math.round(77 + (220 - 77) * ratio)
const g = Math.round(43 + (73 - 43) * ratio)
const b = Math.round(251 + (170 - 251) * ratio)
return RGBA.fromInts(r, g, b)
}
const totalWidth = createMemo(() => {
let maxWidth = 0
for (const line of activeShape.left) {
if (line.length > maxWidth) maxWidth = line.length
}
return maxWidth
})
const renderTextSegment = (segment: TextSegment): JSX.Element[] => {
const fg = segment.muted ? theme.textMuted : segment.link ? theme.info : white
const attrs = segment.link ? TextAttributes.BOLD : undefined
return segment.text.split("").map((ch) =>
ch === " " ? <text selectable={false}> </text> : <text fg={fg} attributes={attrs} selectable={false}>{ch}</text>
)
}
const renderLogoRightRow = (rightLine: string, lineIndex: number): JSX.Element[] => {
const hasTags = rightLine.includes("{muted}") || rightLine.includes("{link}")
if (lineIndex === 0) {
return [<text fg={white} attributes={TextAttributes.BOLD} selectable={false}>{rightLine}</text>]
}
if (lineIndex === 6 && rightLine.includes("Powered by")) {
return [<text fg={white} selectable={false}>{rightLine}</text>]
}
if (lineIndex === 8) {
const dim = tint(theme.background, theme.textMuted, 0.5)
const els: JSX.Element[] = []
for (const ch of rightLine) {
if (ch === " ") els.push(<text selectable={false}> </text>)
else els.push(<text fg={dim} selectable={false}>{ch}</text>)
}
return els
}
if (hasTags) {
const segments = parseSegments(rightLine)
if (segments.length === 0) {
const stripped = rightLine.replace(/\{muted\}|\{\/muted\}|\{link\}|\{\/link\}/g, "")
return renderTextSegment({ text: stripped, muted: false, link: false })
}
return segments.flatMap(renderTextSegment)
}
if (lineIndex === 5) {
return renderTextSegment({ text: rightLine, muted: true, link: false })
}
return renderTextSegment({ text: rightLine, muted: false, link: false })
}
const renderLine = (
line: string,
startPosition: number,
bold: boolean,
useGradient: boolean,
): JSX.Element[] => {
const attrs = bold ? TextAttributes.BOLD : undefined
const elements: JSX.Element[] = []
let i = 0
let currentPosition = startPosition
while (i < line.length) {
const rest = line.slice(i)
const markerIndex = marks.length > 0 ? rest.search(SHADOW_MARKER) : -1
if (markerIndex === -1) {
const text = rest
for (let j = 0; j < text.length; j++) {
let char = text[j]
if (activeShape.charMap?.[char]) char = activeShape.charMap[char]
if (char === " ") {
elements.push(<text attributes={attrs} selectable={false}> </text>)
} else {
let fg = useGradient ? getGradientColor(currentPosition + j, totalWidth()) : white
const opacity = activeShape.charOpacity?.[text[j]]
if (useGradient && typeof opacity === "number") fg = tint(theme.background, fg, opacity)
elements.push(
<text fg={fg} attributes={attrs} selectable={false}>
{char}
</text>,
)
}
}
break
}
if (markerIndex > 0) {
const text = rest.slice(0, markerIndex)
for (let j = 0; j < text.length; j++) {
let char = text[j]
if (activeShape.charMap?.[char]) char = activeShape.charMap[char]
if (char === " ") {
elements.push(<text attributes={attrs} selectable={false}> </text>)
} else {
let fg = useGradient ? getGradientColor(currentPosition + j, totalWidth()) : white
const opacity = activeShape.charOpacity?.[text[j]]
if (useGradient && typeof opacity === "number") fg = tint(theme.background, fg, opacity)
elements.push(
<text fg={fg} attributes={attrs} selectable={false}>
{char}
</text>,
)
}
}
currentPosition += markerIndex
}
const marker = rest[markerIndex]
const fg = useGradient ? getGradientColor(currentPosition, totalWidth()) : white
const shadow = tint(theme.background, fg, 0.45)
switch (marker) {
case "_":
elements.push(
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
{" "}
</text>,
)
break
case "^":
elements.push(
<text fg={fg} bg={shadow} attributes={attrs} selectable={false}>
▀
</text>,
)
break
case "~":
elements.push(
<text fg={shadow} attributes={attrs} selectable={false}>
▀
</text>,
)
break
}
i += markerIndex + 1
currentPosition += 1
}
return elements
}
if (props.column === "left") {
return (
<box flexDirection="column" flexShrink={0}>
<For each={activeShape.left}>
{(line) => (
<box flexDirection="row">{renderLine(line, 0, false, true)}</box>
)}
</For>
</box>
)
}
if (props.column === "right") {
return (
<box flexDirection="column" flexGrow={1} minWidth={0}>
<For each={activeShape.right}>
{(rightLine, idx) => (
<box flexDirection="row">{renderLogoRightRow(rightLine, idx())}</box>
)}
</For>
</box>
)
}
return (
<box flexDirection="row" gap={2} alignItems="flex-start">
<box flexDirection="column" flexShrink={0}>
<For each={activeShape.left}>
{(line) => (
<box flexDirection="row">{renderLine(line, 0, false, true)}</box>
)}
</For>
</box>
<box flexDirection="column" flexGrow={1} minWidth={0}>
<For each={activeShape.right}>
{(rightLine, idx) => (
<box flexDirection="row">{renderLogoRightRow(rightLine, idx())}</box>
)}
</For>
</box>
</box>
)
}
export function GoLogo() {
return <Logo shape={go as LogoType} column="left" />
}