API плагинов PejavaCommander

Здесь описано, как устроен PejavaCommander, и приведён полный API для плагинов. Пошаговые примеры собраны в Cookbook.


1. Архитектура

┌──────────────────────────── Main-процесс (Node) ─────────────────────────────┐
│ PluginHost   SettingsService   ThemeService   KeybindingService   PtyService │
│ (поиск,      (settings.json)   (палитры)      (keybindings.json)  (shell)    │
│  Node-части)                                                                  │
└──────────────▲──────────────────────────────▲─────────────────────────────────┘
               │ IPC (preload: window.pcBridge)│
┌──────────────┴──── Главное окно (UI) ────────┴────────┐  ┌── Окно настроек ────┐
│ Workbench: команды, хоткеи, контекстные ключи, меню,   │  │ General / Colors /  │
│ тема, диалоги, реестры (провайдеры/поля/режимы)        │  │ Keyboard            │
│ Слой 1: TerminalLayer (xterm.js + shell)               │  └─────────────────────┘
│ Слой 2: PanelsLayer (1–2 панели + строка F-клавиш)     │
│ PluginLoader → activate(pc, context) UI-частей         │
└────────────────────────────────────────────────────────┘

Два слоя. Слой 1 — ваш shell ($SHELL) в настоящем PTY. Его отрисовывает xterm.js с полной поддержкой цветов. Слой 2 — панели. Они закрывают всё окно, кроме последней строки: там видно приглашение shell. Ctrl+O показывает и скрывает панели.

Рабочую директорию задаёт shell. Интеграция с shell сообщает текущую директорию при каждом приглашении, и активная файловая панель переходит туда же. Когда пользователь переходит в другую директорию в панели, приложение отправляет в shell cd, но только если shell стоит на приглашении.

Всё сделано плагинами. Файловые панели, режимы отображения, файловые операции, палитры, менеджер плагинов и даже команды приложения — встроенные плагины из src/plugins/. Ваши плагины пользуются тем же API. Пользовательский плагин с тем же id заменяет встроенный.

Декларации и код. Статичные вещи плагин объявляет в plugin.json: команды, клавиши, меню, цвета, палитры, настройки, режимы. Динамичные регистрирует в коде: обработчики команд, провайдеры панелей, поля.

Без перезапуска. Включение, выключение, установка и удаление плагина применяются сразу. Всё, что плагин зарегистрировал через API, снимается автоматически.

Безопасность. Как и расширения VS Code, плагины считаются доверенным кодом: у Node-части полный доступ к Node.js. Ставьте плагины только из источников, которым доверяете.

2. Устройство плагина

my-plugin/
  plugin.json      манифест (обязателен)
  renderer.js      UI-часть, ES-модуль (необязательно)
  main.js          Node-часть, CommonJS (необязательно)
  styles.css       дополнительный CSS (перечисляется в "styles")
  palettes/…       файлы палитр

UI-часть (renderer.js) работает в главном окне:

export function activate(pc, context) {
  pc.commands.register('my.hello', () => pc.ui.showMessage('Привет!'));
}
export function deactivate() {} // необязательно: регистрации снимаются автоматически

Node-часть (main.js) работает в main-процессе Electron:

exports.activate = (context) => {
  context.rpc.handle('sum', (a, b) => a + b); // вызов из UI: pc.rpc.call('sum', 1, 2)
};
exports.deactivate = () => {};

Жизненный цикл

  1. При запуске приложение ищет плагины в папке встроенных, в пользовательской папке и в папках --plugin-dev.
  2. Активирует Node-части включённых плагинов.
  3. Главное окно применяет декларативные вклады плагина, затем импортирует модуль renderer и вызывает activate(pc, context). Встроенные плагины идут первыми.
  4. При отключении или удалении приложение вызывает deactivate() и снимает всё, что плагин зарегистрировал.

3. Манифест (plugin.json)

Поле Тип Описание
id string, обязательно Уникальный id, [a-z0-9][a-z0-9._-]*. Используйте префикс: yourname.feature.
name string, обязательно Отображаемое имя.
version string, обязательно Semver, например 1.2.0.
description string Показывается в менеджере плагинов.
author string
renderer string Путь к UI-модулю (ES-модуль).
main string Путь к Node-модулю (CommonJS).
styles string[] CSS-файлы для главного окна.
required boolean Только для встроенных: плагин нельзя отключить.
contributes object Точки расширения, см. ниже.

contributes.commands

{ "command": "my.cmd", "title": "Сделать что-то", "category": "My", "keybarTitle": "Делай", "enablement": "activePanelHasTarget", "description": "…" }

Задаёт заголовки для палитры команд, меню, строки F-клавиш и редактора клавиш. Обработчик регистрируется в коде через pc.commands.register. enablement — when-условие: пока оно ложно, клавиша команды ничего не делает, а подпись в строке F-клавиш приглушена (для случая «клавиша здесь есть, но сейчас не над чем работать»; если команда в этой панели вообще неуместна, используйте when привязки).

contributes.keybindings

