diff --git a/assets/C64_Pro-STYLE.woff2 b/assets/C64_Pro-STYLE.woff2 new file mode 100644 index 0000000..1d0a8d6 Binary files /dev/null and b/assets/C64_Pro-STYLE.woff2 differ diff --git a/assets/C64_Pro_Mono-STYLE.woff2 b/assets/C64_Pro_Mono-STYLE.woff2 new file mode 100644 index 0000000..a1b72c5 Binary files /dev/null and b/assets/C64_Pro_Mono-STYLE.woff2 differ diff --git a/assets/PixeloidMono.ttf b/assets/PixeloidMono.ttf new file mode 100644 index 0000000..4b81885 Binary files /dev/null and b/assets/PixeloidMono.ttf differ diff --git a/assets/PixeloidSans.ttf b/assets/PixeloidSans.ttf new file mode 100644 index 0000000..5297cba Binary files /dev/null and b/assets/PixeloidSans.ttf differ diff --git a/assets/PixeloidSansBold.ttf b/assets/PixeloidSansBold.ttf new file mode 100644 index 0000000..9cf3288 Binary files /dev/null and b/assets/PixeloidSansBold.ttf differ diff --git a/packages/ReefVM b/packages/ReefVM index 1a18a71..47f829f 160000 --- a/packages/ReefVM +++ b/packages/ReefVM @@ -1 +1 @@ -Subproject commit 1a18a713d7ae86b03a6bef38cc53d12ecfbf9627 +Subproject commit 47f829fcada71655f0d40ec363b5bcc844af8856 diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 23fca89..3efa7cd 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp.ts' import * as terms from '#parser/shrimp.terms' import type { SyntaxNode, Tree } from '@lezer/common' import { assert, errorMessage } from '#utils/utils' -import { toBytecode, type Bytecode, type ProgramItem } from 'reefvm' +import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm' import { checkTreeForErrors, getAllChildren, @@ -72,9 +72,12 @@ export class Compiler { this.instructions.push(['RETURN']) } - if (DEBUG) logInstructions(this.instructions) - this.bytecode = toBytecode(this.instructions) + + if (DEBUG) { + const bytecodeString = bytecodeToString(this.bytecode) + console.log(`\n🤖 bytecode:\n----------------\n${bytecodeString}\n\n`) + } } catch (error) { if (error instanceof CompilerError) { throw new Error(error.toReadableString(input)) @@ -475,19 +478,3 @@ export class Compiler { } } } - -const logInstructions = (instructions: ProgramItem[]) => { - const instructionsString = instructions - .map((parts) => { - const isPush = parts[0] === 'PUSH' - return parts - .map((part, i) => { - const partAsString = typeof part == 'string' && isPush ? `'${part}'` : part!.toString() - return i > 0 ? partAsString : part - }) - .join(' ') - }) - .join('\n') - - console.log(`\n🤖 instructions:\n----------------\n${instructionsString}\n\n`) -} diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 3cff986..0f2e1c0 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -213,7 +213,7 @@ describe('Regex', () => { }) }) -describe.skip('native functions', () => { +describe('native functions', () => { test('print function', () => { const add = (x: number, y: number) => x + y expect(`add 5 9`).toEvaluateTo(14, { add }) diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index a37cd7d..b2d84c6 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -4,7 +4,7 @@ import { shrimpTheme } from '#editor/plugins/theme' import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage' import { shrimpHighlighting } from '#editor/plugins/theme' import { shrimpKeymap } from '#editor/plugins/keymap' -import { asciiEscapeToHtml, log, toElement } from '#utils/utils' +import { asciiEscapeToHtml, assert, assertNever, log, toElement } from '#utils/utils' import { Signal } from '#utils/signal' import { shrimpErrors } from '#editor/plugins/errors' import { debugTags } from '#editor/plugins/debugTags' @@ -12,6 +12,11 @@ import { getContent, persistencePlugin } from '#editor/plugins/persistence' import '#editor/editor.css' import type { HtmlEscapedString } from 'hono/utils/html' +import { catchErrors } from '#editor/plugins/catchErrors' +import { connectToNose, noseSignals } from '#editor/noseClient' +import type { Value } from 'reefvm' + +connectToNose() export const Editor = () => { return ( @@ -23,6 +28,7 @@ export const Editor = () => { parent: ref, doc: getContent(), extensions: [ + catchErrors, shrimpKeymap, basicSetup, shrimpTheme, @@ -30,7 +36,7 @@ export const Editor = () => { shrimpHighlighting, shrimpErrors, persistencePlugin, - debugTags, + // debugTags, ], }) @@ -47,23 +53,31 @@ export const Editor = () => { ) } -export const outputSignal = new Signal<{ output: string } | { error: string }>() +noseSignals.connect((message) => { + if (message.type === 'error') { + log.error(`Nose error: ${message.data}`) + errorSignal.emit(`Nose error: ${message.data}`) + } else if (message.type === 'reef-output') { + const x = outputSignal.emit(message.data) + } else if (message.type === 'connected') { + outputSignal.emit(`╞ Connected to Nose VM`) + } +}) -let outputTimeout: ReturnType +export const outputSignal = new Signal() +export const errorSignal = new Signal() -outputSignal.connect((output) => { +outputSignal.connect((value) => { const el = document.querySelector('#output')! el.innerHTML = '' - let content - if ('error' in output) { - el.classList.add('error') - content = output.error - } else { - el.classList.remove('error') - content = output.output - } + el.innerHTML = asciiEscapeToHtml(valueToString(value)) +}) - el.innerHTML = asciiEscapeToHtml(content) +errorSignal.connect((error) => { + const el = document.querySelector('#output')! + el.innerHTML = '' + el.classList.add('error') + el.innerHTML = asciiEscapeToHtml(error) }) type StatusBarMessage = { @@ -96,3 +110,37 @@ statusBarSignal.connect(async ({ side, message, className, order }) => { sideEl.insertBefore(toElement(messageEl), nodes[index]!) } }) + +const valueToString = (value: Value | string): string => { + if (typeof value === 'string') { + return value + } + + switch (value.type) { + case 'null': + return 'null' + case 'boolean': + return value.value ? 'true' : 'false' + case 'number': + return value.value.toString() + case 'string': + return value.value + case 'array': + return `${value.value.map(valueToString).join('\n')}` + case 'dict': { + const entries = Array.from(value.value.entries()).map( + ([key, val]) => `"${key}": ${valueToString(val)}` + ) + return `{${entries.join(', ')}}` + } + case 'regex': + return `/${value.value.source}/` + case 'function': + return `` + case 'native': + return `` + default: + assertNever(value) + return `` + } +} diff --git a/src/editor/noseClient.ts b/src/editor/noseClient.ts new file mode 100644 index 0000000..a581ae7 --- /dev/null +++ b/src/editor/noseClient.ts @@ -0,0 +1,59 @@ +import { Signal } from '#utils/signal' +import type { Bytecode, Value } from 'reefvm' +let ws: WebSocket + +type IncomingMessage = + | { type: 'connected' } + | { type: 'ping'; data: number } + | { type: 'commands'; data: number } + | { + type: 'apps' + data: { + name: string + type: 'browser' | 'server' + }[] + } + | { + type: 'session:start' + data: { + NOSE_DIR: string + cwd: string + hostname: string + mode: string + project: string + } + } + | { type: 'reef-output'; data: Value } + | { type: 'error'; data: string } + +export const noseSignals = new Signal() + +export const connectToNose = (url: string = 'ws://localhost:3000/ws') => { + ws = new WebSocket(url) + ws.onopen = () => noseSignals.emit({ type: 'connected' }) + + ws.onmessage = (event) => { + const message = JSON.parse(event.data) + noseSignals.emit(message) + } + + ws.onerror = (event) => { + console.error(`💥WebSocket error:`, event) + } + + ws.onclose = () => { + console.log(`🚪 Connection closed`) + } +} + +let id = 0 +export const sendToNose = (code: Bytecode) => { + if (!ws) { + throw new Error('WebSocket is not connected.') + } else if (ws.readyState !== WebSocket.OPEN) { + throw new Error(`WebSocket is not open, current status is ${ws.readyState}.`) + } + + id += 1 + ws.send(JSON.stringify({ type: 'reef-bytecode', data: code, id })) +} diff --git a/src/editor/plugins/catchErrors.ts b/src/editor/plugins/catchErrors.ts new file mode 100644 index 0000000..b40cee6 --- /dev/null +++ b/src/editor/plugins/catchErrors.ts @@ -0,0 +1,9 @@ +import { errorSignal } from '#editor/editor' +import { EditorView } from '@codemirror/view' + +export const catchErrors = EditorView.exceptionSink.of((exception) => { + console.error('CodeMirror error:', exception) + errorSignal.emit( + `Editor error: ${exception instanceof Error ? exception.message : String(exception)}` + ) +}) diff --git a/src/editor/plugins/keymap.tsx b/src/editor/plugins/keymap.tsx index 7c006e9..6f948f6 100644 --- a/src/editor/plugins/keymap.tsx +++ b/src/editor/plugins/keymap.tsx @@ -1,9 +1,9 @@ -import { statusBarSignal } from '#editor/editor' -import { run } from '#editor/runCode' +import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode' import { EditorState } from '@codemirror/state' import { keymap } from '@codemirror/view' let multilineMode = false + const customKeymap = keymap.of([ { key: 'Enter', @@ -11,17 +11,22 @@ const customKeymap = keymap.of([ if (multilineMode) return false const input = view.state.doc.toString() - run(input) + history.push(input) + runCode(input) + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: '' }, + selection: { anchor: 0 }, + }) return true }, }, { - key: 'Alt-Enter', + key: 'Shift-Enter', run: (view) => { if (multilineMode) { const input = view.state.doc.toString() - run(input) + runCode(input) return true } @@ -31,7 +36,62 @@ const customKeymap = keymap.of([ selection: { anchor: view.state.doc.length + 1 }, }) - updateStatusMessage() + return true + }, + }, + + { + key: 'Tab', + preventDefault: true, + run: (view) => { + return true + }, + }, + + { + key: 'ArrowUp', + run: (view) => { + const command = history.previous() + if (command === undefined) return false + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: command }, + selection: { anchor: command.length }, + }) + return true + }, + }, + + { + key: 'ArrowDown', + run: (view) => { + const command = history.next() + if (command === undefined) return false + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: command }, + selection: { anchor: command.length }, + }) + return true + }, + }, + + { + key: 'Mod-k 1', + preventDefault: true, + run: (view) => { + const input = view.state.doc.toString() + printParserOutput(input) + + return true + }, + }, + + { + key: 'Mod-k 2', + preventDefault: true, + run: (view) => { + const input = view.state.doc.toString() + printBytecodeOutput(input) + return true }, }, @@ -45,7 +105,6 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => { firstTime = false if (transaction.newDoc.toString().includes('\n')) { multilineMode = true - updateStatusMessage() return transaction } } @@ -53,7 +112,6 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => { transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => { if (inserted.toString().includes('\n')) { multilineMode = true - updateStatusMessage() return } }) @@ -63,22 +121,38 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => { export const shrimpKeymap = [customKeymap, singleLineFilter] -const updateStatusMessage = () => { - statusBarSignal.emit({ - side: 'left', - message: multilineMode ? 'Press Alt-Enter run' : 'Alt-Enter will enter multiline mode', - className: 'status', - }) +class History { + private commands: string[] = [] + private index: number | undefined - statusBarSignal.emit({ - side: 'right', - message: ( -
- • multiline -
- ), - className: 'multiline-status', - }) + push(command: string) { + this.commands.push(command) + this.index = undefined + } + + previous(): string | undefined { + if (this.commands.length === 0) return + + if (this.index === undefined) { + this.index = this.commands.length - 1 + } else if (this.index > 0) { + this.index -= 1 + } + + return this.commands[this.index] + } + + next(): string | undefined { + if (this.commands.length === 0 || this.index === undefined) return + + if (this.index < this.commands.length - 1) { + this.index += 1 + return this.commands[this.index] + } else { + this.index = undefined + return '' + } + } } -requestAnimationFrame(() => updateStatusMessage()) +const history = new History() diff --git a/src/editor/runCode.tsx b/src/editor/runCode.tsx index bbe6229..27b68ff 100644 --- a/src/editor/runCode.tsx +++ b/src/editor/runCode.tsx @@ -1,16 +1,36 @@ -import { outputSignal } from '#editor/editor' +import { outputSignal, errorSignal } from '#editor/editor' import { Compiler } from '#compiler/compiler' import { errorMessage, log } from '#utils/utils' -import { VM } from 'reefvm' +import { bytecodeToString, run } from 'reefvm' +import { parser } from '#parser/shrimp' +import { sendToNose } from '#editor/noseClient' -export const run = async (input: string) => { +export const runCode = async (input: string) => { try { const compiler = new Compiler(input) - const vm = new VM(compiler.bytecode) - const output = await vm.run() - outputSignal.emit({ output: String(output.value) }) + sendToNose(compiler.bytecode) } catch (error) { log.error(error) - outputSignal.emit({ error: `${errorMessage(error)}` }) + errorSignal.emit(`${errorMessage(error)}`) + } +} + +export const printParserOutput = (input: string) => { + try { + const cst = parser.parse(input) + outputSignal.emit(cst.toString()) + } catch (error) { + log.error(error) + errorSignal.emit(`${errorMessage(error)}`) + } +} + +export const printBytecodeOutput = (input: string) => { + try { + const compiler = new Compiler(input) + outputSignal.emit(bytecodeToString(compiler.bytecode)) + } catch (error) { + log.error(error) + errorSignal.emit(`${errorMessage(error)}`) } } diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 1c6521a..6eeeecd 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -6,6 +6,8 @@ @top Program { item* } +@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } + @tokens { @precedence { Number "-" Regex "/"} @@ -43,8 +45,6 @@ } -@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } - @precedence { pipe @left, multiplicative @left, diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 616e218..476243a 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -5,7 +5,7 @@ import {trackScope} from "./scopeTracker" import {highlighting} from "./highlight" export const parser = LRParser.deserialize({ version: 14, - states: ".jQVQaOOO#XQbO'#CfO$RQPO'#CgO$aQPO'#DmO$xQaO'#CeO%gOSO'#CuOOQ`'#Dq'#DqO%uOPO'#C}O%zQPO'#DpO&cQaO'#D|OOQ`'#DO'#DOOOQO'#Dn'#DnO&kQPO'#DmO&yQaO'#EQOOQO'#DX'#DXO'hQPO'#DaOOQO'#Dm'#DmO'mQPO'#DlOOQ`'#Dl'#DlOOQ`'#Db'#DbQVQaOOOOQ`'#Dp'#DpOOQ`'#Cd'#CdO'uQaO'#DUOOQ`'#Do'#DoOOQ`'#Dc'#DcO(PQbO,58}O&yQaO,59RO&yQaO,59RO)XQPO'#CgO)iQPO,59PO)zQPO,59PO)uQPO,59PO*uQPO,59PO*}QaO'#CwO+VQWO'#CxOOOO'#Du'#DuOOOO'#Dd'#DdO+kOSO,59aOOQ`,59a,59aO+yO`O,59iOOQ`'#De'#DeO,OQaO'#DQO,WQPO,5:hO,]QaO'#DgO,bQPO,58|O,sQPO,5:lO,zQPO,5:lO-PQaO,59{OOQ`,5:W,5:WOOQ`-E7`-E7`OOQ`,59p,59pOOQ`-E7a-E7aOOQO1G.m1G.mO-^QPO1G.mO&yQaO,59WO&yQaO,59WOOQ`1G.k1G.kOOOO,59c,59cOOOO,59d,59dOOOO-E7b-E7bOOQ`1G.{1G.{OOQ`1G/T1G/TOOQ`-E7c-E7cO-xQaO1G0SO!QQbO'#CfOOQO,5:R,5:ROOQO-E7e-E7eO.YQaO1G0WOOQO1G/g1G/gOOQO1G.r1G.rO.jQPO1G.rO.tQPO7+%nO.yQaO7+%oOOQO'#DZ'#DZOOQO7+%r7+%rO/ZQaO7+%sOOQ`<uAN>uO&yQaO'#D]OOQO'#Dh'#DhO0nQPOAN>yO0yQPO'#D_OOQOAN>yAN>yO1OQPOAN>yO1TQPO,59wO1[QPO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQPOG24eO1fQPO,59yO1kQPO1G/cOOQOLD*PLD*PO.yQaO1G/eO/ZQaO7+$}OOQO7+%P7+%POOQO<uAN>uO&yQRO'#D]OOQO'#Dh'#DhO0nQQOAN>yO0yQQO'#D_OOQOAN>yAN>yO1OQQOAN>yO1TQQO,59wO1[QQO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQQOG24eO1fQQO,59yO1kQQO1G/cOOQOLD*PLD*PO.yQRO1G/eO/ZQRO7+$}OOQO7+%P7+%POOQO<n#a#b;W#b#cCR#c#o;W#o;'S$_;'S;=`$v<%lO$_V>s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#h?i#h#o;W#o;'S$_;'S;=`$v<%lO$_V?n^jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#Y@j#Y#];W#]#^Aa#^#o;W#o;'S$_;'S;=`$v<%lO$_V@qY!SPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VAf[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZB[#Z#o;W#o;'S$_;'S;=`$v<%lO$_VBcY!QPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VCW[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#W;W#W#XC|#X#o;W#o;'S$_;'S;=`$v<%lO$_VDTYjSvROt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VDx]jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#UEq#U#b;W#b#cIX#c#o;W#o;'S$_;'S;=`$v<%lO$_VEv[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aFl#a#o;W#o;'S$_;'S;=`$v<%lO$_VFq[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#hGg#h#o;W#o;'S$_;'S;=`$v<%lO$_VGl[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#YHb#Y#o;W#o;'S$_;'S;=`$v<%lO$_VHiYnRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VI`YsRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VJT[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZJy#Z#o;W#o;'S$_;'S;=`$v<%lO$_VKQY|PjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__Kw[!lWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jLm#j#o;W#o;'S$_;'S;=`$v<%lO$_VLr[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aMh#a#o;W#o;'S$_;'S;=`$v<%lO$_VMm[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aNc#a#o;W#o;'S$_;'S;=`$v<%lO$_VNjYpRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V! _[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!!T#g#o;W#o;'S$_;'S;=`$v<%lO$_V!![YhRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_^!#RY!nWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__!#x[!mWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!$n#g#o;W#o;'S$_;'S;=`$v<%lO$_V!$s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jGg#j#o;W#o;'S$_;'S;=`$v<%lO$_V!%pUzRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~!&XO!v~", - tokenizers: [0, 1, 2, 3, tokenizer], + tokenizers: [tokenizer, 0, 1, 2, 3], topRules: {"Program":[0,5]}, tokenPrec: 768 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index 1505f62..44bf627 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -105,6 +105,24 @@ describe('Parentheses', () => { `) }) + test('a word start with *', () => { + expect('find *cool*').toMatchTree(` + FunctionCall + Identifier find + PositionalArg + Word *cool* + `) + }) + + test('a word can look like a binop', () => { + expect('find cool*wow').toMatchTree(` + FunctionCall + Identifier find + PositionalArg + Word cool*wow + `) + }) + test('nested parentheses', () => { expect('(2 + (1 * 4))').toMatchTree(` ParenExpr @@ -131,7 +149,7 @@ describe('Parentheses', () => { }) }) -describe('BinOp', () => { +describe.only('BinOp', () => { test('addition tests', () => { expect('2 + 3').toMatchTree(` BinOp diff --git a/src/server/index.css b/src/server/index.css index 240bd2e..2719f41 100644 --- a/src/server/index.css +++ b/src/server/index.css @@ -47,6 +47,20 @@ --ansi-bright-white: #FFFFFF; } +@font-face { + font-family: 'C64ProMono'; + src: url('../../assets/C64_Pro_Mono-STYLE.woff2') format('woff2'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Pixeloid Mono'; + src: url('../../assets/PixeloidMono.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + * { margin: 0; padding: 0; diff --git a/src/server/server.tsx b/src/server/server.tsx index 094b732..1c7291d 100644 --- a/src/server/server.tsx +++ b/src/server/server.tsx @@ -1,6 +1,7 @@ import index from './index.html' const server = Bun.serve({ + port: process.env.PORT ? Number(process.env.PORT) : 3001, routes: { '/*': index, @@ -19,7 +20,6 @@ const server = Bun.serve({ }, }, }, - development: process.env.NODE_ENV !== 'production' && { hmr: true, console: true, diff --git a/src/utils/signal.ts b/src/utils/signal.ts index c10609c..d9629a4 100644 --- a/src/utils/signal.ts +++ b/src/utils/signal.ts @@ -1,9 +1,17 @@ /** * How to use a Signal: * - * Create a signal: + * Create a signal with primitives: + * const nameSignal = new Signal() + * const countSignal = new Signal() + * + * Create a signal with objects: * const chatSignal = new Signal<{ username: string, message: string }>() * + * Create a signal with no data (void): + * const clickSignal = new Signal() + * const clickSignal2 = new Signal() // Defaults to void + * * Connect to the signal: * const disconnect = chatSignal.connect((data) => { * const {username, message} = data; @@ -11,7 +19,10 @@ * }) * * Emit a signal: + * nameSignal.emit("Alice") + * countSignal.emit(42) * chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" }); + * clickSignal.emit() // No argument for void signals * * Forward a signal: * const relaySignal = new Signal<{ username: string, message: string }>() @@ -25,7 +36,7 @@ * chatSignal.disconnect() */ -export class Signal { +export class Signal { private listeners: Array<(data: T) => void> = [] connect(listenerOrSignal: Signal | ((data: T) => void)) {