mirror of
https://github.com/litruv/lit.ruv.wtf.git
synced 2026-07-27 04:06:03 +10:00
Compare commits
3 Commits
1f658c179f
...
d762f8e67e
| Author | SHA1 | Date | |
|---|---|---|---|
| d762f8e67e | |||
| df10268a83 | |||
| 6d7ee1db71 |
10
package-lock.json
generated
10
package-lock.json
generated
@@ -9,9 +9,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@litruv/mxjs-lite": "^1.0.1",
|
||||||
"jimp": "^1.6.0"
|
"jimp": "^1.6.0"
|
||||||
},
|
}
|
||||||
"devDependencies": {}
|
|
||||||
},
|
},
|
||||||
"node_modules/@jimp/core": {
|
"node_modules/@jimp/core": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
@@ -425,6 +425,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@litruv/mxjs-lite": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@litruv/mxjs-lite/-/mxjs-lite-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-kEL/MZdezhibPNQFiQWUWRBPRxfjWTUNnDNpqxmDyLFFG8iLXMQy2mJAMYPBgxw6D+NXARiMcABp5pEWezfC9w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/token": {
|
"node_modules/@tokenizer/token": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"author": "Max Litruv Boonzaayer",
|
"author": "Max Litruv Boonzaayer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@litruv/mxjs-lite": "^1.0.1",
|
||||||
"jimp": "^1.6.0"
|
"jimp": "^1.6.0"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1133
website/matrix-client.old.js
Normal file
1133
website/matrix-client.old.js
Normal file
File diff suppressed because it is too large
Load Diff
1243
website/mxjs-lite.js
Normal file
1243
website/mxjs-lite.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -253,13 +253,9 @@ class NumberMatchGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap-around horizontal (end of one row to start of next)
|
// Wrap-around: scan reading order across row boundaries (right to end of row,
|
||||||
if (rowEnd === rowStart + 1 && colStart === this.width - 1 && colEnd === 0) {
|
// down to next row, left-to-right until reaching the second tile)
|
||||||
// Adjacent via wrap
|
if (rowStart !== rowEnd && this.isSegmentClear(start, end, 1)) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (diff === 1 || diff === this.width) {
|
|
||||||
// Directly adjacent
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,9 +552,9 @@ export const gameMode = {
|
|||||||
*/
|
*/
|
||||||
function getBoardLineCount(game) {
|
function getBoardLineCount(game) {
|
||||||
const rows = Math.ceil(game.getTiles().length / game.width);
|
const rows = Math.ceil(game.getTiles().length / game.width);
|
||||||
// header + top border + rows + bottom border + empty + status + message
|
// header + top border + rows + blank lines between rows + bottom border + empty + status + message + controls
|
||||||
// (prompt uses write not writeln, so doesn't add a line)
|
// (prompt uses write not writeln, so doesn't add a line)
|
||||||
return 1 + 1 + rows + 1 + 1 + 1 + 1;
|
return 1 + 1 + rows + Math.max(0, rows - 1) + 1 + 1 + 1 + 1 + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -607,6 +603,9 @@ function renderBoard(term, game, selectedIndex, message = '') {
|
|||||||
|
|
||||||
line += '│';
|
line += '│';
|
||||||
term.writeln(line);
|
term.writeln(line);
|
||||||
|
if (row < rows - 1) {
|
||||||
|
term.writeln(` │${' '.repeat(width)}│`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bottom border
|
// Bottom border
|
||||||
@@ -621,6 +620,9 @@ function renderBoard(term, game, selectedIndex, message = '') {
|
|||||||
const msgText = message || '';
|
const msgText = message || '';
|
||||||
term.writeln(` ${msgText}`.padEnd(50));
|
term.writeln(` ${msgText}`.padEnd(50));
|
||||||
|
|
||||||
|
// Clickable controls line
|
||||||
|
term.writeln(' \x1b[90m[ \x1b[36madd\x1b[90m ] [ \x1b[36mhint\x1b[90m ] [ \x1b[36mnew\x1b[90m ] [ \x1b[36mquit\x1b[90m ]\x1b[0m');
|
||||||
|
|
||||||
// Prompt
|
// Prompt
|
||||||
term.write('\x1b[33mgame>\x1b[0m ');
|
term.write('\x1b[33mgame>\x1b[0m ');
|
||||||
|
|
||||||
|
|||||||
56
website/scripts/commands/samsay.js
Normal file
56
website/scripts/commands/samsay.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* SAM Say command - Use SAM (Software Automatic Mouth) to speak text
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SAM (Software Automatic Mouth) speech synthesizer instance
|
||||||
|
* @type {object|null}
|
||||||
|
*/
|
||||||
|
let sam = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SAM speech synthesizer with SAM voice
|
||||||
|
* @returns {object|null} SAM instance or null if unavailable
|
||||||
|
*/
|
||||||
|
function initSam() {
|
||||||
|
if (sam) return sam;
|
||||||
|
if (typeof SamJs !== 'undefined') {
|
||||||
|
// SAM preset: speed=72, pitch=64, mouth=128, throat=128
|
||||||
|
sam = new SamJs({ speed: 72, pitch: 64, mouth: 128, throat: 128 });
|
||||||
|
}
|
||||||
|
return sam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speak text using SAM
|
||||||
|
* @param {string} text - Text to speak
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function samSpeak(text) {
|
||||||
|
const samInstance = initSam();
|
||||||
|
if (samInstance) {
|
||||||
|
try {
|
||||||
|
samInstance.speak(text);
|
||||||
|
} catch (_err) {
|
||||||
|
// Silently fail if speech doesn't work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
description: 'Use SAM speech synthesizer to speak text',
|
||||||
|
execute: (term, writeClickable, VERSION, args) => {
|
||||||
|
const message = args.join(' ');
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return 'Usage: samsay <message>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof SamJs === 'undefined') {
|
||||||
|
return 'SAM speech synthesizer is not available.';
|
||||||
|
}
|
||||||
|
|
||||||
|
samSpeak(message);
|
||||||
|
return `SAM says: ${message}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -13,10 +13,12 @@ import contactCmd from './scripts/commands/contact.js';
|
|||||||
import privacyCmd from './scripts/commands/privacy.js';
|
import privacyCmd from './scripts/commands/privacy.js';
|
||||||
import blueskyCmd from './scripts/commands/bluesky.js';
|
import blueskyCmd from './scripts/commands/bluesky.js';
|
||||||
import numbermatchCmd, { gameMode, processGameInput, handleTileClick } from './scripts/commands/numbermatch.js';
|
import numbermatchCmd, { gameMode, processGameInput, handleTileClick } from './scripts/commands/numbermatch.js';
|
||||||
|
import samsayCmd from './scripts/commands/samsay.js';
|
||||||
|
|
||||||
// Import Matrix client
|
// Import Matrix client
|
||||||
import {
|
import {
|
||||||
initMatrixClient,
|
initMatrixClient,
|
||||||
|
updateClientSession,
|
||||||
chatMode,
|
chatMode,
|
||||||
matrixApi,
|
matrixApi,
|
||||||
fetchPublicLastMessage,
|
fetchPublicLastMessage,
|
||||||
@@ -32,10 +34,49 @@ import {
|
|||||||
exitChatMode,
|
exitChatMode,
|
||||||
updateQuickCommands,
|
updateQuickCommands,
|
||||||
runChatCommand,
|
runChatCommand,
|
||||||
|
runGameCommand,
|
||||||
renderChatPrompt,
|
renderChatPrompt,
|
||||||
sendChatMessage
|
sendChatMessage
|
||||||
} from './matrix-client.js';
|
} from './matrix-client.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SAM (Software Automatic Mouth) speech synthesizer instance
|
||||||
|
* @type {object|null}
|
||||||
|
*/
|
||||||
|
let sam = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SAM speech synthesizer with SAM voice
|
||||||
|
* @returns {object|null} SAM instance or null if unavailable
|
||||||
|
*/
|
||||||
|
function initSam() {
|
||||||
|
if (sam) return sam;
|
||||||
|
if (typeof SamJs !== 'undefined') {
|
||||||
|
// SAM preset: speed=72, pitch=64, mouth=128, throat=128
|
||||||
|
sam = new SamJs({ speed: 72, pitch: 64, mouth: 128, throat: 128 });
|
||||||
|
}
|
||||||
|
return sam;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speak text using SAM
|
||||||
|
* @param {string} text - Text to speak
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function samSpeak(text) {
|
||||||
|
const samInstance = initSam();
|
||||||
|
if (samInstance) {
|
||||||
|
try {
|
||||||
|
samInstance.speak(text);
|
||||||
|
} catch (_err) {
|
||||||
|
// Silently fail if speech doesn't work
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose samSpeak globally for matrix-client
|
||||||
|
window.samSpeak = samSpeak;
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
const VERSION = '1.0.0';
|
const VERSION = '1.0.0';
|
||||||
|
|
||||||
@@ -260,6 +301,7 @@ term.registerLinkProvider({
|
|||||||
|
|
||||||
// Find all command names that match our known commands
|
// Find all command names that match our known commands
|
||||||
const commandNames = ['help', 'about', 'clear', 'echo', 'date', 'whoami', 'history', 'color', 'banner', 'bluesky', 'chat', 'github', 'contact', 'privacy', 'numbermatch'];
|
const commandNames = ['help', 'about', 'clear', 'echo', 'date', 'whoami', 'history', 'color', 'banner', 'bluesky', 'chat', 'github', 'contact', 'privacy', 'numbermatch'];
|
||||||
|
const gameCommandNames = ['add', 'hint', 'new', 'quit'];
|
||||||
|
|
||||||
// When in game mode, detect clickable tile numbers on board lines
|
// When in game mode, detect clickable tile numbers on board lines
|
||||||
if (gameMode.active && lineText.includes('│')) {
|
if (gameMode.active && lineText.includes('│')) {
|
||||||
@@ -326,6 +368,33 @@ term.registerLinkProvider({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When in game mode, register add/hint/new/quit as clickable words on the controls line
|
||||||
|
if (gameMode.active) {
|
||||||
|
gameCommandNames.forEach(cmd => {
|
||||||
|
let startIndex = 0;
|
||||||
|
while (true) {
|
||||||
|
const index = lineText.indexOf(cmd, startIndex);
|
||||||
|
if (index === -1) break;
|
||||||
|
const charBefore = index > 0 ? lineText[index - 1] : ' ';
|
||||||
|
const charAfter = index + cmd.length < lineText.length ? lineText[index + cmd.length] : ' ';
|
||||||
|
if (/[\s\[]/.test(charBefore) && /[\s\]]/.test(charAfter)) {
|
||||||
|
const capturedCmd = cmd;
|
||||||
|
links.push({
|
||||||
|
range: {
|
||||||
|
start: { x: index + 1, y: bufferLineNumber },
|
||||||
|
end: { x: index + cmd.length + 1, y: bufferLineNumber }
|
||||||
|
},
|
||||||
|
text: cmd,
|
||||||
|
activate: () => {
|
||||||
|
if (window.runGameCommand) window.runGameCommand(capturedCmd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
startIndex = index + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
callback(links.length > 0 ? links : undefined);
|
callback(links.length > 0 ? links : undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -481,7 +550,14 @@ const commands = {
|
|||||||
},
|
},
|
||||||
numbermatch: {
|
numbermatch: {
|
||||||
description: numbermatchCmd.description,
|
description: numbermatchCmd.description,
|
||||||
execute: (args) => numbermatchCmd.execute(term, writeClickable, VERSION, args, commandHistory)
|
execute: (args) => {
|
||||||
|
numbermatchCmd.execute(term, writeClickable, VERSION, args, commandHistory);
|
||||||
|
updateQuickCommands('game');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
samsay: {
|
||||||
|
description: samsayCmd.description,
|
||||||
|
execute: (args) => samsayCmd.execute(term, writeClickable, VERSION, args, commandHistory)
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
description: 'Connect to chat room',
|
description: 'Connect to chat room',
|
||||||
@@ -538,6 +614,14 @@ const commands = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync session to mxClient if exists
|
||||||
|
if (window.matrixSession.accessToken && window.matrixSession.userId) {
|
||||||
|
updateClientSession({
|
||||||
|
accessToken: window.matrixSession.accessToken,
|
||||||
|
userId: window.matrixSession.userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register/login user if not logged in
|
// Register/login user if not logged in
|
||||||
if (!window.matrixSession.accessToken) {
|
if (!window.matrixSession.accessToken) {
|
||||||
try {
|
try {
|
||||||
@@ -584,6 +668,8 @@ const commands = {
|
|||||||
localStorage.setItem('matrix_username', username);
|
localStorage.setItem('matrix_username', username);
|
||||||
localStorage.setItem('matrix_password', password);
|
localStorage.setItem('matrix_password', password);
|
||||||
|
|
||||||
|
updateClientSession({ accessToken: regData.access_token, userId: regData.user_id });
|
||||||
|
|
||||||
term.writeln(` Registered as: ${regData.user_id}\r\n`);
|
term.writeln(` Registered as: ${regData.user_id}\r\n`);
|
||||||
} else if (regData.errcode === 'M_USER_IN_USE') {
|
} else if (regData.errcode === 'M_USER_IN_USE') {
|
||||||
// Username exists, try to login
|
// Username exists, try to login
|
||||||
@@ -610,6 +696,8 @@ const commands = {
|
|||||||
localStorage.setItem('matrix_username', username);
|
localStorage.setItem('matrix_username', username);
|
||||||
localStorage.setItem('matrix_password', password);
|
localStorage.setItem('matrix_password', password);
|
||||||
|
|
||||||
|
updateClientSession({ accessToken: loginData.access_token, userId: loginData.user_id });
|
||||||
|
|
||||||
term.writeln(` Logged in as: ${loginData.user_id}\r\n`);
|
term.writeln(` Logged in as: ${loginData.user_id}\r\n`);
|
||||||
} else {
|
} else {
|
||||||
return ` Error: Failed to login - ${loginData.error || loginData.errcode || 'Unknown error'}`;
|
return ` Error: Failed to login - ${loginData.error || loginData.errcode || 'Unknown error'}`;
|
||||||
@@ -829,7 +917,12 @@ function positionInlineInput() {
|
|||||||
|
|
||||||
if (mentionSuggestions.style.display !== 'none') {
|
if (mentionSuggestions.style.display !== 'none') {
|
||||||
mentionSuggestions.style.left = inlineInput.style.left;
|
mentionSuggestions.style.left = inlineInput.style.left;
|
||||||
mentionSuggestions.style.top = ((cursorY * charHeight) - charHeight - 6) + 'px';
|
|
||||||
|
// Get the actual height of the suggestions box
|
||||||
|
const suggestionsHeight = mentionSuggestions.offsetHeight || 0;
|
||||||
|
|
||||||
|
// Position it above the cursor line
|
||||||
|
mentionSuggestions.style.top = ((cursorY * charHeight) - suggestionsHeight - 6) + 'px';
|
||||||
mentionSuggestions.style.fontSize = term.options.fontSize + 'px';
|
mentionSuggestions.style.fontSize = term.options.fontSize + 'px';
|
||||||
mentionSuggestions.style.lineHeight = charHeight + 'px';
|
mentionSuggestions.style.lineHeight = charHeight + 'px';
|
||||||
}
|
}
|
||||||
@@ -864,6 +957,192 @@ function hideInlineInput() {
|
|||||||
mentionSuggestions.style.display = 'none';
|
mentionSuggestions.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command autocomplete state
|
||||||
|
*/
|
||||||
|
const commandAutocomplete = {
|
||||||
|
matches: [],
|
||||||
|
index: -1,
|
||||||
|
tokenStart: 0,
|
||||||
|
tokenEnd: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get command matches for autocomplete
|
||||||
|
* @param {string} query - Command prefix to match
|
||||||
|
* @param {boolean} isChatMode - Whether in chat mode
|
||||||
|
* @returns {Array<{name: string, description: string}>} Matching commands
|
||||||
|
*/
|
||||||
|
function getCommandMatches(query, isChatMode) {
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
|
if (isChatMode) {
|
||||||
|
// Chat commands
|
||||||
|
const chatCommands = [
|
||||||
|
{ name: '/help', description: 'Show chat commands' },
|
||||||
|
{ name: '/nick', description: 'Change display name' },
|
||||||
|
{ name: '/samsay', description: 'Send message with SAM speech' },
|
||||||
|
{ name: '/quit', description: 'Exit chat mode' }
|
||||||
|
];
|
||||||
|
return chatCommands.filter(cmd => cmd.name.startsWith(lowerQuery));
|
||||||
|
} else {
|
||||||
|
// Terminal commands
|
||||||
|
const terminalCommands = Object.keys(commands).map(name => ({
|
||||||
|
name,
|
||||||
|
description: commands[name].description || ''
|
||||||
|
}));
|
||||||
|
return terminalCommands.filter(cmd => cmd.name.toLowerCase().startsWith(lowerQuery));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if command suggestions are visible
|
||||||
|
* @returns {boolean} True if suggestions are visible
|
||||||
|
*/
|
||||||
|
function hasVisibleCommandSuggestions() {
|
||||||
|
return mentionSuggestions.style.display !== 'none'
|
||||||
|
&& commandAutocomplete.matches.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset command autocomplete state
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function resetCommandAutocomplete() {
|
||||||
|
commandAutocomplete.matches = [];
|
||||||
|
commandAutocomplete.index = -1;
|
||||||
|
commandAutocomplete.tokenStart = 0;
|
||||||
|
commandAutocomplete.tokenEnd = 0;
|
||||||
|
mentionSuggestions.style.display = 'none';
|
||||||
|
mentionSuggestions.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render command suggestions
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function renderCommandSuggestions() {
|
||||||
|
const { matches, index } = commandAutocomplete;
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
mentionSuggestions.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitedMatches = matches.slice(0, 5);
|
||||||
|
mentionSuggestions.innerHTML = limitedMatches.map((cmd, i) => {
|
||||||
|
const selected = i === index ? ' selected' : '';
|
||||||
|
return `<div class="mention-item${selected}">${cmd.name} <span style="opacity: 0.6">- ${cmd.description}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
mentionSuggestions.style.display = 'block';
|
||||||
|
positionInlineInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh command suggestions based on current input
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function refreshCommandSuggestions() {
|
||||||
|
const value = inlineInput.value;
|
||||||
|
const cursorPos = inlineInput.selectionStart;
|
||||||
|
|
||||||
|
// Check if we're at the start with a "/" or just typing a command
|
||||||
|
const isChatMode = chatMode.active;
|
||||||
|
let query = '';
|
||||||
|
let tokenStart = 0;
|
||||||
|
let tokenEnd = cursorPos;
|
||||||
|
|
||||||
|
if (isChatMode) {
|
||||||
|
// In chat mode, look for /command at start
|
||||||
|
if (value.startsWith('/')) {
|
||||||
|
const match = value.match(/^(\/\w*)/);
|
||||||
|
if (match && cursorPos <= match[1].length) {
|
||||||
|
query = match[1];
|
||||||
|
tokenStart = 0;
|
||||||
|
tokenEnd = match[1].length;
|
||||||
|
} else {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In terminal mode, look for command at start (no slash)
|
||||||
|
if (cursorPos === value.length) {
|
||||||
|
const match = value.match(/^(\w*)/);
|
||||||
|
if (match && match[1].length > 0) {
|
||||||
|
query = match[1];
|
||||||
|
tokenStart = 0;
|
||||||
|
tokenEnd = match[1].length;
|
||||||
|
} else {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = getCommandMatches(query, isChatMode);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commandAutocomplete.matches = matches;
|
||||||
|
commandAutocomplete.index = -1;
|
||||||
|
commandAutocomplete.tokenStart = tokenStart;
|
||||||
|
commandAutocomplete.tokenEnd = tokenEnd;
|
||||||
|
|
||||||
|
renderCommandSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the selected command autocomplete
|
||||||
|
* @returns {boolean} True if a command was applied
|
||||||
|
*/
|
||||||
|
function applyCommandAutocomplete() {
|
||||||
|
if (!hasVisibleCommandSuggestions()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle through commands or select first
|
||||||
|
if (commandAutocomplete.index < commandAutocomplete.matches.length - 1) {
|
||||||
|
commandAutocomplete.index++;
|
||||||
|
} else {
|
||||||
|
commandAutocomplete.index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = commandAutocomplete.matches[commandAutocomplete.index];
|
||||||
|
const fullValue = inlineInput.value;
|
||||||
|
const valueAfter = fullValue.slice(commandAutocomplete.tokenEnd);
|
||||||
|
const isChatMode = chatMode.active;
|
||||||
|
|
||||||
|
// For chat commands, include the slash; for terminal commands, don't
|
||||||
|
const commandText = selected.name;
|
||||||
|
const needsSpace = !valueAfter.startsWith(' ') && valueAfter.length > 0;
|
||||||
|
const newValue = commandText + (needsSpace ? ' ' : '') + valueAfter;
|
||||||
|
const newCursor = commandText.length + (needsSpace ? 1 : 0);
|
||||||
|
|
||||||
|
inlineInput.value = newValue;
|
||||||
|
inlineInput.setSelectionRange(newCursor, newCursor);
|
||||||
|
|
||||||
|
if (isChatMode) {
|
||||||
|
chatMode.inputLine = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update token end for continued cycling
|
||||||
|
commandAutocomplete.tokenEnd = commandText.length;
|
||||||
|
|
||||||
|
renderCommandSuggestions();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit current input
|
* Submit current input
|
||||||
*/
|
*/
|
||||||
@@ -889,6 +1168,7 @@ async function submitInlineInput() {
|
|||||||
term.writeln('\x1b[33mChat Commands:\x1b[0m');
|
term.writeln('\x1b[33mChat Commands:\x1b[0m');
|
||||||
term.writeln(' /help - Show this help message');
|
term.writeln(' /help - Show this help message');
|
||||||
term.writeln(' /nick [name] - Change your display name');
|
term.writeln(' /nick [name] - Change your display name');
|
||||||
|
term.writeln(' /samsay [text] - Send message with SAM speech');
|
||||||
term.writeln(' /quit - Exit chat mode');
|
term.writeln(' /quit - Exit chat mode');
|
||||||
term.writeln('─'.repeat(term.cols || 60));
|
term.writeln('─'.repeat(term.cols || 60));
|
||||||
term.write('\x1b[1;32m>\x1b[0m ');
|
term.write('\x1b[1;32m>\x1b[0m ');
|
||||||
@@ -939,6 +1219,24 @@ async function submitInlineInput() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmd.startsWith('/samsay ')) {
|
||||||
|
const message = cmd.substring(8).trim();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
// Move cursor up to separator, clear it
|
||||||
|
term.write('\x1b[1A\x1b[2K\r');
|
||||||
|
term.writeln('\x1b[31mError: /samsay [text]\x1b[0m');
|
||||||
|
term.writeln('─'.repeat(term.cols || 60));
|
||||||
|
term.write('\x1b[1;32m>\x1b[0m ');
|
||||||
|
setTimeout(() => positionInlineInput(), 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to chat (will play when message comes back)
|
||||||
|
await sendChatMessage(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (cmd && !cmd.startsWith('/')) {
|
if (cmd && !cmd.startsWith('/')) {
|
||||||
// Don't write the message here - let sync handle it
|
// Don't write the message here - let sync handle it
|
||||||
await sendChatMessage(cmd);
|
await sendChatMessage(cmd);
|
||||||
@@ -962,6 +1260,7 @@ async function submitInlineInput() {
|
|||||||
term.write(rawValue + '\r\n');
|
term.write(rawValue + '\r\n');
|
||||||
const continueGame = processGameInput(term, cmd);
|
const continueGame = processGameInput(term, cmd);
|
||||||
if (!continueGame) {
|
if (!continueGame) {
|
||||||
|
updateQuickCommands('terminal');
|
||||||
writePrompt();
|
writePrompt();
|
||||||
}
|
}
|
||||||
setTimeout(() => positionInlineInput(), 10);
|
setTimeout(() => positionInlineInput(), 10);
|
||||||
@@ -1051,8 +1350,9 @@ async function init() {
|
|||||||
welcomeBannerMinimal
|
welcomeBannerMinimal
|
||||||
});
|
});
|
||||||
|
|
||||||
// Expose chat command for onclick handlers
|
// Expose chat and game command handlers for onclick buttons
|
||||||
window.runChatCommand = runChatCommand;
|
window.runChatCommand = runChatCommand;
|
||||||
|
window.runGameCommand = runGameCommand;
|
||||||
|
|
||||||
// Display welcome banner with clickable commands
|
// Display welcome banner with clickable commands
|
||||||
const cols = term.cols;
|
const cols = term.cols;
|
||||||
@@ -1084,24 +1384,62 @@ async function init() {
|
|||||||
// Handle inline input events
|
// Handle inline input events
|
||||||
inlineInput.addEventListener('keydown', async (e) => {
|
inlineInput.addEventListener('keydown', async (e) => {
|
||||||
playTerminalKeySound(e);
|
playTerminalKeySound(e);
|
||||||
if (e.key === 'Escape' && hasVisibleMentionSuggestions()) {
|
|
||||||
|
// Check if we have visible suggestions (mentions OR commands)
|
||||||
|
const hasMentions = hasVisibleMentionSuggestions();
|
||||||
|
const hasCommands = hasVisibleCommandSuggestions();
|
||||||
|
const hasSuggestions = hasMentions || hasCommands;
|
||||||
|
|
||||||
|
if (e.key === 'Escape' && hasSuggestions) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
resetMentionAutocomplete();
|
if (hasMentions) resetMentionAutocomplete();
|
||||||
} else if (e.key === ' ' && hasVisibleMentionSuggestions()) {
|
if (hasCommands) resetCommandAutocomplete();
|
||||||
e.preventDefault();
|
} else if (e.key === ' ' && hasSuggestions) {
|
||||||
commitSelectedMentionSuggestion();
|
|
||||||
} else if (e.key === 'Enter' && hasVisibleMentionSuggestions()) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (hasMentions) commitSelectedMentionSuggestion();
|
||||||
|
if (hasCommands) {
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
inlineInput.value += ' ';
|
||||||
|
if (chatMode.active) chatMode.inputLine = inlineInput.value;
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Enter' && hasSuggestions) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (hasMentions) {
|
||||||
commitSelectedMentionSuggestion();
|
commitSelectedMentionSuggestion();
|
||||||
|
} else if (hasCommands) {
|
||||||
|
// Just accept the current command and submit
|
||||||
|
resetCommandAutocomplete();
|
||||||
|
submitInlineInput();
|
||||||
|
}
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submitInlineInput();
|
submitInlineInput();
|
||||||
} else if (e.key === 'Tab' && chatMode.active) {
|
} else if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (chatMode.active && hasMentions) {
|
||||||
await applyMentionAutocomplete();
|
await applyMentionAutocomplete();
|
||||||
|
} else if (hasCommands) {
|
||||||
|
applyCommandAutocomplete();
|
||||||
|
} else {
|
||||||
|
// Trigger command suggestions on Tab
|
||||||
|
refreshCommandSuggestions();
|
||||||
|
if (hasVisibleCommandSuggestions()) {
|
||||||
|
applyCommandAutocomplete();
|
||||||
|
} else if (chatMode.active) {
|
||||||
|
// Try mention autocomplete in chat
|
||||||
|
await applyMentionAutocomplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (commandHistory.length > 0) {
|
if (hasCommands) {
|
||||||
|
// Navigate command suggestions up
|
||||||
|
if (commandAutocomplete.index > 0) {
|
||||||
|
commandAutocomplete.index--;
|
||||||
|
renderCommandSuggestions();
|
||||||
|
}
|
||||||
|
} else if (commandHistory.length > 0) {
|
||||||
|
// Navigate command history
|
||||||
if (historyIndex === -1 || historyIndex >= commandHistory.length) {
|
if (historyIndex === -1 || historyIndex >= commandHistory.length) {
|
||||||
historyIndex = commandHistory.length - 1;
|
historyIndex = commandHistory.length - 1;
|
||||||
} else if (historyIndex > 0) {
|
} else if (historyIndex > 0) {
|
||||||
@@ -1111,7 +1449,13 @@ async function init() {
|
|||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (historyIndex < commandHistory.length - 1) {
|
if (hasCommands) {
|
||||||
|
// Navigate command suggestions down
|
||||||
|
if (commandAutocomplete.index < commandAutocomplete.matches.length - 1) {
|
||||||
|
commandAutocomplete.index++;
|
||||||
|
renderCommandSuggestions();
|
||||||
|
}
|
||||||
|
} else if (historyIndex < commandHistory.length - 1) {
|
||||||
historyIndex++;
|
historyIndex++;
|
||||||
inlineInput.value = commandHistory[historyIndex] || '';
|
inlineInput.value = commandHistory[historyIndex] || '';
|
||||||
} else {
|
} else {
|
||||||
@@ -1126,6 +1470,9 @@ async function init() {
|
|||||||
chatMode.inputLine = inlineInput.value;
|
chatMode.inputLine = inlineInput.value;
|
||||||
await refreshMentionSuggestionsFromInput();
|
await refreshMentionSuggestionsFromInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh command suggestions if typing at the start
|
||||||
|
refreshCommandSuggestions();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click on terminal focuses input
|
// Click on terminal focuses input
|
||||||
|
|||||||
Reference in New Issue
Block a user