{ "key": "ctrl+shift+d", "mac": "meta+shift+d", "command": "my.cmd", "when": "panelFocus", "args": ["x"], "keybarTitle": "Дубль" }
  • key — клавиша для всех платформ. mac, linux и win переопределяют её для своей платформы.
  • args передаётся обработчику команды (одно значение или массив).
  • keybarTitle — подпись именно этой привязки в строке F-клавиш (важнее подписи команды).
  • Синтаксис клавиш и when описан в разделе 7.

contributes.menubar / contributes.menus

"menubar": [{ "id": "tools", "label": "Tools", "order": 45 }],
"menus": [
  { "menu": "tools", "command": "my.cmd", "group": "1", "order": 10 },
  { "menu": "tools", "command": "panels.setMode", "args": ["left", "columns3"], "label": "Columns 3", "checked": "leftPanelMode == 'columns3'" },
  { "menu": "tools", "submenu": "tools.more", "label": "Ещё" },
  { "menu": "tools.more", "command": "my.other", "when": "activePanelProvider == 'fs'" }
]

Встроенные верхние меню: left (10), file (20), command (30), options (40), right (50). Пункты сортируются по group, затем по order, между группами ставится разделитель. when скрывает пункт, checked ставит галочку; оба — выражения над контекстными ключами. Сочетание клавиш рядом с пунктом берётся из хоткеев.

contributes.colors

{ "id": "myplugin.badge.foreground", "default": "#ffcc00", "description": "Текст бейджа" }

Превращается в CSS-переменную --myplugin-badge-foreground и редактируется в Settings → Colors.

contributes.palettes

{ "id": "my-theme", "label": "My Theme", "path": "palettes/my-theme.json" }

Формат файла палитры описан в разделе 8.

contributes.settings

{ "key": "myplugin.limit", "type": "number", "default": 100, "description": "Максимум элементов", "enum": null, "hidden": false }

type: boolean | number | string. С enum: [...] настройка показывается выпадающим списком. Настройки появляются в Settings → General.

contributes.panelModes

{ "id": "wide", "title": "Wide", "columns": 2, "order": 50, "providers": ["fs"],
  "fields": [{ "field": "name" }, { "field": "size", "width": 8, "align": "right", "title": "Байт" }] }

"details" решает, показывают ли панели в этом режиме блок details: true (по умолчанию), false или ключ булевой настройки (тогда режим следует этой настройке, и команда или hotkey, переключающие её, показывают и скрывают блок). Подробнее — в разделе 6.

4. API UI-части (pc)

activate(pc, context) получает:

context: { id, manifest, baseUrl, asUrl(relPath), subscriptions: { push(...disposables) }, state }

  • context.state.get(key, fallback) и context.state.update(key, value) — постоянное хранилище плагина (асинхронное).

Каждая функция register…/on… возвращает disposable ({ dispose() }). Освобождать их вручную не нужно: они снимаются при остановке плагина.

pc.commands

register(id, handler, meta?) Регистрирует обработчик. meta: { title, category, keybarTitle } для команд, которых нет в манифесте.
execute(id, ...args) Выполняет команду и возвращает Promise с её результатом.
list() [{ id, title, category, keybarTitle, available }]
getKeys(id, args?) Клавиши, привязанные к команде, например ['f5']; с args — только привязки с такими аргументами (getKeys('panels.setMode', ['column'])).

pc.keybindings

register({ key, command, when?, args?, mac?, keybarTitle? }) Добавляет привязку из кода.
format(key) Форма для показа: 'shift+meta+p' → ⇧⌘P на macOS.

pc.context

set(key, value) и get(key) читают и пишут контекстные ключи для условий when. Можно заводить свои ключи, например myplugin.busy.

pc.menus

registerMenu({ id, label, order }) и registerItem({ menu, command, … }) — в тех же форматах, что и в манифесте.

pc.panels

registerProvider(id, provider) Источник панели (§6).
registerField(id, field) Поле колонки (§6).
registerMode(mode) Режим отображения (формат как в манифесте).
registerView(id, view) Вид панели: свой способ отрисовки (§6).
getField(id) Описание зарегистрированного поля.
providers, fields, modes, views Что зарегистрировано.
active, passive, left, right, getPanel(side) Дескрипторы панелей (ниже).
activeSide 'left' или 'right'.
visible, isDual Виден ли слой? Показаны ли две панели?
show(), hide(), toggle() Показать или скрыть слой панелей.
activate(side), switch(), swap() Сменить активную панель или поменять панели местами.
resizable, toggleResizable() Режим изменения ширины: разделитель можно тянуть; более узкая панель сохраняет ширину в пикселях (50/50, пока окно для неё слишком узкое). Переключение сбрасывает на 50/50 и возвращает новый режим.
togglePanel(side), setDual(bool) Одна панель или две.
onDidChangeLocation(fn) fn({ panel, location }) для любой панели.
onDidChangeProviders(fn), onDidChangeModes(fn) Изменения реестров.

Дескриптор панели

