Add Native Blog and Info Update
This commit is contained in:
@@ -79,7 +79,7 @@
|
||||
<TuiLine
|
||||
line={child}
|
||||
index={k}
|
||||
segments={parseColorText(child.content)}
|
||||
segments={parseColorText(child.content, $themeColors.colorMap)}
|
||||
complete={true}
|
||||
showImage={true}
|
||||
selectedIndex={-1}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
class:selected
|
||||
class:inline
|
||||
class:no-border={line.border === false}
|
||||
class:flex-expand={line.flex}
|
||||
style="--btn-color: {getButtonStyle(line.style)}"
|
||||
onclick={() => onClick(index, line)}
|
||||
onmouseenter={() => onHover(index)}
|
||||
@@ -55,7 +56,7 @@
|
||||
<span class="btn-text" style:text-align={line.textStyle || "left"}>
|
||||
{#each segments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
getSegmentStyle,
|
||||
parseDimension,
|
||||
} from "./utils";
|
||||
import { themeColors } from "$lib/stores/theme";
|
||||
import type { CardLine, TerminalLine } from "./types";
|
||||
import TuiLine from "./TuiLine.svelte";
|
||||
import "$lib/assets/css/tui-card.css";
|
||||
@@ -26,12 +27,12 @@
|
||||
onLinkClick = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
const segments = $derived(parseColorText(line.content));
|
||||
const segments = $derived(parseColorText(line.content, $themeColors.colorMap));
|
||||
const titleSegments = $derived(
|
||||
line.cardTitle ? parseColorText(line.cardTitle) : [],
|
||||
line.cardTitle ? parseColorText(line.cardTitle, $themeColors.colorMap) : [],
|
||||
);
|
||||
const footerSegments = $derived(
|
||||
line.cardFooter ? parseColorText(line.cardFooter) : [],
|
||||
line.cardFooter ? parseColorText(line.cardFooter, $themeColors.colorMap) : [],
|
||||
);
|
||||
|
||||
const cardStyle = $derived(
|
||||
@@ -146,7 +147,7 @@
|
||||
<TuiLine
|
||||
line={item.line}
|
||||
index={item.index}
|
||||
segments={parseColorText(item.line.content)}
|
||||
segments={parseColorText(item.line.content, $themeColors.colorMap)}
|
||||
complete={true}
|
||||
showImage={true}
|
||||
selectedIndex={-1}
|
||||
@@ -161,7 +162,7 @@
|
||||
<TuiLine
|
||||
line={group.line}
|
||||
index={group.index}
|
||||
segments={parseColorText(group.line.content)}
|
||||
segments={parseColorText(group.line.content, $themeColors.colorMap)}
|
||||
complete={true}
|
||||
showImage={true}
|
||||
selectedIndex={-1}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<span class="checkbox-label">
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<TuiLine
|
||||
line={item.line}
|
||||
index={item.index}
|
||||
segments={parseColorText(item.line.content)}
|
||||
segments={parseColorText(item.line.content, colorMap)}
|
||||
complete={true}
|
||||
showImage={item.line.type === "image"}
|
||||
selectedIndex={-1}
|
||||
@@ -127,7 +127,7 @@
|
||||
<TuiLine
|
||||
line={group.line}
|
||||
index={group.index}
|
||||
segments={parseColorText(group.line.content)}
|
||||
segments={parseColorText(group.line.content, colorMap)}
|
||||
complete={true}
|
||||
showImage={group.line.type === "image"}
|
||||
selectedIndex={-1}
|
||||
@@ -167,10 +167,16 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tui-group.expand > :global(*) {
|
||||
.tui-group.expand > :global(*),
|
||||
.tui-group.expand > :global(*) > :global(*) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tui-group.expand :global(.tui-button) {
|
||||
flex: 1;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@keyframes lineSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
{/if}
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -76,12 +76,26 @@
|
||||
const isHeader = $derived(line.type === "header");
|
||||
const isLink = $derived(line.type === "link");
|
||||
const isTooltip = $derived(line.type === "tooltip");
|
||||
|
||||
const blankHeight = $derived(
|
||||
line.type === "blank" && line.height !== undefined
|
||||
? typeof line.height === "number"
|
||||
? `${line.height}px`
|
||||
: line.height
|
||||
: undefined,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class:mobile-hidden={line.mobile === false} style="display: contents">
|
||||
{#if isBlank}
|
||||
{#if inline}<span class="inline-blank"></span>{:else}<div
|
||||
{#if inline}<span
|
||||
class="inline-blank"
|
||||
style={blankHeight ? `width: ${blankHeight}` : undefined}
|
||||
></span>{:else}<div
|
||||
class="tui-line blank"
|
||||
style={blankHeight
|
||||
? `height: ${blankHeight}; min-height: ${blankHeight};`
|
||||
: undefined}
|
||||
></div>{/if}
|
||||
{:else if isDivider}
|
||||
<div class="tui-divider" id={line.id}>
|
||||
@@ -149,7 +163,7 @@
|
||||
<Icon icon="mdi:pound" width="20" class="header-icon" />
|
||||
{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="20"
|
||||
width={seg.iconSize ?? 20}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
@@ -161,7 +175,7 @@
|
||||
<Icon icon="mdi:pound" width="25" class="header-icon" />
|
||||
{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="25"
|
||||
width={seg.iconSize ?? 25}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
@@ -175,7 +189,7 @@
|
||||
<span class="inline-content {line.type}">
|
||||
{getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="14"
|
||||
width={seg.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
@@ -196,7 +210,7 @@
|
||||
line.type,
|
||||
)}{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="14"
|
||||
width={seg.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
@@ -210,7 +224,7 @@
|
||||
<span class="inline-content {line.type}">
|
||||
{getLinePrefix(line.type)}{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="14"
|
||||
width={seg.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
@@ -224,7 +238,7 @@
|
||||
line.type,
|
||||
)}{#each segments as seg}{#if seg.icon}<Icon
|
||||
icon={seg.icon}
|
||||
width="14"
|
||||
width={seg.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>{:else if getSegmentStyle(seg)}<span
|
||||
style={getSegmentStyle(seg)}>{seg.text}</span
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<button class="link-text" onclick={onClick}>
|
||||
{#each segments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="progress-label">
|
||||
{#each contentSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="progress-value">
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="12" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 12} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
{/if}
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
@@ -98,7 +98,7 @@
|
||||
{#if segment.icon}
|
||||
<Icon
|
||||
icon={segment.icon}
|
||||
width="14"
|
||||
width={segment.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>
|
||||
{:else if getSegmentStyle(segment)}
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
{/if}
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
@@ -202,7 +202,7 @@
|
||||
{/if}
|
||||
<div class="select-options">
|
||||
{#each filteredOptions as option, i}
|
||||
{@const optionSegments = parseColorText(option.label)}
|
||||
{@const optionSegments = parseColorText(option.label, $themeColors.colorMap)}
|
||||
<div
|
||||
class="select-option"
|
||||
class:selected={value === option.value}
|
||||
@@ -232,7 +232,7 @@
|
||||
{#if segment.icon}
|
||||
<Icon
|
||||
icon={segment.icon}
|
||||
width="14"
|
||||
width={segment.iconSize ?? 14}
|
||||
class="inline-icon"
|
||||
/>
|
||||
{:else if getSegmentStyle(segment)}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
{/if}
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<span class="toggle-label">
|
||||
{#each labelSegments as segment}
|
||||
{#if segment.icon}
|
||||
<Icon icon={segment.icon} width="14" class="inline-icon" />
|
||||
<Icon icon={segment.icon} width={segment.iconSize ?? 14} class="inline-icon" />
|
||||
{:else if getSegmentStyle(segment)}
|
||||
<span style={getSegmentStyle(segment)}>{segment.text}</span>
|
||||
{:else}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getButtonStyle, parseColorText, getSegmentStyle } from "./utils";
|
||||
import { themeColors } from "$lib/stores/theme";
|
||||
import type { TooltipLine } from "./types";
|
||||
import "$lib/assets/css/tui-tooltip.css";
|
||||
|
||||
@@ -13,7 +14,7 @@
|
||||
let triggerEl: HTMLSpanElement | undefined = $state();
|
||||
let tooltipStyle = $state("");
|
||||
|
||||
const contentSegments = $derived(parseColorText(line.content));
|
||||
const contentSegments = $derived(parseColorText(line.content, $themeColors.colorMap));
|
||||
const position = $derived(line.tooltipPosition || "top");
|
||||
|
||||
function updateTooltipPosition() {
|
||||
|
||||
@@ -29,15 +29,71 @@ const err = (content: string): TerminalLine => ({ type: 'error', content });
|
||||
const info = (content: string): TerminalLine => ({ type: 'info', content });
|
||||
const blank = (): TerminalLine => ({ type: 'blank', content: '' });
|
||||
|
||||
const pageRoutes: Record<string, string> = {
|
||||
home: '/',
|
||||
about: '/about',
|
||||
portfolio: '/portfolio',
|
||||
projects: '/projects',
|
||||
models: '/models',
|
||||
components: '/components',
|
||||
links: '/links'
|
||||
};
|
||||
interface PageNode {
|
||||
name: string;
|
||||
path: string;
|
||||
children?: PageNode[];
|
||||
}
|
||||
|
||||
const pageTree: PageNode[] = [
|
||||
{ name: 'home', path: '/' },
|
||||
{ name: 'about', path: '/about' },
|
||||
{ name: 'portfolio', path: '/portfolio' },
|
||||
{
|
||||
name: 'projects',
|
||||
path: '/projects',
|
||||
children: [
|
||||
{ name: 'opensource', path: '/projects/opensource' },
|
||||
{ name: 'packages', path: '/projects/packages' },
|
||||
{ name: 'hackathons', path: '/projects/hackathons' },
|
||||
{ name: 'research', path: '/projects/research' }
|
||||
]
|
||||
},
|
||||
{ name: 'models', path: '/models' },
|
||||
{ name: 'components', path: '/components' },
|
||||
{ name: 'links', path: '/links' },
|
||||
{ name: 'blog', path: '/blog' }
|
||||
];
|
||||
|
||||
const pageRoutes: Record<string, string> = pageTree.reduce((routes, node) => {
|
||||
routes[node.name] = node.path;
|
||||
node.children?.forEach((child) => {
|
||||
routes[child.name] = child.path;
|
||||
});
|
||||
return routes;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
function resolvePagePath(target: string): string | undefined {
|
||||
if (pageRoutes[target] !== undefined) return pageRoutes[target];
|
||||
|
||||
const segments = target.split('/').filter(Boolean);
|
||||
if (segments.length < 2) return undefined;
|
||||
|
||||
let nodes: PageNode[] = pageTree;
|
||||
let path: string | undefined;
|
||||
for (const segment of segments) {
|
||||
const node = nodes.find((candidate) => candidate.name === segment);
|
||||
if (!node) return undefined;
|
||||
path = node.path;
|
||||
nodes = node.children ?? [];
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function cowsay(message: string): string[] {
|
||||
const text = message || 'Moo?';
|
||||
const border = '-'.repeat(text.length + 2);
|
||||
return [
|
||||
` ${border}`,
|
||||
`< ${text} >`,
|
||||
` ${border}`,
|
||||
' \\ ^__^',
|
||||
' \\ (oo)\\_______',
|
||||
' (__)\\ )\\/\\',
|
||||
' ||----w |',
|
||||
' || ||'
|
||||
];
|
||||
}
|
||||
|
||||
const baseCommands: Command[] = [
|
||||
{
|
||||
@@ -59,7 +115,7 @@ const baseCommands: Command[] = [
|
||||
tableRows: rows,
|
||||
style: 'accent'
|
||||
} as TerminalLine);
|
||||
ctx.terminal.write(info('Tip: try (&accent)neofetch(&), (&accent)cd projects(&), or (&accent)theme(&)'));
|
||||
ctx.terminal.write(info('Tip: try (&accent)neofetch(&), (&accent)cowsay(&), or (&accent)cd projects(&)'));
|
||||
ctx.terminal.write(blank());
|
||||
}
|
||||
},
|
||||
@@ -74,7 +130,7 @@ const baseCommands: Command[] = [
|
||||
description: 'Show a short bio',
|
||||
run: (_args, ctx) => {
|
||||
ctx.terminal.writeLines([
|
||||
out(`(&primary,bold)${ctx.user.name}(&) (&muted)— ${ctx.user.title}(&)`),
|
||||
out(`(&primary,bold)${ctx.user.displayname}(&) (&muted)— ${ctx.user.title}(&)`),
|
||||
out(`(&muted)${ctx.user.bio}(&)`),
|
||||
blank()
|
||||
]);
|
||||
@@ -85,14 +141,37 @@ const baseCommands: Command[] = [
|
||||
aliases: ['banner'],
|
||||
description: 'Display system info',
|
||||
run: (_args, ctx) => {
|
||||
ctx.terminal.writeLines([
|
||||
const launched = new Date('2020-01-01T00:00:00Z');
|
||||
const days = Math.floor((Date.now() - launched.getTime()) / 86400000);
|
||||
const infoLines: TerminalLine[] = [
|
||||
out(`(&accent,bold)${ctx.user.username}(&)(&muted)@(&)(&primary,bold)${ctx.user.hostname}(&)`),
|
||||
out('(&muted)------------------------------(&)'),
|
||||
out(`(&blue,bold)Name(&): ${ctx.user.name}`),
|
||||
out(`(&blue,bold)Name(&): ${ctx.user.displayname}`),
|
||||
out(`(&blue,bold)Title(&): ${ctx.user.title}`),
|
||||
out(`(&blue,bold)Location(&): ${ctx.user.location}`),
|
||||
out(`(&blue,bold)Shell(&): tui-terminal`),
|
||||
out(`(&blue,bold)Theme(&): ${ctx.getColorTheme()} (${ctx.getMode()})`),
|
||||
out(`(&blue,bold)Uptime(&): ${days} days since launch`),
|
||||
out(`(&blue,bold)Languages(&): ${ctx.skills.languages.slice(0, 6).join(', ')}`),
|
||||
out('(&red)███(&)(&yellow)███(&)(&green)███(&)(&cyan)███(&)(&blue)███(&)(&magenta)███(&)')
|
||||
];
|
||||
ctx.terminal.writeLines([
|
||||
{
|
||||
type: 'image',
|
||||
content: '',
|
||||
image: ctx.user.avatar,
|
||||
imageAlt: ctx.user.displayname,
|
||||
imageWidth: 325,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
content: '',
|
||||
inline: true,
|
||||
groupDirection: 'column',
|
||||
groupGap: '0',
|
||||
children: infoLines
|
||||
},
|
||||
blank()
|
||||
]);
|
||||
}
|
||||
@@ -134,17 +213,16 @@ const baseCommands: Command[] = [
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
description: 'Open a mail draft',
|
||||
description: 'Show contact email',
|
||||
run: (_args, ctx) => {
|
||||
ctx.terminal.write(ok(`Opening mail to (&accent)${ctx.user.email}(&)`));
|
||||
ctx.navigate(`mailto:${ctx.user.email}`, true);
|
||||
ctx.terminal.write(out(`(&accent)${ctx.user.email}(&)`));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'cd',
|
||||
aliases: ['goto', 'open'],
|
||||
description: 'Navigate to a page',
|
||||
usage: 'cd <page>',
|
||||
usage: 'cd <page> (e.g. projects/opensource)',
|
||||
run: (args, ctx) => {
|
||||
const target = (args[0] || 'home').toLowerCase().replace(/^\/+|\/+$/g, '');
|
||||
const navItem = ctx.navigation.find((item) => item.name.toLowerCase() === target);
|
||||
@@ -153,9 +231,10 @@ const baseCommands: Command[] = [
|
||||
ctx.navigate(navItem.path, navItem.external);
|
||||
return;
|
||||
}
|
||||
if (pageRoutes[target] !== undefined) {
|
||||
const resolvedPath = resolvePagePath(target);
|
||||
if (resolvedPath !== undefined) {
|
||||
ctx.terminal.write(info(`Navigating to (&accent)${target}(&)...`));
|
||||
ctx.navigate(pageRoutes[target]);
|
||||
ctx.navigate(resolvedPath);
|
||||
return;
|
||||
}
|
||||
ctx.terminal.write(err(`No such page: ${target}. Try (&bold)ls(&).`));
|
||||
@@ -163,13 +242,26 @@ const baseCommands: Command[] = [
|
||||
},
|
||||
{
|
||||
name: 'ls',
|
||||
aliases: ['dir'],
|
||||
description: 'List the pages you can visit',
|
||||
run: (_args, ctx) => {
|
||||
const names = Object.keys(pageRoutes);
|
||||
ctx.navigation.forEach((item) => {
|
||||
if (item.external && !names.includes(item.name)) names.push(item.name);
|
||||
const lines: TerminalLine[] = [];
|
||||
|
||||
pageTree.forEach((node) => {
|
||||
lines.push(out(`(&blue)drwxr-xr-x(&) (&primary,bold)${node.name}(&)/`));
|
||||
node.children?.forEach((child, i) => {
|
||||
const branch = i === node.children!.length - 1 ? '└──' : '├──';
|
||||
lines.push(out(`(&muted) ${branch}(&) (&primary,bold)${child.name}(&)/`));
|
||||
});
|
||||
});
|
||||
ctx.terminal.write(out(names.map((name) => `(&blue)${name}(&)`).join(' ')));
|
||||
|
||||
ctx.navigation.forEach((item) => {
|
||||
if (item.external && pageRoutes[item.name] === undefined) {
|
||||
lines.push(out(`(&blue)drwxr-xr-x(&) (&primary,bold)${item.name}(&)/`));
|
||||
}
|
||||
});
|
||||
|
||||
ctx.terminal.writeLines(lines);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -217,10 +309,48 @@ const baseCommands: Command[] = [
|
||||
usage: 'echo <text>',
|
||||
run: (args, ctx) => ctx.terminal.write(out(args.join(' ')))
|
||||
},
|
||||
{
|
||||
name: 'cowsay',
|
||||
description: 'Make a cow say something',
|
||||
usage: 'cowsay <text>',
|
||||
run: (args, ctx) => {
|
||||
const lines = cowsay(args.join(' '));
|
||||
ctx.terminal.writeLines(lines.map((line) => out(`(&accent)${line}(&)`)));
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
description: 'Show the current date and time',
|
||||
run: (_args, ctx) => ctx.terminal.write(out(new Date().toString()))
|
||||
description: 'Show the current date and time (Eastern)',
|
||||
run: (_args, ctx) => {
|
||||
const now = new Date();
|
||||
const easternString = now.toLocaleString('en-US', {
|
||||
timeZone: 'America/New_York',
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: 'short'
|
||||
});
|
||||
|
||||
const easternParts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: 'America/New_York',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
}).formatToParts(now);
|
||||
const part = (type: string) => Number(easternParts.find((p) => p.type === type)?.value);
|
||||
const easternDate = new Date(Date.UTC(part('year'), part('month') - 1, part('day')));
|
||||
const startOfYear = new Date(Date.UTC(part('year'), 0, 1));
|
||||
const dayOfYear = Math.floor((easternDate.getTime() - startOfYear.getTime()) / 86400000) + 1;
|
||||
|
||||
ctx.terminal.writeLines([
|
||||
out(easternString),
|
||||
info(`(&muted)Day ${dayOfYear} of ${part('year')}(&)`)
|
||||
]);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sudo',
|
||||
@@ -230,15 +360,6 @@ const baseCommands: Command[] = [
|
||||
ctx.terminal.write(
|
||||
err(`(&error)Nice try.(&) ${ctx.user.username} is not in the sudoers file. This incident will be reported.`)
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'blog',
|
||||
hidden: true,
|
||||
description: 'Open the blog',
|
||||
run: (_args, ctx) => {
|
||||
const blogItem = ctx.navigation.find((item) => item.name.toLowerCase() === 'blog');
|
||||
if (blogItem) ctx.navigate(blogItem.path, blogItem.external);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface WarningLine extends BaseTerminalLine {
|
||||
|
||||
export interface BlankLine extends BaseTerminalLine {
|
||||
type: 'blank';
|
||||
height?: string | number;
|
||||
}
|
||||
|
||||
export interface DividerLine extends BaseTerminalLine {
|
||||
@@ -80,6 +81,7 @@ export interface ButtonLine extends BaseTerminalLine {
|
||||
external?: boolean;
|
||||
textStyle?: 'start' | 'center' | 'end';
|
||||
border?: boolean;
|
||||
flex?: boolean;
|
||||
}
|
||||
|
||||
export interface LinkLine extends BaseTerminalLine {
|
||||
|
||||
@@ -85,10 +85,11 @@ export function parseColorText(text: string, colors: ThemeColorMap = colorMap):
|
||||
segments.push({ text: text.slice(lastIndex, match.index) });
|
||||
}
|
||||
|
||||
// Check if this is an icon match (Group 1 is the icon name)
|
||||
// Check if this is an icon match (Group 1 is the icon name, optionally followed by a size)
|
||||
if (match[1]) {
|
||||
const iconName = match[1].trim();
|
||||
segments.push({ text: '', icon: iconName });
|
||||
const [iconName, sizeArg] = match[1].split(',').map(part => part.trim());
|
||||
const iconSize = sizeArg ? Number(sizeArg) : undefined;
|
||||
segments.push({ text: '', icon: iconName, iconSize: iconSize && !isNaN(iconSize) ? iconSize : undefined });
|
||||
lastIndex = match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user