299 lines
27 KiB
JavaScript
299 lines
27 KiB
JavaScript
/* Life OS command line: only the listed application commands are executable. */
|
||
(() => {
|
||
const t = (key, values) => LifeI18n.t(`terminal.${key}`, values);
|
||
const actions = () => window.LifeOSActions;
|
||
const startedAt = Date.now();
|
||
const localDate = offset => { const value = new Date(); value.setDate(value.getDate() + offset); return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; };
|
||
const date = value => value === 'today' ? localDate(0) : value === 'tomorrow' ? localDate(1) : value || '';
|
||
const validDay = value => { const parsed = new Date(`${value}T12:00:00`); return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(parsed.getTime()) && `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, '0')}-${String(parsed.getDate()).padStart(2, '0')}` === value; };
|
||
const words = line => {
|
||
const result = []; let current = '', quote = '', escaped = false, started = false;
|
||
for (const char of line) {
|
||
if (escaped) { current += char; escaped = false; started = true; }
|
||
else if (char === '\\' && quote) escaped = true;
|
||
else if (quote) { if (char === quote) quote = ''; else current += char; }
|
||
else if (char === '"' || char === "'") { quote = char; started = true; }
|
||
else if (/\s/.test(char)) { if (started) { result.push(current); current = ''; started = false; } }
|
||
else { current += char; started = true; }
|
||
}
|
||
if (quote || escaped) throw new Error('unclosedQuote');
|
||
if (started) result.push(current);
|
||
return result;
|
||
};
|
||
const options = (input, allowed) => {
|
||
const flags = {}, values = [];
|
||
for (let index = 0; index < input.length; index++) {
|
||
const value = input[index];
|
||
if (value.startsWith('--')) {
|
||
const key = value.slice(2);
|
||
if (!allowed.includes(key)) throw new Error('invalidOption');
|
||
if (key === 'pin') { flags.pin = true; continue; }
|
||
if (input[index + 1] === undefined || input[index + 1].startsWith('--')) throw new Error('invalidOption');
|
||
if (key === 'tag') (flags.tag ||= []).push(input[++index]); else flags[key] = input[++index];
|
||
} else values.push(value);
|
||
}
|
||
return { flags, values };
|
||
};
|
||
const resolve = (items, value) => {
|
||
if (!value) return '';
|
||
if (/^[1-9]\d*$/.test(value) && items[Number(value) - 1]) return items[Number(value) - 1].id;
|
||
const matches = items.filter(item => item.id === value || item.title.toLocaleLowerCase() === value.toLocaleLowerCase());
|
||
if (!matches.length) throw new Error('notFound');
|
||
if (matches.length > 1) throw new Error('ambiguous');
|
||
return matches[0].id;
|
||
};
|
||
const board = () => actions().board();
|
||
const requireTitle = value => { if (!value?.trim()) throw new Error('usage'); return value.trim(); };
|
||
const help = () => ['help', 'memory add / memory add "…" — ' + t('guidedHelp'), 'memory add "…" [--date YYYY-MM-DD] [--tag …] [--energy 1-5] [--weather cloudy] [--place …] [--task …] [--text …]', 'memory list [--tag …] [--query …] / memory delete [#|date|title]', 'goal add ["…"] / goal list / goal delete [#|title]', 'goal add "…" --due YYYY-MM-DD [--description …] [--pin]', 'goal done <#> / goal reopen <#>', 'task add ["…"] / task list / task delete [#|title]', 'task add "…" --due tomorrow [--goal …] [--priority high|normal|low]', 'task done <#> / task reopen <#>', 'date / time / datetime / uptime / version', 'status / apps / history / echo "…"', 'open <app> / clear / console'].join('\n');
|
||
const wizardFields = ['title', 'date', 'tags', 'energy', 'mood', 'location', 'taskId'];
|
||
let wizard = null;
|
||
let flow = null;
|
||
const itemsFor = kind => kind === 'memory' ? mems() : board()[kind === 'goal' ? 'goals' : 'tasks'];
|
||
const itemLabel = (kind, item) => kind === 'memory' ? `${item.date} ${item.title || item.text?.slice(0, 40) || '—'}` : item.title;
|
||
const findItem = (kind, value) => {
|
||
const items = itemsFor(kind);
|
||
if (/^[1-9]\d*$/.test(value) && items[Number(value) - 1]) return items[Number(value) - 1];
|
||
const matches = items.filter(item => item.id === value || (kind === 'memory' && item.date === value) || item.title?.toLocaleLowerCase() === value.toLocaleLowerCase());
|
||
if (!matches.length) throw new Error('notFound');
|
||
if (matches.length > 1) throw new Error('ambiguous');
|
||
return matches[0];
|
||
};
|
||
const flowFields = { goal: ['title','description','targetDate','showOnDesktop'], task: ['title','dueDate','goalId','priority'] };
|
||
const flowPrompt = () => {
|
||
if (flow.operation === 'delete' && !flow.target) return { message: `${t('flow.pick', { kind: t(flow.kind) })}\n${itemsFor(flow.kind).map((item,index) => `${index + 1}. ${itemLabel(flow.kind,item)}`).slice(0,30).join('\n')}\n${t('wizard.controls')}` };
|
||
if (flow.operation === 'delete') return { message: t('flow.confirmDelete', { kind: t(flow.kind), title: itemLabel(flow.kind,flow.target) }) };
|
||
const field = flowFields[flow.kind][flow.step];
|
||
if (!field) return { message: t('flow.confirmCreate', { kind: t(flow.kind), title: flow.draft.title }) };
|
||
const items = field === 'goalId' ? board().goals.map((goal,index) => `${index + 1}. ${goal.title}`).slice(0,20).join('\n') || '—' : '';
|
||
const value = field === 'goalId' ? board().goals.find(goal => goal.id === flow.draft.goalId)?.title : flow.draft[field];
|
||
return { message: `${t(`flow.${field}`, { value: value || '—', items })}\n${t('wizard.controls')}` };
|
||
};
|
||
const startFlow = (kind, operation, value = '') => {
|
||
if (operation === 'delete') { if (!itemsFor(kind).length) return { message: t('empty') }; flow = { kind, operation, target: value ? findItem(kind,value) : null }; return flowPrompt(); }
|
||
flow = { kind, operation, step: value ? 1 : 0, draft: kind === 'goal' ? { title: value, description: '', targetDate: '', showOnDesktop: false } : { title: value, dueDate: '', goalId: '', priority: 'normal' } };
|
||
return flowPrompt();
|
||
};
|
||
const answerFlow = input => {
|
||
const value = input.trim(), current = flow;
|
||
if (value === 'cancel') { flow = null; return { message: t('flow.cancelled'), cancel: true }; }
|
||
if (value === 'back') {
|
||
if (current.operation === 'delete') current.target = null;
|
||
else current.step = Math.max(0,current.step - 1);
|
||
return flowPrompt();
|
||
}
|
||
if (current.operation === 'delete') {
|
||
if (!current.target) { if (!value) throw new Error('usage'); current.target = findItem(current.kind,value); return flowPrompt(); }
|
||
if (!['yes','y'].includes(value.toLowerCase())) { if (!['no','n'].includes(value.toLowerCase())) throw new Error('confirmAnswer'); flow = null; return { message: t('flow.cancelled'), cancel: true }; }
|
||
const item = itemsFor(current.kind).find(record => record.id === current.target.id);
|
||
if (!item) { flow = null; throw new Error('notFound'); }
|
||
const label = itemLabel(current.kind,item);
|
||
actions()[`delete${current.kind[0].toUpperCase()}${current.kind.slice(1)}`](item.id);
|
||
flow = null; return { message: t('flow.deleted', { kind: t(current.kind), title: label }) };
|
||
}
|
||
const field = flowFields[current.kind][current.step], draft = current.draft;
|
||
if (!field) {
|
||
if (!['yes','y'].includes(value.toLowerCase())) { if (!['no','n'].includes(value.toLowerCase())) throw new Error('confirmAnswer'); flow = null; return { message: t('flow.cancelled'), cancel: true }; }
|
||
const item = current.kind === 'goal' ? actions().createGoal(draft) : actions().createTask(draft);
|
||
flow = null; return { message: t(current.kind === 'goal' ? 'goalSaved' : 'taskSaved', { title: item.title }) };
|
||
}
|
||
if (field === 'title') { if (!value && !draft.title || value.length > (current.kind === 'goal' ? 100 : 140)) throw new Error('invalidTitle'); draft.title = value || draft.title; }
|
||
if (field === 'description') { if (value.length > 240) throw new Error('invalidValue'); if (value) draft.description = value === '-' ? '' : value; }
|
||
if (field === 'targetDate' || field === 'dueDate') { const selected = value === '-' ? '' : date(value) || draft[field]; if (selected && !validDay(selected)) throw new Error('invalidDate'); draft[field] = selected; }
|
||
if (field === 'showOnDesktop') { if (value && !['yes','y','no','n'].includes(value.toLowerCase())) throw new Error('invalidValue'); const enabled = value ? ['yes','y'].includes(value.toLowerCase()) : draft.showOnDesktop; if (enabled && !draft.targetDate) throw new Error('pinNeedsDate'); draft.showOnDesktop = enabled; }
|
||
if (field === 'goalId' && value) draft.goalId = value === '-' ? '' : resolve(board().goals,value);
|
||
if (field === 'priority') { if (value && !['high','normal','low'].includes(value.toLowerCase())) throw new Error('invalidPriority'); draft.priority = value ? value.toLowerCase() : draft.priority; }
|
||
current.step++; return flowPrompt();
|
||
};
|
||
const wizardPrompt = () => {
|
||
if (wizard.step >= wizardFields.length) return { compose: { ...wizard.draft } };
|
||
const field = wizardFields[wizard.step], tasks = field === 'taskId' ? board().tasks.map((item, index) => `${index + 1}. ${item.title}`).slice(0, 20).join('\n') || '—' : '';
|
||
const current = field === 'taskId' ? board().tasks.find(item => item.id === wizard.draft.taskId)?.title : field === 'tags' ? wizard.draft.tags.join(', ') : wizard.draft[field];
|
||
return { message: `${t(`wizard.${field}`, { tasks, value: current || '—' })}\n${t('wizard.controls')}` };
|
||
};
|
||
const nextStep = () => { while (wizard.step < wizardFields.length && wizard.seeded.has(wizardFields[wizard.step])) wizard.step++; return wizardPrompt(); };
|
||
const backStep = () => {
|
||
if (!wizard) return { message: t('noWizard') };
|
||
wizard.step = Math.min(wizard.step, wizardFields.length);
|
||
wizard.step = Math.max(0, wizard.step - 1);
|
||
return wizardPrompt();
|
||
};
|
||
const answer = input => {
|
||
const value = input.trim();
|
||
if (value === 'cancel') { wizard = null; return { message: t('cancelled'), cancel: true }; }
|
||
if (value === 'back') return backStep();
|
||
const field = wizardFields[wizard.step], draft = wizard.draft;
|
||
if (field === 'title') { if ((!value && !draft.title) || value.length > 120) throw new Error('invalidTitle'); draft.title = value || draft.title; }
|
||
if (field === 'date') {
|
||
const selected = date(value) || draft.date || localDate(0);
|
||
if (!validDay(selected)) throw new Error('invalidDate');
|
||
if (mems().some(item => item.date === selected)) throw new Error('memoryExists');
|
||
draft.date = selected;
|
||
}
|
||
if (field === 'tags' && value) draft.tags = value === '-' ? [] : [...new Set(value.split(/[,,]/).map(tag => tag.trim()).filter(Boolean))].slice(0, 12);
|
||
if (field === 'energy') { if (value && !/^[1-5]$/.test(value)) throw new Error('invalidValue'); draft.energy = value ? Number(value) : draft.energy; }
|
||
if (field === 'mood') { if (value && !['clear','light','cloudy','rain','storm'].includes(value.toLowerCase())) throw new Error('invalidValue'); draft.mood = value ? value.toLowerCase() : draft.mood; }
|
||
if (field === 'location') { if (value.length > 100) throw new Error('invalidValue'); if (value) draft.location = value === '-' ? '' : value; }
|
||
if (field === 'taskId' && value) draft.taskId = value === '-' ? '' : resolve(board().tasks, value);
|
||
wizard.step++;
|
||
return nextStep();
|
||
};
|
||
const run = line => {
|
||
if (flow) return answerFlow(line);
|
||
if (wizard) return answer(line);
|
||
const [kind, verb, ...rest] = words(line.trim());
|
||
if (!kind) return {};
|
||
if (kind === 'help') return { message: help() };
|
||
if (kind === 'clear') return { clear: true };
|
||
if (kind === 'date' || kind === 'time' || kind === 'datetime') {
|
||
if (verb) throw new Error('usage');
|
||
const options = kind === 'date' ? { dateStyle: 'full' } : kind === 'time' ? { timeStyle: 'medium' } : { dateStyle: 'full', timeStyle: 'medium' };
|
||
return { message: new Intl.DateTimeFormat(LifeI18n.locale(), options).format(new Date()) };
|
||
}
|
||
if (kind === 'uptime') { if (verb) throw new Error('usage'); const seconds = Math.floor((Date.now() - startedAt) / 1000); return { message: t('uptime', { minutes: Math.floor(seconds / 60), seconds: seconds % 60 }) }; }
|
||
if (kind === 'version') { if (verb) throw new Error('usage'); return { message: `Life OS v${window.LifeOSVersion || '1.18.1'}` }; }
|
||
if (kind === 'status') { if (verb) throw new Error('usage'); const data = board(); return { message: t('status', { memories: mems().length, goals: data.goals.length, tasks: data.tasks.length }) }; }
|
||
if (kind === 'apps') { if (verb) throw new Error('usage'); return { message: Object.keys(APPS).map(id => `${id} — ${LifeI18n.t(`app.${id}`)}`).join('\n') }; }
|
||
if (kind === 'history') { if (verb) throw new Error('usage'); return { message: history.slice(-20).map((entry,index) => `${index + 1}. ${entry}`).join('\n') || t('empty') }; }
|
||
if (kind === 'echo') return { message: words(line.trim()).slice(1).join(' ') };
|
||
if (kind === 'console') return { message: t('consoleHelp') };
|
||
if (kind === 'open') {
|
||
if (!Object.hasOwn(APPS, verb)) throw new Error('unknownApp');
|
||
openApp(verb); return { message: t('opened', { app: LifeI18n.t(`app.${verb}`) }) };
|
||
}
|
||
if (kind === 'memory' && verb === 'add') {
|
||
const { flags, values } = options(rest, ['date','tag','energy','weather','place','task','text']), title = values.join(' ').trim();
|
||
const draft = { title, date: date(flags.date) || localDate(0), tags: flags.tag || [], energy: flags.energy === undefined ? 3 : Number(flags.energy), mood: flags.weather || 'cloudy', location: flags.place || '', taskId: resolve(board().tasks, flags.task), text: flags.text || '' };
|
||
if (!validDay(draft.date)) throw new Error('invalidDate');
|
||
if (mems().some(item => item.date === draft.date) && (flags.date || flags.text !== undefined)) throw new Error('memoryExists');
|
||
if (!Number.isInteger(draft.energy) || draft.energy < 1 || draft.energy > 5 || !['clear','light','cloudy','rain','storm'].includes(draft.mood)) throw new Error('invalidValue');
|
||
if (flags.text === undefined) {
|
||
wizard = { draft, step: 0, seeded: new Set([...(title ? ['title'] : []), ...Object.keys(flags).map(key => ({ tag: 'tags', weather: 'mood', place: 'location', task: 'taskId' }[key] || key))]) };
|
||
return nextStep();
|
||
}
|
||
requireTitle(title);
|
||
const memory = actions().createMemory(draft); return { message: t('memorySaved', { title: memory.title, date: memory.date }) };
|
||
}
|
||
if (kind === 'memory' && verb === 'list') {
|
||
const { flags, values } = options(rest, ['tag','query']); if (values.length) throw new Error('usage');
|
||
const found = mems().filter(item => (!flags.tag?.length || flags.tag.every(tag => (item.tags || []).includes(tag))) && (!flags.query || [item.title, item.text, item.location].join(' ').toLocaleLowerCase().includes(flags.query.toLocaleLowerCase())));
|
||
return { message: found.length ? found.slice(0, 30).map(item => `${item.date} ${item.title || item.text?.slice(0, 50) || '—'}`).join('\n') : t('empty') };
|
||
}
|
||
if (kind === 'memory' && verb === 'delete') return startFlow('memory','delete',rest.join(' '));
|
||
if (kind === 'goal' && verb === 'add') {
|
||
if (!rest.some(part => part.startsWith('--'))) return startFlow('goal','add',rest.join(' '));
|
||
const { flags, values } = options(rest, ['due','description','pin']); if (flags.pin && !flags.due) throw new Error('pinNeedsDate'); const goal = actions().createGoal({ title: requireTitle(values.join(' ')), targetDate: date(flags.due), description: flags.description || '', showOnDesktop: Boolean(flags.pin) });
|
||
return { message: t('goalSaved', { title: goal.title }) };
|
||
}
|
||
if (kind === 'goal' && verb === 'list') {
|
||
const goals = board().goals;
|
||
return { message: goals.length ? goals.map((goal, index) => `${index + 1}. ${goal.done ? '✓' : '○'} ${goal.title}${goal.targetDate ? ` ${goal.targetDate}` : ''}`).join('\n') : t('empty') };
|
||
}
|
||
if (kind === 'goal' && verb === 'delete') return startFlow('goal','delete',rest.join(' '));
|
||
if (kind === 'goal' && ['done','reopen'].includes(verb)) {
|
||
if (!rest.length) throw new Error('usage');
|
||
const goal = actions().setGoalDone(findItem('goal',rest.join(' ')).id,verb === 'done');
|
||
return { message: t(verb === 'done' ? 'goalCompleted' : 'goalReopened', { title: goal.title }) };
|
||
}
|
||
if (kind === 'task' && verb === 'add') {
|
||
if (!rest.some(part => part.startsWith('--'))) return startFlow('task','add',rest.join(' '));
|
||
const { flags, values } = options(rest, ['due','goal','priority']); const task = actions().createTask({ title: requireTitle(values.join(' ')), dueDate: date(flags.due), goalId: resolve(board().goals, flags.goal), priority: flags.priority || 'normal' });
|
||
return { message: t('taskSaved', { title: task.title }) };
|
||
}
|
||
if (kind === 'task' && verb === 'list') {
|
||
const tasks = board().tasks;
|
||
return { message: tasks.length ? tasks.map((task, index) => `${index + 1}. ${task.done ? '✓' : '○'} ${task.title}${task.dueDate ? ` ${task.dueDate}` : ''}`).join('\n') : t('empty') };
|
||
}
|
||
if (kind === 'task' && verb === 'delete') return startFlow('task','delete',rest.join(' '));
|
||
if (kind === 'task' && ['done', 'reopen'].includes(verb)) {
|
||
const index = Number(rest[0]);
|
||
if (!Number.isInteger(index) || index < 1 || rest.length !== 1 || !board().tasks[index - 1]) throw new Error('notFound');
|
||
const task = actions().setTaskDone(board().tasks[index - 1].id, verb === 'done');
|
||
return { message: t(verb === 'done' ? 'taskCompleted' : 'taskReopened', { title: task.title }) };
|
||
}
|
||
throw new Error('unknownCommand');
|
||
};
|
||
const finishWizard = () => { wizard = null; };
|
||
const cancelSession = () => { wizard = null; flow = null; };
|
||
const active = () => Boolean(wizard || flow);
|
||
window.LifeOSTerminalEngine = { run, words, backStep, finishWizard, cancelSession, active };
|
||
|
||
const lines = [], history = [];
|
||
let welcomed = false;
|
||
let historyIndex = 0, compose = null, noteDraft = '';
|
||
const addLine = (value, kind = 'result') => { lines.push({ value, kind }); if (lines.length > 150) lines.shift(); };
|
||
const output = node => {
|
||
const screen = node.querySelector('.terminal-output'); if (!screen) return;
|
||
screen.replaceChildren();
|
||
for (const line of lines) { const pre = document.createElement('pre'); pre.className = `terminal-line ${line.kind}`; pre.textContent = line.value; screen.append(pre); }
|
||
screen.scrollTop = screen.scrollHeight;
|
||
};
|
||
const composer = (node, active) => {
|
||
node.querySelector('.terminal-compose').hidden = !active;
|
||
node.querySelector('.terminal-form').hidden = active;
|
||
if (active) { node.querySelector('.terminal-note').value = noteDraft; node.querySelector('.terminal-note').focus(); }
|
||
else node.querySelector('.terminal-input').focus();
|
||
};
|
||
window.terminalApp = node => {
|
||
if (node.querySelector('.terminal-shell') && node.dataset.terminalLocale === lang()) { output(node); return; }
|
||
if (!welcomed) { lines.unshift({ value: t('welcome', { version: window.LifeOSVersion || '1.18.1' }), kind: 'system' }); welcomed = true; }
|
||
node.dataset.terminalLocale = lang();
|
||
node.innerHTML = `<div class="terminal-shell"><div class="terminal-header"><strong>Life OS / ${t('title')}</strong><span>${t('hint')}</span></div><div class="terminal-output" role="log" aria-live="polite"></div><div class="terminal-shortcuts"><button type="button" data-command="help">help</button><button type="button" data-command="memory add">${t('memory')}</button><button type="button" data-command="task add ">${t('task')}</button><button type="button" data-command="goal add ">${t('goal')}</button><button type="button" data-command="task list">task list</button></div><form class="terminal-form"><label for="terminal-input">lifeos ></label><input id="terminal-input" class="terminal-input" autocomplete="off" autocapitalize="off" spellcheck="false" aria-label="${t('command')}" maxlength="600"><button type="submit">↵</button></form><div class="terminal-compose" hidden><label for="terminal-note">${t('notePrompt')}</label><textarea id="terminal-note" class="terminal-note" rows="5" maxlength="12000"></textarea><div><button type="button" data-note="save">${t('saveNote')}</button><button type="button" data-note="back">${t('back')}</button><button type="button" data-note="cancel">${t('cancel')}</button></div></div></div>`;
|
||
output(node); composer(node, Boolean(compose));
|
||
node.querySelector('.terminal-form').addEventListener('submit', event => {
|
||
event.preventDefault(); const input = node.querySelector('.terminal-input'), line = input.value.trim(); if (!line && !active()) return;
|
||
if (line) history.push(line); historyIndex = history.length; input.value = ''; addLine(`lifeos > ${line || '↵'}`, 'command');
|
||
try { const result = run(line); if (result.clear) lines.length = 0; if (result.message) addLine(result.message); if (result.compose) { compose = result.compose; addLine(t('notePrompt')); composer(node, true); } if (result.cancel) { compose = null; noteDraft = ''; } }
|
||
catch (error) { addLine(t(error.message in LifeI18n.dictionaries[lang()].terminal ? error.message : 'unknownCommand'), 'error'); }
|
||
output(node);
|
||
});
|
||
node.querySelector('.terminal-input').addEventListener('keydown', event => {
|
||
const input = event.currentTarget;
|
||
if (event.key === 'Escape' && active()) { event.preventDefault(); event.stopPropagation(); addLine(t('flow.cancelled')); cancelSession(); output(node); return; }
|
||
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') { event.preventDefault(); historyIndex = Math.max(0, Math.min(history.length, historyIndex + (event.key === 'ArrowUp' ? -1 : 1))); input.value = history[historyIndex] || ''; }
|
||
if (event.key === 'Tab' && !active()) { const roots = ['help','memory add ','memory list','memory delete','goal add ','goal list','goal delete','task add ','task list','task delete','task done ','task reopen ','open ','clear']; const matches = roots.filter(item => item.startsWith(input.value)); if (matches.length === 1) { event.preventDefault(); input.value = matches[0]; } }
|
||
});
|
||
node.querySelector('.terminal-shortcuts').addEventListener('click', event => { const command = event.target.closest('[data-command]')?.dataset.command; if (command) { const input = node.querySelector('.terminal-input'); input.value = command; input.focus(); } });
|
||
const saveNote = () => { try { const memory = actions().createMemory({ ...compose, text: node.querySelector('.terminal-note').value }); addLine(t('memorySaved', { title: memory.title, date: memory.date })); compose = null; noteDraft = ''; finishWizard(); composer(node, false); } catch (error) { addLine(t(error.message in LifeI18n.dictionaries[lang()].terminal ? error.message : 'unknownCommand'), 'error'); } output(node); };
|
||
const cancelNote = () => { compose = null; noteDraft = ''; finishWizard(); addLine(t('cancelled')); composer(node, false); output(node); };
|
||
node.querySelector('.terminal-compose').addEventListener('click', event => { const action = event.target.closest('[data-note]')?.dataset.note; if (action === 'save') saveNote(); if (action === 'cancel') cancelNote(); if (action === 'back') { compose = null; const result = backStep(); addLine(result.message); composer(node, false); output(node); } });
|
||
node.querySelector('.terminal-note').addEventListener('input', event => { noteDraft = event.currentTarget.value; });
|
||
node.querySelector('.terminal-note').addEventListener('keydown', event => { if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); saveNote(); } if (event.key === 'Escape') { event.stopPropagation(); cancelNote(); } });
|
||
};
|
||
const consoleOutput = result => {
|
||
if (result.clear) lines.length = 0;
|
||
if (result.message) addLine(result.message);
|
||
if (result.compose) { compose = result.compose; addLine(t('notePrompt')); }
|
||
if (result.cancel) { compose = null; noteDraft = ''; }
|
||
const node = document.querySelector('#content-terminal');
|
||
if (node?.querySelector('.terminal-shell')) { output(node); if (result.compose || result.cancel) composer(node, Boolean(compose)); }
|
||
const message = result.compose ? t('consoleNotePrompt') : result.message || (result.clear ? t('cleared') : '');
|
||
if (message) console.info('[Life OS]', message);
|
||
return message;
|
||
};
|
||
window.LifeOS = Object.freeze({
|
||
run(line) {
|
||
const input = String(line || '').trim();
|
||
if (compose) { console.info('[Life OS]', t('consoleNotePrompt')); return t('consoleNotePrompt'); }
|
||
if (!input && !active()) return t('usage');
|
||
try { if (input) history.push(input); addLine(`devtools > ${input || '↵'}`, 'command'); return consoleOutput(run(input)); }
|
||
catch (error) { const key = Object.hasOwn(LifeI18n.dictionaries[lang()].terminal,error.message) ? error.message : 'unknownCommand'; return consoleOutput({ message: t(key) }); }
|
||
},
|
||
help() { return this.run('help'); },
|
||
note(text) {
|
||
if (!compose) { console.warn('[Life OS]', t('noNote')); return t('noNote'); }
|
||
try {
|
||
const memory = actions().createMemory({ ...compose, text: String(text || '') });
|
||
compose = null; noteDraft = ''; finishWizard();
|
||
const node = document.querySelector('#content-terminal');
|
||
if (node?.querySelector('.terminal-shell')) composer(node, false);
|
||
return consoleOutput({ message: t('memorySaved', { title: memory.title, date: memory.date }) });
|
||
} catch (error) { const key = Object.hasOwn(LifeI18n.dictionaries[lang()].terminal,error.message) ? error.message : 'unknownCommand'; return consoleOutput({ message: t(key) }); }
|
||
},
|
||
cancel() { cancelSession(); compose = null; noteDraft = ''; return consoleOutput({ message: t('flow.cancelled'), cancel: true }); }
|
||
});
|
||
console.info('[Life OS]', t('consoleReady'));
|
||
const style = document.createElement('style');
|
||
style.textContent = `#content-terminal{padding:0;overflow:hidden}.terminal-shell{height:100%;display:flex;flex-direction:column;background:#111a16;color:#d4efd2;font:13px/1.55 'DM Mono',ui-monospace,monospace}.terminal-header{display:flex;justify-content:space-between;gap:10px;padding:12px 15px;border-bottom:1px solid #375040;color:#a8ed9c}.terminal-header span{color:#94a899;font-size:11px}.terminal-output{flex:1;min-height:0;overflow:auto;padding:15px;white-space:pre-wrap;overflow-wrap:anywhere}.terminal-line{font:inherit;margin:0 0 10px;white-space:pre-wrap}.terminal-line.command,.terminal-line.system{color:#a8ed9c}.terminal-line.error{color:#ff9e99}.terminal-shortcuts{display:flex;gap:6px;overflow:auto;padding:7px 12px;border-top:1px solid #375040}.terminal-shortcuts button,.terminal-compose button,.terminal-form button{border:1px solid #456047;background:#233329;color:#d4efd2;border-radius:7px;padding:5px 9px;white-space:nowrap}.terminal-form{direction:ltr;display:flex;align-items:center;gap:8px;border-top:1px solid #375040;padding:10px 14px}.terminal-form label{color:#a8ed9c;white-space:nowrap}.terminal-input{direction:ltr;text-align:left;flex:1;min-width:0;background:transparent;color:#f0fff0;border:0;outline:none;font:inherit}.terminal-compose{border-top:1px solid #375040;padding:12px;display:grid;gap:8px}.terminal-compose[hidden],.terminal-form[hidden]{display:none}.terminal-note{width:100%;resize:vertical;background:#1c2a21;color:#f0fff0;border:1px solid #456047;border-radius:7px;padding:10px;font:inherit}.terminal-compose>div{display:flex;gap:8px}@media(max-width:650px){.terminal-header{display:block}.terminal-header span{display:block}.terminal-shell{font-size:12px}}`;
|
||
document.head.append(style);
|
||
})();
|