Свойство
side, isActive
providerId, location Что показано и где.
modeId, mode (нормализованный объект режима), availableModes, defaultModeId, sort ({ field, reverse })
title, error Заголовок панели, последняя ошибка чтения.
itemsVersion Растёт при каждой перезагрузке или пересортировке элементов (виды по нему решают, когда перестроиться).
locationInfo Объект info, который провайдер вернул вместе со списком (fs: { dev }).
items, cursorItem, cursorIndex
selectedItems Отмеченные элементы.
targetItems С чем работают операции: выбор вида (см. хуки видов), иначе отмеченные элементы, иначе элемент под курсором.
currentItem С чем работают F3/F4: выбор вида, иначе элемент под курсором.
view { id, sortable, itemOps } текущего вида.
rowsPerColumn, pageSize Текущая геометрия.
Метод
navigate(location, { focus? }) Тот же провайдер, новое место. focus — имя элемента, на который встанет курсор.
setProvider(id, location?) Сменить источник. back() возвращает к предыдущему.
refresh(), openItem(item?), goParent()
setMode(id), resetMode(), setSort(field, reverse?) Режим, не подходящий провайдеру, заменяется режимом по умолчанию.
setCursor(i), moveCursor(delta), setCursorByName(name)
moveDirection(dir) 'up' 'down' 'left' 'right' 'pageUp' 'pageDown'; шаг определяет вид.
click(index, { shiftKey, metaKey, ctrlKey }) Поведение мыши: клик — курсор, Cmd/Ctrl — переключить отметку, Shift — отметить диапазон.
isSelected(item)
setSelection(names, { cursorName? }) Заменить выделение (так работает выделение рамкой).
toggleSelect(item?, value?), select(names, value), selectWhere(pred, value), invertSelection(), clearSelection()
expand(item), collapse(item), toggleExpand(item), isExpanded(item) Раскрытие папок на месте в виде с tree: true. Их элементы идут в items сразу после папки, с именами "<папка>/<имя>", полями depth и treeParent; место папки — её item.path. При перечитывании того же места папки остаются раскрытыми.
onDidChangeLocation(fn), onDidChange(fn)

pc.terminal

cwd, onDidChangeCwd(fn) Рабочая директория shell.
onDidPrompt(fn) Срабатывает на каждом новом приглашении (команда завершилась).
cd(dir) cd в shell. Возвращает { ok, reason? }; reason: 'busy' значит, что запущена программа.
run(command, { showOutput = true }) Выполнить командную строку. Панели скрываются до следующего приглашения.
sendText(text) Напечатать в командную строку (без Enter). '\r' выполняет строку.
clearCommandLine()
write(data) Сырые байты в PTY.
quote(str) Безопасное экранирование для shell.
focus(), isIdle()

pc.fs

Асинхронные: list(dir) → { path, parent, entries[] }; stat(path); isDirectory(path); roots() → [{ label, path, kind: 'home' \| 'root' \| 'volume', total?, free? }] (размеры — если том ответил за 400 мс); mkdir(path); trash(paths[]); rename(from, to); openPath(path) (приложение по умолчанию); reveal(path). dirSize(paths) → { bytes, files, dirs, errors } (рекурсивно, ссылки не раскрываются); readText(path, maxBytes = 256K) → { text, truncated, binary, size }; thumbnail(path, size, mtime?) → data URL или null (системные миниатюры: картинки, видео, PDF на macOS; с кэшем и очередью); cachedThumbnail(path, size, mtime?) (синхронно, undefined, если ещё не загружено). Синхронные: fileUrl(path) → URL pc://local/… для <img>/<video>/<audio> (с перемоткой); info() → { home, sep, platform, uid, user }; работа с путями — join, dirname, basename, extname, normalize, resolve(base, input).

Элемент: { name, path, isDir, isLink, linkBroken?, linkTarget?, size, mtime, ctime, birthtime, atime, mode, uid, gid, isExecutable, error? }.

pc.ui

showQuickPick(items, { title, placeholder, activeIndex, panel?, anchor? }) Элементы — строки или { label, description?, detail?, value? }; { separator: true, label } начинает группу с заголовком; фильтр ищет и по заголовку группы и оставляет его над найденными. Возвращает выбранный элемент или undefined.
showForm({ title, fields, submitLabel, validate, onChange, actions }) Диалог с несколькими полями. Поле: { id, label, type: 'text' | 'password' | 'number' | 'select' | 'checkbox' | 'separator', value, placeholder, hint, options: [{ label, value }], visible(values), autofocus, button: { label, run(values) } }. validate(values) возвращает текст ошибки; onChange(values, id) может вернуть { id: value }, чтобы поменять другие поля; actions: [{ label, run(values, { setMessage }) }] — дополнительные кнопки, не закрывающие диалог (например, «Проверить»). Возвращает { id: value } или undefined.
showInputBox({ title, prompt, value, placeholder, validate, password }) validate(v) возвращает текст ошибки или ничего; password: true скрывает ввод. Возвращает строку или undefined.
confirm(message, { title, buttons = ['Yes','No'], danger }) Возвращает подпись нажатой кнопки или undefined.
размещение Пока панели показаны, диалоги открываются по центру активной панели. panel: 'left' | 'right' | дескриптор — по центру указанной панели, anchor: 'window' — по центру всего окна (для общих окон приложения). Работает для всех трёх видов диалогов.
showMessage(text, level = 'info') Уведомление: 'info', 'warning' или 'error'.
fileKind(item), fileExt(name), hasThumbnail(item), canShowInBrowser(name) Типы файлов: folder, image, video, audio, archive, pdf, document, source, text, executable, other; canShowInBrowser → 'image' | 'video' | 'audio' | null.
fileIconSvg(item), renderFileIcon(host, item) Иконки форматов (с учётом item.icon).
showOpenDialog(options), showSaveDialog(options) Системные диалоги (опции Electron).

