Skip to content

Commit ef59007

Browse files
committed
Adjusts plugin syntax
1 parent a1471eb commit ef59007

4 files changed

Lines changed: 162 additions & 115 deletions

File tree

‎python/test_plugin.far.py‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,9 @@ def __init__(self, psi_ptr=None):
2929
log.info("Simple test plugin initialized")
3030

3131
def get_plugin_info(self):
32-
"""Return plugin metadata dict (required by the adapter)."""
32+
"""Return PluginInfo dict with menu items, flags, and prefix."""
3333
return {
34-
"title": self.title,
35-
"description": self.description,
36-
"author": self.author,
37-
"version": ".".join(str(v) for v in self.version),
34+
"plugin_menu": self.title,
3835
}
3936

4037
def GetPluginInfoW(self, info_ptr):

‎src/adapter/adapter.cpp‎

Lines changed: 128 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,15 @@ std::unique_ptr<PluginModule> PythonFarAdapter::CreatePluginModule(const wchar_t
455455
LOG_TRACE("Successfully loaded plugin module");
456456

457457
auto pluginModule = std::make_unique<PluginModule>(filename, pModule.release(), pPluginClass.release());
458-
459-
458+
459+
// Plugin must have a non-empty title class attribute.
460+
if (pluginModule->GetTitle().empty()) {
461+
LOG_TRACE("Plugin has no title");
462+
m_ErrorSummary = L"Plugin Error";
463+
m_ErrorDescription = L"Plugin class must define a non-empty 'title' attribute";
464+
return nullptr;
465+
}
466+
460467
return pluginModule;
461468

462469
} catch (...) {
@@ -1024,22 +1031,22 @@ void PluginModule::ReportPluginInfoFailure(const std::wstring& summary, const st
10241031

10251032
void PluginModule::GetPluginInfoW(PluginInfo* Info) {
10261033
LOG_TRACE("PluginModule::GetPluginInfoW");
1027-
1034+
10281035
if (!Info) return;
10291036

10301037
Info->StructSize = sizeof(PluginInfo);
10311038
Info->Flags = 0;
10321039
Info->CommandPrefix = nullptr;
10331040
Info->Instance = nullptr;
1034-
1041+
10351042
Info->DiskMenu.Count = 0;
10361043
Info->DiskMenu.Strings = nullptr;
10371044
Info->DiskMenu.Guids = nullptr;
1038-
1045+
10391046
Info->PluginMenu.Count = 0;
10401047
Info->PluginMenu.Strings = nullptr;
10411048
Info->PluginMenu.Guids = nullptr;
1042-
1049+
10431050
Info->PluginConfig.Count = 0;
10441051
Info->PluginConfig.Strings = nullptr;
10451052
Info->PluginConfig.Guids = nullptr;
@@ -1067,7 +1074,7 @@ void PluginModule::GetPluginInfoW(PluginInfo* Info) {
10671074
if (PyErr_Occurred()) PyErr_Clear();
10681075
ReportPluginInfoFailure(
10691076
L"Plugin does not implement get_plugin_info().",
1070-
L"Define get_plugin_info() returning a dict with at least a 'title', "
1077+
L"Define get_plugin_info() returning a dict, "
10711078
L"or inherit from the far.Plugin base class which provides it.");
10721079
return;
10731080
}
@@ -1088,84 +1095,135 @@ void PluginModule::GetPluginInfoW(PluginInfo* Info) {
10881095
if (PyErr_Occurred()) PyErr_Clear();
10891096
ReportPluginInfoFailure(
10901097
L"get_plugin_info() did not return a dict.",
1091-
L"It must return a dict with at least a 'title' key.");
1098+
L"It must return a dict.");
10921099
return;
10931100
}
10941101

1095-
// A plugin must provide at least a non-empty title (its name).
1096-
{
1097-
PyObject* titleCheck = PyDict_GetItemString(result, "title"); // borrowed
1098-
bool hasTitle = titleCheck && PyUnicode_Check(titleCheck)
1099-
&& PyUnicode_GetLength(titleCheck) > 0;
1100-
if (!hasTitle) {
1101-
ReportPluginInfoFailure(
1102-
L"get_plugin_info() did not provide a plugin name.",
1103-
L"The returned dict must contain a non-empty 'title'.");
1104-
return;
1105-
}
1106-
}
1107-
1108-
// Parse the dictionary result
1102+
// --- Parse the dictionary ---
1103+
11091104
PyObject* flagsObj = PyDict_GetItemString(result, "flags");
11101105
if (flagsObj && PyLong_Check(flagsObj)) {
11111106
Info->Flags = PyLong_AsLong(flagsObj);
11121107
}
11131108

1114-
PyObject* titleObj = PyDict_GetItemString(result, "title");
1115-
if (titleObj && PyUnicode_Check(titleObj)) {
1116-
const char* titleStr = PyUnicode_AsUTF8(titleObj);
1117-
if (titleStr) {
1118-
int len = MultiByteToWideChar(CP_UTF8, 0, titleStr, -1, nullptr, 0);
1119-
titleBuffer.resize(len);
1120-
MultiByteToWideChar(CP_UTF8, 0, titleStr, -1, &titleBuffer[0], len);
1121-
menuStrings.clear();
1122-
menuStrings.push_back(&titleBuffer[0]);
1123-
menuGuids.clear();
1124-
menuGuids.push_back(m_Guid); // Use the plugin's GUID
1125-
Info->PluginMenu.Guids = menuGuids.data();
1126-
Info->PluginMenu.Strings = menuStrings.data();
1127-
Info->PluginMenu.Count = menuStrings.size();
1109+
auto parseGuid = [](const wchar_t* s) -> GUID {
1110+
GUID g = {};
1111+
CLSIDFromString(s, &g);
1112+
return g;
1113+
};
1114+
1115+
auto pyToWide = [](PyObject* obj, std::vector<wchar_t>& buf) -> bool {
1116+
const char* utf8 = PyUnicode_AsUTF8(obj);
1117+
if (!utf8) return false;
1118+
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0);
1119+
if (len <= 0) return false;
1120+
buf.resize(len);
1121+
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, buf.data(), len);
1122+
return true;
1123+
};
1124+
1125+
auto readPair = [&](PyObject* item,
1126+
GUID& outGuid,
1127+
std::wstring& outText) -> bool {
1128+
if (!PySequence_Check(item)) return false;
1129+
Py_ssize_t itemLen = PySequence_Size(item);
1130+
if (itemLen != 2) return false;
1131+
1132+
PyObject* guidObj = PySequence_GetItem(item, 0);
1133+
PyObject* textObj = PySequence_GetItem(item, 1);
1134+
if (!guidObj || !textObj || !PyUnicode_Check(guidObj) || !PyUnicode_Check(textObj)) {
1135+
Py_XDECREF(guidObj); Py_XDECREF(textObj);
1136+
return false;
11281137
}
1129-
}
1138+
std::vector<wchar_t> tmp;
1139+
bool ok = pyToWide(textObj, tmp);
1140+
if (ok) outText = tmp.data();
1141+
ok = ok && pyToWide(guidObj, tmp);
1142+
if (ok) outGuid = parseGuid(tmp.data());
1143+
Py_XDECREF(guidObj); Py_XDECREF(textObj);
1144+
return ok;
1145+
};
11301146

1131-
PyObject* descObj = PyDict_GetItemString(result, "description");
1132-
if (descObj && PyUnicode_Check(descObj)) {
1133-
const char* descStr = PyUnicode_AsUTF8(descObj);
1134-
if (descStr) {
1135-
int len = MultiByteToWideChar(CP_UTF8, 0, descStr, -1, nullptr, 0);
1136-
descBuffer.resize(len);
1137-
MultiByteToWideChar(CP_UTF8, 0, descStr, -1, &descBuffer[0], len);
1138-
configStrings.clear();
1139-
configStrings.push_back(&descBuffer[0]);
1140-
// Re-use menuGuids or populate a new guids array for config. We can just point it to menuGuids since it has m_Guid
1141-
if (menuGuids.empty()) {
1142-
menuGuids.push_back(m_Guid);
1143-
}
1144-
Info->PluginConfig.Guids = menuGuids.data();
1145-
Info->PluginConfig.Strings = configStrings.data();
1146-
Info->PluginConfig.Count = configStrings.size();
1147+
auto buildMenu = [&](const char* key,
1148+
std::vector<wchar_t>& buf,
1149+
std::vector<const wchar_t*>& strs,
1150+
std::vector<GUID>& guids,
1151+
PluginMenuItem& menuItem) {
1152+
buf.clear(); strs.clear(); guids.clear();
1153+
PyObject* obj = PyDict_GetItemString(result, key);
1154+
if (!obj) return;
1155+
1156+
// plain string → single item, plugin GUID
1157+
if (PyUnicode_Check(obj)) {
1158+
const char* utf8 = PyUnicode_AsUTF8(obj);
1159+
if (!utf8 || !*utf8) return;
1160+
int len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, nullptr, 0);
1161+
if (len <= 0) return;
1162+
buf.resize(len);
1163+
MultiByteToWideChar(CP_UTF8, 0, utf8, -1, buf.data(), len);
1164+
strs.push_back(buf.data());
1165+
guids.push_back(m_Guid);
1166+
menuItem.Guids = guids.data();
1167+
menuItem.Strings = strs.data();
1168+
menuItem.Count = 1;
1169+
return;
11471170
}
1148-
}
11491171

1150-
PyObject* authorObj = PyDict_GetItemString(result, "author");
1151-
if (authorObj && PyUnicode_Check(authorObj)) {
1152-
const char* authorStr = PyUnicode_AsUTF8(authorObj);
1153-
if (authorStr) {
1154-
int len = MultiByteToWideChar(CP_UTF8, 0, authorStr, -1, nullptr, 0);
1155-
authorBuffer.resize(len);
1156-
MultiByteToWideChar(CP_UTF8, 0, authorStr, -1, &authorBuffer[0], len);
1172+
if (!PySequence_Check(obj)) return;
1173+
Py_ssize_t seqLen = PySequence_Size(obj);
1174+
if (seqLen <= 0) return;
1175+
1176+
// [guid, str] → single item, explicit GUID
1177+
if (seqLen == 2) {
1178+
PyObject* first = PySequence_GetItem(obj, 0);
1179+
bool firstIsStr = PyUnicode_Check(first);
1180+
Py_XDECREF(first);
1181+
if (firstIsStr) {
1182+
GUID itemGuid = {};
1183+
std::wstring itemText;
1184+
if (readPair(obj, itemGuid, itemText)) {
1185+
buf.resize(itemText.size() + 1);
1186+
wcscpy_s(buf.data(), buf.size(), itemText.c_str());
1187+
strs.push_back(buf.data());
1188+
guids.push_back(itemGuid);
1189+
}
1190+
menuItem.Guids = guids.data();
1191+
menuItem.Strings = strs.data();
1192+
menuItem.Count = static_cast<size_t>(strs.size());
1193+
return;
1194+
}
11571195
}
1158-
}
11591196

1160-
PyObject* versionObj = PyDict_GetItemString(result, "version");
1161-
if (versionObj && PyUnicode_Check(versionObj)) {
1162-
const char* versionStr = PyUnicode_AsUTF8(versionObj);
1163-
if (versionStr) {
1164-
int len = MultiByteToWideChar(CP_UTF8, 0, versionStr, -1, nullptr, 0);
1165-
versionBuffer.resize(len);
1166-
MultiByteToWideChar(CP_UTF8, 0, versionStr, -1, &versionBuffer[0], len);
1197+
// [[guid, str], …] → multiple items
1198+
struct Item { GUID guid; std::wstring text; };
1199+
std::vector<Item> items;
1200+
for (Py_ssize_t i = 0; i < seqLen; ++i) {
1201+
PyObject* elem = PySequence_GetItem(obj, i);
1202+
if (!elem) continue;
1203+
Item it = {};
1204+
if (readPair(elem, it.guid, it.text)) {
1205+
items.push_back(std::move(it));
1206+
}
1207+
Py_XDECREF(elem);
11671208
}
1168-
}
1209+
if (items.empty()) return;
1210+
1211+
for (const auto& it : items) {
1212+
size_t pos = buf.size();
1213+
for (wchar_t c : it.text)
1214+
buf.push_back(c);
1215+
buf.push_back(L'\0');
1216+
strs.push_back(buf.data() + pos);
1217+
guids.push_back(it.guid);
1218+
}
1219+
menuItem.Guids = guids.data();
1220+
menuItem.Strings = strs.data();
1221+
menuItem.Count = static_cast<size_t>(strs.size());
1222+
};
1223+
1224+
buildMenu("plugin_menu", menuBuffer, menuStrings, menuGuids, Info->PluginMenu);
1225+
buildMenu("plugin_config", configBuffer, configStrings, configGuids, Info->PluginConfig);
1226+
buildMenu("disk_menu", diskBuffer, diskStrings, diskGuids, Info->DiskMenu);
11691227

11701228
PyObject* prefixObj = PyDict_GetItemString(result, "command_prefix");
11711229
if (prefixObj && PyUnicode_Check(prefixObj)) {

‎src/adapter/adapter.hpp‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class PluginModule {
153153
// Helper to call Python methods
154154
PyObject* CallMethod(const char* methodName, const char* format = nullptr, ...);
155155

156+
// Plugin title (class attribute), required to be non-empty.
157+
const std::wstring& GetTitle() const { return m_Title; }
158+
156159
private:
157160
// Report a fatal PluginInfo failure: log it, record it for Far's GetError,
158161
// and show a message box to the user. `detail` may contain a Python
@@ -178,16 +181,18 @@ class PluginModule {
178181
UUID m_Guid;
179182

180183
// Buffers for string data
181-
std::vector<wchar_t> titleBuffer;
182-
std::vector<wchar_t> descBuffer;
183-
std::vector<wchar_t> authorBuffer;
184-
std::vector<wchar_t> versionBuffer;
184+
std::vector<wchar_t> menuBuffer;
185+
std::vector<wchar_t> configBuffer;
186+
std::vector<wchar_t> diskBuffer;
185187
std::vector<wchar_t> commandPrefixBuffer;
186188

187189
// Arrays of string pointers for PluginInfo
188190
std::vector<const wchar_t*> menuStrings;
189191
std::vector<const wchar_t*> configStrings;
192+
std::vector<const wchar_t*> diskStrings;
190193
std::vector<GUID> menuGuids;
194+
std::vector<GUID> configGuids;
195+
std::vector<GUID> diskGuids;
191196
};
192197

193198
// Global adapter instance

0 commit comments

Comments
 (0)