Cookbook PejavaCommander
Практические рецепты для авторов плагинов. Полный справочник — в API.
Рабочие версии нескольких рецептов лежат в examples/plugins.
- Первый плагин
- Команда, клавиша, меню и строка F-клавиш
- Новая колонка (поле)
- Новый режим отображения
- Данные из Node.js (Node-часть и RPC)
- Своя панель (провайдер)
- Цвета, CSS и палитры
- Настройки плагина
- Работа с терминалом
- Диалоги
- Действие над выбранными файлами
- Хранение состояния
- Замена и изменение встроенного поведения
- Упаковка и установка
- Для пользователя: клавиши и палитры без кода
- Свой вид панели
- Details и перетаскивание в своём провайдере
1. Первый плагин
hello/
plugin.json
renderer.js
plugin.json:
{
"id": "me.hello",
"name": "Hello",
"version": "0.1.0",
"renderer": "renderer.js",
"contributes": {
"commands": [{ "command": "me.hello", "title": "Say Hello", "category": "Hello" }]
}
}
renderer.js:
export function activate(pc) {
pc.commands.register('me.hello', () => pc.ui.showMessage('Привет из моего плагина!'));
}
Запуск без установки (в Windows запустите PejavaCommander.exe, в Linux — AppImage, с тем же параметром):
/Applications/PejavaCommander.app/Contents/MacOS/PejavaCommander --plugin-dev=/path/to/hello
Нажмите Cmd/Ctrl+Shift+P, введите «hello» и нажмите Enter. После правок кода нажмите Cmd/Ctrl+Shift+R, чтобы перезагрузить окно.
2. Команда, клавиша, меню и строка F-клавиш
"contributes": {
"commands": [
{ "command": "me.touch", "title": "Create Empty File…", "category": "Files", "keybarTitle": "Touch" }
],
"keybindings": [
{ "key": "shift+f4", "command": "me.touch", "when": "panelFocus && activePanelProvider == 'fs'" }
],
"menus": [
{ "menu": "file", "command": "me.touch", "group": "2" }
]
}
export function activate(pc) {
pc.commands.register('me.touch', async () => {
const name = await pc.ui.showInputBox({ title: 'Create File', prompt: 'Имя файла:' });
if (!name) return;
await pc.terminal.run(`touch ${pc.terminal.quote(name)}`, { showOutput: false });
});
}
- Строка F-клавиш: зажмите Shift в панелях, и над F4 появится «Touch». Строка всегда показывает привязки F1–F10, действующие прямо сейчас при зажатых модификаторах.
when: ограничивайте клавиши условием. СpanelFocusF-клавиши продолжают работать в программах терминала, когда панели скрыты.
3. Новая колонка (поле)
Поле с возрастом файла:
export function activate(pc) {
const DAY = 86_400_000;
pc.panels.registerField('me.age', {
title: 'Age',
width: 5,
align: 'right',
render: (item) => (item.mtime && !item.isParent ? `${Math.floor((Date.now() - item.mtime) / DAY)}d` : ''),
sortValue: (item) => item.mtime ?? 0,
});
}
Добавьте поле в режим (следующий рецепт). По нему можно и сортировать: Ctrl+F12 → Age.
4. Новый режим отображения
Режим — это обычный JSON, код не нужен:
"panelModes": [
{
"id": "me.ages",
"title": "Name + Age (2 колонки)",
"columns": 2,
"providers": ["fs"],
"fields": [{ "field": "name" }, { "field": "me.age" }]
}
],
"keybindings": [
{ "key": "ctrl+7", "command": "panels.setMode", "args": "me.ages", "when": "panelFocus" }
]
columns — сколько перетекающих колонок заполняют элементы. fields — что показывает каждая колонка. Режим сам появится в меню Left и Right и в Ctrl+M.
Колонки фиксированной ширины вместо фиксированного числа: "columns": "auto", "columnWidth": 30 (в символах). columnWidth может быть и ключом числовой настройки — так встроенный режим columns использует panels.columnWidth.
5. Данные из Node.js (Node-часть и RPC)
У UI-части нет доступа к Node. Код для Node кладите в main.js и вызывайте через RPC. Полный пример — examples/plugins/git-status.
plugin.json: "main": "main.js", "renderer": "renderer.js"
main.js:
const fs = require('fs/promises');
exports.activate = (context) => {
// Считаем строки текстового файла.
context.rpc.handle('countLines', async (file) => {
const text = await fs.readFile(file, 'utf8');
return text.split('\n').length;
});
// Отправляем события в UI-часть.
const timer = setInterval(() => context.rpc.emit('tick', Date.now()), 60_000);
context.subscriptions.push({ dispose: () => clearInterval(timer) });
};
renderer.js:
export function activate(pc) {
pc.commands.register('me.lines', async () => {
const item = pc.panels.active.cursorItem;
if (!item || item.isDir) return;
const n = await pc.rpc.call('countLines', item.path);
pc.ui.showMessage(`${item.name}: ${n} строк`);
});
pc.rpc.on('tick', (t) => console.log('tick', t));
}
Асинхронные поля: render() должен вернуть значение сразу. Кэшируйте результат, при промахе кэша запускайте загрузку, а когда данные придут, вызывайте panel.refresh(). Так сделано в примере git.
6. Своя панель (провайдер)
Провайдер даёт панели новое назначение, например показ переменных окружения:
export function activate(pc) {
pc.panels.registerProvider('me.env', {
title: 'Environment',
description: 'Переменные окружения',
defaultMode: 'me.env.list',
async list() {
const out = await pc.rpc.call('env'); // в main.js: return process.env
const items = [{ name: '..', isParent: true, isDir: true }];
for (const [name, value] of Object.entries(out)) items.push({ name, value });
return { location: 'env', title: 'Environment', items };
},
async open(item, { panel }) {
if (item.isParent) {
if (!(await panel.back())) await panel.setProvider('fs');
return;
}
pc.terminal.sendText(`$${item.name}`); // вставить в командную строку
},
parentLocation: () => null, // «вверх» = назад, откуда пришли
statusText: (item) => item.value ?? '',
});
pc.panels.registerField('me.env.value', { title: 'Value', width: '*', render: (i) => i.value ?? '' });
pc.panels.registerMode({
id: 'me.env.list', title: 'Env', columns: 1, providers: ['me.env'],
fields: [{ field: 'name', width: 24 }, { field: 'me.env.value' }],
});
}
Откройте панель через Alt+F1 / Alt+F2 (провайдер есть в списке источников) или из команды: pc.panels.active.setProvider('me.env').
Ещё пример — список избранных директорий в examples/plugins/bookmarks.
7. Цвета, CSS и палитры
Объявите цвета, чтобы пользователь мог менять их в Settings → Colors:
"contributes": {
"colors": [{ "id": "me.warn.foreground", "default": "#ff5555", "description": "Предупреждения в моей колонке" }]
},
"styles": ["styles.css"]
styles.css (id цвета становится CSS-переменной, точки заменяются дефисами):
.me-warn { color: var(--me-warn-foreground); }
Применение в поле: className: (item) => (item.size > 1e9 ? 'me-warn' : undefined).
Чтобы покрасить всю строку, задайте в провайдере item.color = 'me.warn.foreground'.
Плагину с одной палитрой код не нужен. Пример — examples/plugins/solarized-palette:
"contributes": { "palettes": [{ "id": "example-solarized-dark", "label": "Solarized Dark (example)", "path": "solarized-dark.json" }] }
Совет: соберите палитру в Settings → Colors (Duplicate → правка → Export…) и положите в плагин экспортированный файл.
8. Настройки плагина
"contributes": {
"settings": [
{ "key": "me.maxLines", "type": "number", "default": 1000, "description": "Прекратить подсчёт после N строк" },
{ "key": "me.mode", "type": "string", "enum": ["fast", "exact"], "default": "fast", "description": "Режим подсчёта" }
]
}
const max = pc.settings.get('me.maxLines');
pc.settings.onDidChange((keys) => {
if (keys.includes('me.mode')) reconfigure();
});
await pc.settings.update('me.maxLines', 5000); // или null для сброса
9. Работа с терминалом
// Выполнить команду. Панели скроются до её завершения.
await pc.terminal.run(`du -sh ${pc.terminal.quote(item.name)}`);
// Выполнить тихо, не скрывая панели.
await pc.terminal.run('git fetch', { showOutput: false });
// Напечатать в командную строку, не выполняя.
pc.terminal.sendText(`${pc.terminal.quote(item.path)} `);
// Следить за shell.
pc.terminal.onDidChangeCwd((dir) => console.log('shell теперь в', dir));
pc.terminal.onDidPrompt(() => console.log('команда завершилась'));
// cd (отклоняется, пока работает программа).
const res = await pc.terminal.cd('/tmp');
if (!res.ok) pc.ui.showMessage(`cd не выполнен: ${res.reason}`, 'warning');
10. Диалоги
const pick = await pc.ui.showQuickPick(
[{ label: 'Zip', description: '.zip', value: 'zip' }, { label: 'Tar', value: 'tar' }],
{ title: 'Формат архива' },
);
if (!pick) return; // Esc
const name = await pc.ui.showInputBox({
title: 'Архив',
prompt: 'Имя файла:',
value: 'backup.zip',
validate: (v) => (v.trim() ? undefined : 'Обязательно'),
});
const answer = await pc.ui.confirm('Перезаписать файл?', { buttons: ['Overwrite', 'Cancel'], danger: true });
if (answer !== 'Overwrite') return;
pc.ui.showMessage('Готово'); // info
pc.ui.showMessage('Осторожно', 'warning');
pc.ui.showMessage('Ошибка', 'error');
11. Действие над выбранными файлами
targetItems возвращает отмеченные элементы или элемент под курсором, если ничего не отмечено:
pc.commands.register('me.zip', async () => {
const panel = pc.panels.active;
if (panel.providerId !== 'fs') return;
const names = panel.targetItems.map((i) => pc.terminal.quote(i.name)).join(' ');
if (!names) return;
await pc.terminal.run(`zip -r archive.zip ${names}`);
await panel.refresh();
panel.clearSelection();
});
Клавиши выделения: Insert / Ctrl+T / Shift+↑↓ — отметить, Num+ / Num− — по маске, Num* — инвертировать, Cmd/Ctrl+A — всё.
12. Хранение состояния
export async function activate(pc, context) {
const count = (await context.state.get('launches', 0)) + 1;
await context.state.update('launches', count);
}
Для больших данных используйте Node-часть и context.storagePath.
13. Замена и изменение встроенного поведения
- Переназначить клавиши (без кода): Settings → Keyboard. Изменения сохраняются в
keybindings.json. - Изменить действие клавиши в определённом контексте: объявите привязку с более конкретным
when. Она важнее встроенной:{ "key": "f3", "command": "me.preview", "when": "panelFocus && activePanelProvider == 'fs' && !cursorIsDirectory" } - Заменить встроенный плагин: установите плагин с тем же
id, напримерcore.fileops. Он заменит встроенную копию; в менеджере источник будет показан как «installed*». - Выключить встроенную функцию: отключите плагин в менеджере (F2), например
core.fileopsилиcore.palettes.core.workbenchиcore.panelsотключить нельзя. - Изменить действие
Enterв файловых панелях: зарегистрируйте свой провайдер с idfsв плагине, который заменяетcore.filesystem. Или привяжитеenterк своей команде с более конкретнымwhen.
14. Упаковка и установка
cd my-plugin && zip -r ../my-plugin.zip . # plugin.json в корне архива
Откройте менеджер плагинов (Cmd/Ctrl+Shift+X или Options → Plugin Manager), нажмите F5 Install и выберите:
- From folder… — копирует папку в
<userData>/plugins/<id>; - From .zip package…;
- From URL… — скачивает
.zip. Этим будет пользоваться будущий сервер плагинов.
В менеджере: F2 — включить/выключить, F8 — удалить, Enter — подробности, F4 — показать папку, Esc/F10 — закрыть менеджер.
15. Для пользователя: клавиши и палитры без кода
- F9 открывает палитру команд: Settings → Keyboard, найдите «Command Palette», нажмите +, затем F9.
- Убрать сочетание: нажмите ✕ на его плашке. Для встроенной клавиши сохранится запись
{"key": "...", "command": "-id"}. - Поделиться клавишами: Settings → Keyboard → Export… / Import….
- Свои цвета: Settings → Colors → выберите палитру → Duplicate → правка (главное окно обновляется сразу) → Activate. Поделиться — через Export… / Import….
- Быстро сменить палитру: Options → Select Color Palette….
16. Свой вид панели
Вид определяет, как рисуется панель. Список, сетка иконок и превью — всё это виды. Этот пример показывает каждый элемент полосой, пропорциональной размеру:
export function activate(pc) {
pc.panels.registerView('me.sizebars', {
title: 'Size bars',
create(container, ctx) {
const root = document.createElement('div');
root.style.cssText = 'flex:1; overflow:hidden; padding:0 1ch';
container.append(root);
root.addEventListener('mousedown', (e) => {
const row = e.target.closest('[data-index]');
if (row) ctx.panel.click(Number(row.dataset.index), e); // выделение через Shift/Cmd работает само
});
let top = 0;
const rows = () => Math.max(1, Math.floor(root.clientHeight / ctx.metrics.rowHeight()));
return {
get pageSize() { return rows(); },
step: (dir) => ({ up: -1, down: 1, pageUp: -rows(), pageDown: rows() })[dir] ?? 0,
render() {
const { items, cursorIndex } = ctx.panel;
const n = rows();
if (cursorIndex < top) top = cursorIndex;
if (cursorIndex >= top + n) top = cursorIndex - n + 1;
const max = Math.max(1, ...items.map((i) => i.size || 0));
root.replaceChildren(...items.slice(top, top + n).map((item, k) => {
const row = document.createElement('div');
row.dataset.index = top + k;
row.className = `pc-row${top + k === cursorIndex ? ' is-cursor' : ''}${ctx.panel.isSelected(item) ? ' is-selected' : ''}`;
const pct = Math.round(((item.size || 0) / max) * 100);
row.style.background = top + k === cursorIndex ? '' :
`linear-gradient(90deg, color-mix(in srgb, var(--panel-cursor-background) 40%, transparent) ${pct}%, transparent ${pct}%)`;
row.textContent = item.name;
return row;
}));
},
dispose: () => root.remove(),
};
},
});
pc.panels.registerMode({ id: 'me.sizebars', title: 'Size bars', view: 'me.sizebars', providers: ['fs'] });
}
Включается через Ctrl+M или привязкой клавиши к panels.setMode с аргументом "me.sizebars".
Советы:
- Используйте классы ядра (
pc-row,is-cursor,is-selected), тогда палитры применятся автоматически. ctx.mode.optionsпередаёт произвольные параметры из описания режима.- Для содержимого файлов:
pc.fs.fileUrl(path)(img/video/audio),pc.fs.readText(path)иpc.fs.thumbnail(path, size). - Вид, который следит за другой панелью (как превью), подписывается через
pc.panels.getPanel(otherSide).onDidChange(...)и обязан отписаться вdispose().
17. Details и перетаскивание в своём провайдере
pc.panels.registerProvider('me.notes', {
title: 'Notes',
async list() { /* … элементы { name, path, size, mtime } … */ },
// Блок details под панелью в две колонки (F9 / Ctrl+I делают его компактным: только путь).
details(item) {
return [
['Note', item.name, { wide: true }],
['Words', String(item.words)], ['Updated', new Date(item.mtime).toLocaleString()],
];
},
// Заметки можно перетащить в файловую панель…
canDrag: (item) => Boolean(item.path),
// …а файлы — бросить сюда, чтобы импортировать.
dropEffect: (drop) => (drop.source?.providerId === 'me.notes' ? 'none' : 'copy'),
async acceptDrop(drop, { panel }) {
await pc.rpc.call('import', drop.paths);
await panel.refresh();
},
});
Панель находит перетаскиваемые элементы по элементам вида с data-index, поэтому это работает в любом виде: в списке, в иконках и в вашем собственном. Файловая панель, принявшая перетаскивание, вызывает fileops.copyTo / moveTo с path ваших элементов.