pc.settings

get(key, fallback), update(key, value) (null сбрасывает значение), onDidChange(fn(keys[])).

pc.theme

getColor(id), palette ({ id, name, type }), cssVar(id), listPalettes(), setPalette(id), onDidChange(fn).

pc.plugins

list(), errors(), setEnabled(id, bool), installFromFolder(dir?), installFromZip(file?), installFromUrl(url), uninstall(id), onDidChange(fn). Функции установки без пути открывают диалог выбора файла.

pc.rpc

call(method, ...args) вызывает Node-часть этого плагина. on(event, fn) принимает события, которые она отправляет.

pc.app

openSettings(tab?) ('general' | 'colors' | 'keys'), reload(), quit(), info(), toggleDevTools().

pc.tabs

Каждый таб окна — отдельная страница со своим терминалом, панелями и экземплярами плагинов (Node-части плагинов общие). Вызовы без id относятся к табу самого плагина: new(), newWindow(), close(id?) (спрашивает, если идёт программа), next(), previous(), selectIndex(i) (8 и больше — последний), rename(name, id?) (пустое имя — автоматический заголовок), info() → { id, title, customName, count }, moveToNewWindow(id?). Автоматический заголовок — document.title страницы: папка шелла или «папка — программа», пока идёт команда (заголовок, выставленный самой программой, важнее). Команды: tabs.new ⌘T, tabs.newWindow ⌘N, tabs.close ⌘W, tabs.next ⌘⇧] / ⌃Tab, tabs.previous ⌘⇧[ / ⌃⇧Tab, tabs.select ⌘1…⌘9, tabs.rename, tabs.moveToNewWindow. Цвета полосы табов: tabs.*.

5. API Node-части

exports.activate = (context) => { … };
context.
id, path, manifest id плагина, абсолютный путь к папке, разобранный манифест.
storagePath Папка для ваших данных (<userData>/plugin-data/<id>); создайте её сами.
rpc.handle(method, fn) Открывает fn для pc.rpc.call(method, …). Может быть async; результат должен сериализоваться. Ошибки приходят вызывающей стороне как отклонённый Promise.
rpc.emit(event, payload) Отправляет событие в UI-часть (pc.rpc.on(event, fn)).
subscriptions Сюда кладут объекты { dispose() }, их освободят при деактивации.
log(...args) Вывод в консоль с id плагина.

6. Панели: провайдеры, поля, режимы

Панель показывает элементы от провайдера в некотором месте (location) и раскладывает их по режиму. Режим выбирает вид (по умолчанию list), а колонки списка показывают поля.

Провайдер (назначение панели)

pc.panels.registerProvider('my', {
  title: 'My Source',                  // для выбора источника и заголовок по умолчанию
  description: 'видно в списке Alt+F1',
  hidden: false,                       // true — не показывать в списке источников
  defaultMode: 'my.mode',
  syncWithTerminal: false,             // true только для локальных директорий (провайдер fs)
  sortItems: true,
  modesOf: 'fs',                       // необязательно: брать и режимы этого провайдера (элементы похожи на файлы)                     // false — оставить порядок из list()
  getDefaultLocation(ctx) { return 'root'; },
  // Необязательно: пункты для окна Select Source (Alt+F1/F2) под своими заголовками групп.
  // Пункт открывает `location` в этом провайдере (или в `providerId`) либо выполняет
  // `command` с `args` (по умолчанию — сторона панели). Отвечайте быстро: через 1,5 с окно откроется без них.
  async sources(ctx) { return [{ separator: true, label: 'Моё' }, { label: 'Корень', description: '/', location: 'root', current: false }]; },
  // Необязательно: кликабельные части пути в шапке панели. Без этого показан
  // весь заголовок, а клик по нему открывает выбор источника.
  breadcrumbs(location, ctx) { return [{ label: 'root', location: 'root' }, { label: 'sub', location: 'root/sub' }]; },

  async list(location, ctx) {          // обязательно
    return {
      location,                        // нормализованное место (необязательно)
      title: `My: ${location}`,        // заголовок панели (необязательно)
      items: [
        { name: '..', isParent: true, isDir: true },
        { name: 'a', label: 'Item A', isDir: false, size: 10, color: 'panel.executable.foreground' },
      ],
    };
  },

  async open(item, ctx) {              // Enter / двойной клик
    if (item.isDir) return { location: item.name, focus: undefined }; // перейти
    // либо сделать что угодно и вернуть undefined
  },
  parentLocation(location, ctx) { return location === 'root' ? null : 'root'; }, // null → panel.back()
  childName(location) { return location; }, // куда встанет курсор после подъёма
  statusText(item, ctx) { return item.name; },

  // Необязательно: строки блока details — [label, value, { wide }?]
  details(item, ctx) { return [['Path', item.path, { wide: true }], ['Size', '10 B']]; },
  // Необязательно: дополнительные строки, пока что-то отмечено (после строки «Selected»).
  selectionDetails(items, ctx) { return [['Size', 'show ⌘I', { command: 'my.calc', action: 'show ⌘I' }]]; },

  // Необязательно: перетаскивание
  canDrag(item) { return true; },                        // элементы можно перетаскивать из панели
  dropEffect(drop, ctx) { return 'copy'; },              // 'copy' | 'move' | 'none'
  async acceptDrop(drop, ctx) { /* drop.paths → drop.target.location */ },
});

Файловые операции в других провайдерах. С fileOps: true клавиши F3/F5/F6/F7/F8 (core.fileops) работают и здесь, через хуки провайдера: exportItems(items, ctx) → { paths, cleanup } (локальные копии для чтения), importPaths(paths, location, ctx) (положить файлы внутрь), removeItems(items, ctx) (удалить насовсем, без Корзины), makeDirectory(location, name, ctx), readOnly(location) → false или причина (тогда F6/F7/F8 приглушены), hostPath(location) (где лежит, место по умолчанию для F5 наружу). Режим с "providers": ["fs"] предлагается и провайдерам с modesOf: 'fs', если в нём нет "strictProviders": true (так помечен Visual: он сканирует настоящие папки). Часть пути в breadcrumbs с полем provider переключает панель на этот провайдер.

Открыватели. pc.panels.registerOpener(id, { match(item, ctx), open(item, panel) }): Enter на файле в файловой панели сначала спрашивает открыватели, и только потом запускает файл или открывает его приложением. PejavaArchive так открывает архивы как папки. pc.panels.findOpener(item, ctx) возвращает первый подходящий.

Превьюеры. pc.panels.registerPreviewer(id, { match(item, ctx), async create(host, item, ctx) }): вид Preview (Ctrl+8, Quick View Ctrl+Q) сначала спрашивает превьюеры, а уже потом показывает элемент по-своему. ctx — { panel, source, providerId, addMeta(label, value), isStale() } (source — панель, чей элемент показан). create заполняет host и возвращает экземпляр или null, если элемент всё-таки не его (тогда Preview покажет его как обычно). Хуки экземпляра, все необязательные: dispose(); focus() / hasFocus() (активная панель Preview передаёт ему фокус); accepts(item, providerId) + update(item) (тот же файл изменился: оставить экземпляр, а не создавать новый). PejavaCodeEditor — превьюер.

Правка файла из другого плагина. await pc.commands.execute('codeEditor.open', localPath, { title, afterSave, onClose }) открывает локальный файл в редакторе кода на всё окно и возвращает false, если это не текст (бинарный, слишком большой). afterSave() вызывается после каждого сохранения; если он бросает ошибку, текст остаётся несохранённым (ошибка с cancelled: true не показывается). Так PejavaRemote правит файлы на сервере: скачанная копия, afterSave загружает её обратно, onClose удаляет.

Элементы, забирающие клавиши. Пока фокус внутри элемента с атрибутом data-pc-own-keys (редактор кода), клавиши панелей не срабатывают и набор не уходит в командную строку; клавиши без when (⌘T, ⌘Q, ⌘⇧P…) работают. Контекстный ключ editorFocus тогда истинен, а panelFocus ложен.

PejavaArchive (pejava.archive) — полный пример всего этого: провайдер archive, место "<путь к архиву>::<папка внутри>". Читает всё, что знает bsdtar (libarchive): zip и родственные (jar, apk, epub, whl…), tar с gz/bz2/xz/zst/lzma, 7z, rar, iso, cab, xar/pkg, cpio, ar/deb, rpm, а также одиночные .gz/.bz2/.xz/.zst. zip меняется на месте через Info-ZIP zip, tar.* и 7z пересобираются bsdtar, остальные форматы и архивы внутри архивов — только для чтения. Зашифрованные архивы спрашивают пароль.

PejavaRemote (pejava.remote) добавляет сетевые подключения: провайдер remote (список подключений) и remote.files (место "<id подключения>::<путь на сервере>"), а также пункты sources() в окне выбора источника. SFTP (с учётом ~/.ssh/config: HostName, User, Port, IdentityFile, ProxyJump, ProxyCommand; ключи хостов сверяются с ~/.ssh/known_hosts), FTP, FTPS (явный и неявный TLS) и WebDAV (Basic и Digest) открываются прямо в панели, работают F3–F8 и Shift+F6; F4 правит копию и закачивает её обратно, когда редактор закрыт. Ресурсы SMB, AFP и NFS монтирует система (macOS mount volume, Linux GVfs gio mount), и они открываются в обычной файловой панели. Сохранённые подключения лежат в ~/.pejava/connections.json (права 0600; пароли шифруются через Electron safeStorage, пока включена настройка remote.encryptPasswords и доступна связка ключей). Также предлагаются хосты из ~/.ssh/config, менеджера сайтов FileZilla, ~/.netrc и Bonjour/mDNS в локальной сети.

Перетаскивание. Виды помечают элементы через data-index и draggable, остальное делает панель. Перетаскиваются отмеченные элементы или тот, который тянут. drop — это { source: { side, providerId, location, items } | null, external, target: { location, item, info }, modifiers: { alt, meta, ctrl, shift }, effect, paths }. external означает, что файлы пришли из другого приложения (например, Finder). Без acceptDrop панель не принимает перетаскивание. Провайдер fs работает по правилам Finder: тот же диск — перенос, другой диск — копирование, Option — копирование, Cmd — перенос. Если под указателем папка, файлы попадут в неё, иначе — в текущее место панели. Провайдер вызывает команды fileops.moveTo / fileops.copyTo ((paths, destDir), спрашивают только о конфликтах).

Блок details. Под каждой панелью во всех режимах, если режим не указал "details": false (см. contributes.panelModes) и вид его не скрыл, — информация о текущем элементе в две колонки из provider.details(item) и маленькое превью справа (настройка panels.detailsPreview, Ctrl+Alt+I). Если что-то отмечено, первой идёт строка «Selected». Без details() блок показывает имя и statusText(). Когда блок виден, он заменяет строку статуса. Кнопка [▼] на его верхней рамке (F9, Ctrl+I, команда panels.toggleDetailsCompact) делает блок компактным: только строка Path; [▲] возвращает полный вид. Каждая панель помнит свой выбор. Значение Path (и любая строка с { copy: true }) по клику копируется в буфер. Строка с { command, args?, action? } — кнопка: клик по значению (или только по его части action) запускает команду. fs так показывает Size: show ⌘I: подсчёт размера папки (или всего отмеченного; команда fs.calcSize, ⌘I / Ctrl+Alt+L), результат заменяет и <DIR> в колонке Size. Изменив данные на месте, вызовите redraw() у панели. У панели: hasDetails, detailsCompact, setDetailsCompact(bool); контекстные ключи activePanelHasDetails, activePanelDetailsCompact.

ctx — это { panel, api, settings }, где panel — дескриптор запрашивающей панели.

Поля элемента

name Уникален внутри списка, по нему работает выделение.
label Поле name показывает его вместо name.
title Человеческое имя для видов, отличных от списка (подписи иконок, превью).
icon SVG-разметка или URL картинки для вида «Иконки» и превью.
description Показывается как «Info» в превью для элементов, которые не являются файлами.
isDir, isParent
color id цвета для всей строки.
classes Дополнительные CSS-классы строки.
selectable: false Элемент нельзя отметить.
любые другие Доступны вашим полям.

Поле (содержимое колонки)

pc.panels.registerField('age', {
  title: 'Age', width: 5, align: 'right',   // ширина в символах или '*' = всё оставшееся место
  render(item, ctx) { return '3d'; },       // текст ячейки
  sortValue(item) { return item.mtime; },   // позволяет сортировать по полю
  className(item) { return 'is-old'; },     // CSS-класс ячейки (необязательно)
  format(bytes) { … },                      // поле 'size' использует его для суммы выделения
});

Встроенные поля: name (core.panels); size, mtime, perms, owner, ext (core.filesystem); plugin.* (менеджер плагинов).

Режим (вид панели)

columns — сколько перетекающих колонок заполняют элементы: сверху вниз, затем следующая колонка, как в режиме Columns 3. fields — ячейки в каждой такой колонке.

Режим columns fields
column 1 name; метаданные: short — size, mtime; long — ещё perms, owner
columns2 2 name; метаданные: short — size; long — ещё mtime
columns3 3 name; метаданные как у columns2
columns "auto" name; столько колонок шириной panels.columnWidth символов (по умолчанию 30), сколько влезает
visual вид treemap treemap занятого места (плагин visual.disk-usage)
outline вид outline список как в Finder: папки раскрываются на месте (клик по шеврону или →/←), вложенные строки с отступом, остальные колонки выровнены
icons вид icons сетка как в Finder, миниатюры картинок/видео/PDF
preview вид preview показывает текущий элемент другой панели (Quick View, Ctrl+Q)

Свойства режима: id, title, order, columns (число или "auto"), columnWidth (символы или ключ числовой настройки), fields, meta (уровень метаданных по умолчанию: "off", "short" (по умолчанию) или "long"), header (по умолчанию true), providers, view (по умолчанию "list"), options (произвольные параметры для вида).

Метаданные. Элемент fields с "meta": "short" или "long" необязательный: панель показывает его на этом уровне и выше (None → Short → Long). Уровень запоминается для каждой панели и каждого режима; пользователь выбирает его в окне Listing Mode, в меню Left/Right или клавишей Ctrl+4 (panels.cycleMeta, panels.setMeta(level)). У поля метаданных могут быть свои providers (column показывает размеры и даты только для fs и провайдеров с modesOf: 'fs'). У панели: metaLevel (null, если у режима нет метаданных), metaFields, setMeta(level), cycleMeta(). Старые id режимов работают: brief → columns3 без метаданных, medium → columns2, full → column short, long → column long, full2 → columns2 short.

Элемент fields может переопределить width, align и title. providers: [...] ограничивает режим отдельными провайдерами. Если запросить неподходящий режим (например, Ctrl+5 (Outline) в менеджере плагинов), панель покажет режим провайдера по умолчанию; Ctrl+0 сбрасывает режим к нему. Режим запоминается для каждой панели и каждого провайдера.

Вид (как рисуется панель)

pc.panels.registerView('my.view', {
  title: 'My view',
  create(container, ctx) {
    // ctx: { panel, api, settings, mode, fieldDef(id), metrics: { rowHeight(), charWidth() } }
    const root = document.createElement('div');
    container.append(root);
    return {
      render() { /* рисуем ctx.panel.items, ctx.panel.cursorIndex, ctx.panel.isSelected(item) */ },
      step(direction) { return { up: -1, down: 1 }[direction] ?? 0; }, // сдвиг курсора стрелками
      pageSize: 20,                    // необязательно
      statusText() { return '…'; },    // необязательно: заменяет строку статуса
      dispose() { root.remove(); },
    };
  },
});

render() вызывается после каждого изменения: курсор, выделение, элементы, размер, активность. При смене режима панели экземпляр вида создаётся заново.

Описание вида с tree: true разрешает панели раскрывать папки на месте (expand/collapse у панели); таков вид outline.

Хуки блока details: в описании вида details: false скрывает блок, detailsPreview: false скрывает его мини-превью. У экземпляра: details / detailsPreview (то же, но динамически); detailsTarget() → { item, providerId, panel, thumbnail? }, чтобы описывать другой элемент (Preview описывает элемент соседней панели; thumbnail включает или отключает системную миниатюру); detailsExtra() → дополнительные строки (показываются и без элемента). Когда они меняются, вид вызывает ctx.refreshDetails().

Клавиши следуют виду: в описании вида sortable: false скрывает клавиши сортировки (activePanelSortable), а itemOps: false — F3–F8 (activePanelItemOps), так что в строке F-клавиш остаётся только то, что здесь работает. Вид, который выбирает элементы по-своему (кликнутый блок в карте диска), реализует currentItem() и targetItems(), возвращающие объекты { name, path, isDir, size }, и вызывает ctx.changed(), когда выбор меняется. refresh() вызывается, когда панель перечитывает ту же папку (после удаления, копирования, команды в шелле), — для видов со своими данными. Режим подключает вид через "view": "my.view". Встроенные виды: list (ядро), icons и preview (core.views). Плагин может заменить и list. Для кликов используйте ctx.panel.click(index, mouseEvent), для двойного клика — ctx.api.commands.execute('panels.open'): так выделение везде работает одинаково.

7. Клавиши и условия when

Синтаксис клавиш: модификаторы+клавиша, в нижнем регистре. Модификаторы: ctrl, alt, shift и meta (клавиша Cmd на macOS; синонимы cmd, win). Клавиши:

  • буквы a–z, цифры 0–9, f1–f24;
  • enter, escape, tab, space, backspace, delete, insert, home, end, pageup, pagedown, up, down, left, right;
  • numpad_add, numpad_subtract, numpad_multiply, numpad_divide, numpad0–numpad9;
  • знаки `- = [ ] \ ; ’ , . / ``.

Клавиши сопоставляются по физическому положению, поэтому ctrl+o работает и в русской раскладке.

Приоритет: пользовательские привязки важнее привязок плагинов. Среди привязок плагинов выигрывает та, у которой when конкретнее (больше условий через &&), а при равенстве — зарегистрированная позже. Пользовательская запись { "key": "f8", "command": "-fileops.delete" } удаляет привязку по умолчанию.

Синтаксис when: &&, ||, !, ==, !=, скобки, 'строки', числа, true/false и контекстные ключи.

Контекстные ключи

Ключ Значение
panelsVisible, panelFocus, terminalFocus, inputFocus, dialogOpen Фокус и видимость
terminalAltScreen Запущена полноэкранная программа (vim, less, htop)
commandLineDirty В командной строке шелла есть текст (zsh сообщает сам, для других шеллов он читается с экрана после приглашения). Тогда ←/→/Enter/Backspace уходят в командную строку
dualPanels, leftPanelVisible, rightPanelVisible Раскладка
panelsResizable Включён режим изменения ширины панелей (panels.toggleResize, ⌘⇧D / Ctrl+Shift+D или перетаскивание разделителя)
activePanel 'left' / 'right'
activePanelProvider, passivePanelProvider, leftPanelProvider, rightPanelProvider например 'fs', 'plugins'
activePanelMode, passivePanelMode, leftPanelMode, rightPanelMode например 'column'
activePanelMeta, leftPanelMeta, rightPanelMeta Уровень метаданных: 'off', 'short', 'long' или '', если у режима их нет
activePanelView 'list', 'icons', 'preview', …
cursorIsDirectory, cursorIsParent, hasSelection Состояние курсора
activePanelSortable, activePanelItemOps Что умеет активный вид (см. хуки видов)
activePanelHasCurrent, activePanelHasTarget Есть элемент для F3/F4 / для копирования, переноса, удаления
editorFocus Фокус в элементе, забирающем клавиши (data-pc-own-keys, например редактор кода)
overlayOpen Что-то закрывает всё окно (редактор кода по F4); Ctrl+O и Ctrl+` не срабатывают
isMac, platform
fsShowHidden Задаёт core.filesystem

8. Цвета и палитры

  • Цвет — это id со значением по умолчанию (contributes.colors). В CSS он доступен как var(--id-через-дефисы).
  • Палитра — именованный набор значений цветов. Итоговая тема = значения по умолчанию, поверх которых наложена активная палитра.
  • Встроенные палитры приходят из плагинов и доступны только для чтения: чтобы править, сначала сделайте копию (Duplicate). Пользовательские палитры лежат в <userData>/palettes/*.json.
  • В Settings → Colors палитры можно активировать, копировать, переименовывать, удалять, экспортировать и импортировать, а каждый цвет — править с живым предпросмотром.

Файл палитры (формат экспорта; format и version при импорте необязательны):

{
  "format": "pejava-palette",
  "version": 1,
  "name": "My Palette",
  "type": "dark",
  "colors": { "panel.background": "#0000a8", "panel.foreground": "#c0c0c0" }
}

Значения цветов: #rgb, #rgba, #rrggbb или #rrggbbaa.

9. Файлы на диске

<userData> — это ~/Library/Application Support/PejavaCommander на macOS и ~/.config/PejavaCommander на Linux.

Файл Содержимое
settings.json Только изменённые настройки.
keybindings.json Пользовательские хоткеи (формат VS Code, -command удаляет).
palettes/*.json Пользовательские палитры.
plugins/<id>/ Установленные плагины.
plugins.json { "disabled": [ids] }
plugin-data/<id>/ Рекомендуемая папка данных для Node-частей.
state.json Состояние UI (пути и режимы панелей, context.state плагинов).

10. Разработка

PC=/Applications/PejavaCommander.app/Contents/MacOS/PejavaCommander   # macOS; Windows: PejavaCommander.exe, Linux: AppImage
"$PC" --plugin-dev=/path/to/my-plugin      # загрузить плагин с диска (можно несколько раз; подходит и папка с плагинами)
PC_PLUGIN_DEV=/a:/b "$PC"                  # то же через переменную окружения
PC_USER_DATA=/tmp/pc-test "$PC"            # отдельный профиль
  • Cmd/Ctrl+Shift+R перезагружает окно. Shell при этом продолжает работать, его вывод восстанавливается.
  • Cmd+Alt+I / Ctrl+Shift+I открывает DevTools. Объект workbench доступен как window.pcWorkbench.
  • Ошибки в activate показываются уведомлением и в менеджере плагинов (Cmd/Ctrl+Shift+X).
  • Чтобы подхватить изменения Node-части, выключите и снова включите плагин или перезапустите приложение.

Упаковка: заархивируйте папку плагина в zip. plugin.json должен лежать в корне архива или в единственной папке верхнего уровня. Установка: менеджер плагинов → F5 → From .zip / From URL. Установка по URL — основа будущего сервера плагинов.

11. Встроенные плагины и команды

Плагин Что даёт
core.workbench (обязательный) Базовые цвета и настройки, палитра команд, окно настроек, выход, связь панелей с командной строкой, основа меню
core.panels (обязательный) Движение курсора, выделение, раскладка, сортировка, выбор источника, поле name, режимы column/columns2/columns3/columns
core.filesystem Провайдер fs, поля size/mtime/perms/owner/ext, цвета типов файлов
core.fileops F3 просмотр, F4 правка, F5 копирование, F6 перенос, F7 новая папка, F8 удаление (Node-часть для копирования и переноса)
core.palettes Встроенные цветовые палитры: классическая синяя, тёмная, светлая, Solarized Dark и Solarized Light
core.views Вид outline (список как в Finder, папки плавно раскрываются на месте), вид icons (плавный скролл, миниатюры, выделение рамкой) и вид preview, Quick View по Ctrl+Q
core.plugin-manager Провайдер plugins (панель менеджера плагинов)
visual.disk-usage Режим visual / вид treemap: сканирует папку в Node-части (параллельно, с прогрессом, Esc отменяет) и рисует «подушечный» treemap; переход внутрь использует уже собранные данные
pejava.codeeditor Правка текста и кода в панели Preview через Monaco (редактор VS Code): превьюер; ⌘S / Ctrl+S сохраняет, Esc возвращает в другую панель; F4 — правка на весь экран, Shift+F4 — новый файл (команды codeEditor.edit, codeEditor.newFile); настройки codeEditor.*
pejava.remote Сетевые подключения: провайдеры remote и remote.files, режим remote.list, ⌘K / Ctrl+Shift+K — подключиться по адресу, F4/F7/F8 — изменить/добавить/удалить подключение в панели Network; на сервере F4 открывает файл в редакторе кода (каждый ⌘S загружает его обратно, а если файл на сервере успел измениться — сначала спрашивает), Shift+F4 — новый файл

Актуальный список всех команд и клавиш — в Settings → Keyboard или по F1 в панелях (по группам). Cmd/Ctrl+Shift+P открывает палитру команд.