Files

299 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 &gt;</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);
})();