воскресенье, 10 марта 2019 г.

Плагин для Far Manager своими руками

Приветствую тебя, мой любознательный друг!
Наверняка ты в курсе, что любому разработчику как воздух необходим хороший, удобный, быстрый и мощный файловый менеджер. А если ты, волею судеб, работаешь в Windows, эта необходимость возрастает кратно.
Существует много неплохих файловых менеджеров под Windows, но есть один, который затмевает всех!
Это мастодонт, глыба, король всех файловых менеджеров!
И имя ему - Far Manager! Те, кто работает с Far годами, знают, что его живучесть и солидный возраст обеспечивается мощной развитой поддержкой плагинов, которых у него великое множество. И сегодня, дорогой друг, мы на примере создания простого плагина погрузимся в этот удивительный мир Far.
Поехали!


Немного истории
Те, кто впервые видит Far, возможно, немного обескуражены его, мягко говоря, консервативным внешним видом. Пусть эта эпатирующая "олдскульность" не вводит вас в заблуждение: перед вами один из самых мощных файловых менеджеров, архиваторов, редакторов текста всего мира, детище Евгения Рошала, который, кроме него подарил нам и архиватор WinRAR.
И если считать дату первого релиза Far его днем рождения, то 10 сентября 2019 года ему исполнится аж 26 лет. 
Тем не менее этот старичок все еще в строю и продолжает нас радовать своими богатыми возможностями, особенно полезными для разработчиков. 
Вот лишь некоторые из них, согласно официальной документации:
  • управление принтерами, как подключенными к ПК, так и сетевыми;
  • подсветка синтаксиса в исходных текстах программ;
  • работа с FTP-серверами (с поддержкой доступа через различные типы прокси, автоматической докачкой и прочее);
  • поиск и замена символов одновременно во множестве файлов с применением регулярных выражений;
  • средства переименования групп файлов с возможностью использования сложных составных масок из символов подстановки и шаблонов;
  • NNTP/SMTP/POP3/IMAP4 клиенты и отправка сообщений на пейджер;
  • работа при нестандартных размерах текстового экрана;
  • перекодировка текстов с учетом национальных кодовых таблиц;
  • манипуляции с содержимым корзины;
  • управление приоритетами процессов на локальном или на сетевом ПК;
  • автозавершение слов в редакторе и работа с шаблонами;
  • редактирование системного реестра Windows;
  • создание и изменение ярлыков Windows;
  • всевозможные манипуляции с файлами и текстом, делающие комфортной работу с FIDO;
  • кодирование и декодирование файлов в формате UUE;
  • управление программой WinAmp и модификация комментариев MP3-файлов;
  • обработка Quake PAK файлов;
  • Работа с различными серверами через ODBC + работа с серверами ORACLE через OCI;
  • управление службой RAS;
  • запуск внешних программ (компиляторов, конверторов и проч.) при редактировании текстов в редакторе Far;
  • отображение содержимого файлов справки Windows (.hlp и .chm);
  • калькуляторы с разными возможностями;
  • различные игры;
  • функции проверки орфографии при обработке текста в редакторе Far;
  • подготовка каталога сменных накопителей и многое другое.
Far имеет проработанную модульную архитектуру, что позволяет существенно расширять его возможности плагинами, полный список которых можно посмотреть тут. Можно также использовать макросы, написанные на языке Lua, в общем, это мощнейший инструмент в руках опытного разработчика и легковесный производительный редактор файлов в руках начинающего. 

Что ж, теперь пора написать свой маленький плагин для Far. 

Подготовка
Для создания плагина нам понадобится:
  1. Far Manager последней версии, качаем тут и устанавливаем. Папку установки будем обозначать %FAR%;
  2. Исходный код Far Manager выкачиваем с GitHub в папку, которую обозначим %SOURCE%;
  3. Vusial Studio 2017 с поддержкой языка C++ берем с сайта Microsoft (подойдет любой вариант, даже Community Edition);
Исходники есть, инструменты для разработки есть, теперь мы готовы разработать свой Far плагин, хотя...

Что такое Far плагин?
Любой плагин Far - это dll, которая должна экспортировать ряд функций для того, чтобы Far мог интегрировать ее в свой процесс. 
Эти функции представляют собой часть Plugins API и описаны в документации разработчика Far. Здесь приведем лишь их краткое описание:

ФункцияОписание
ExitFARW перед выходом из Far Manager
GetGlobalInfoW основная информация о плагине
GetPluginInfoW дополнительная информация о плагине
OpenW вызывает плагин
SetStartupInfoW Far Manager передает плагину необходимую информацию

Наш плагин очень простой и перед закрытием Far нам не нужно что-либо делать, поэтому функция ExitFARW нам не понадобится.

Создание проекта
Наконец, перейдем непосредственно к программированию. Запускаем Visual Studio, выбираем пункт меню Файл -> Создать -> Проект, после чего, в открывшемся меню создания проекта, выбираем шаблон "Библиотека динамической компоновки (Dll)":


Вводим имя проекта HelloWorld и жмем ОК.
Битность скачанного вами Far Manager-а и битность плагина должны совпадать, поэтому, если Far 64-битный - нужно собрать 64-битную dll-ку. 

Для этого сразу в выпадающем списке "Платформы решения" выбираем нужную битность. 
У меня скачан 32-битный Far, поэтому у меня выбран пункт x86.


Переходим в свойства проекта. 
Раздел "Свойства конфигурации/Каталоги VC++", пункт "Включаемые каталоги", добавляем туда папку %SOURCE%/Far, где %SOURCE% - путь до папки, куда вы выкачали исходники Far. На моем компьютере это выглядит так:


Это необходимо, чтобы мы могли в своем проекте использовать заголовочные файлы Far.
Раздел "C/C++/Язык", пункт "Стандарт языка C++", выбираем "Стандарт ISO C++17":


В исходном коде Far есть много особенностей именно C++17 (например string_view), поэтому если не переключить проект на использование этого стандарта, он просто не скомпилируется.

Раздел "Свойства конфигурации/Общие", пункт "Набор символов". Выбираем значение "Использовать набор символов Юникода", если не выбрано:


Раздел "Компоновщик/Ввод", пункт "Файл определения модуля". Вводим HelloWorld.def:


Этот файл, который мы создадим позднее, содержит список экспортируемых функций Plugins API.
Сохраняем все изменения, нажав OK, и закрываем окно свойств проекта.

Теперь, когда проект настроен, нужно создать несколько вспомогательных файлов.

HelloWorld.def
В этом файле мы перечислим функции, которые будем экспортировать. Если этого не сделать, Far Manager при загрузке плагина их не увидит и, соответственно, сам плагин не будет загружен. 
Содержимое файла выглядит так:
LIBRARY        HelloWorld
; DESCRIPTION    'HelloWorld Far Plugin'
EXPORTS
  GetGlobalInfoW
  SetStartupInfoW
  GetPluginInfoW
  OpenW

Заголовочный файл version.hpp
Этот заголовочный файл содержит макросы с информацией о плагине и его авторе:
/*
 Директива препроцессора, которая говорит, 
 что содержимое этого файла нужно включить в итоговый код программы лишь один раз
*/
#pragma once

/*
Подключаем заголовочный файл plugin.hpp из исходников Far,
чтобы использовать макросы MAKEFARVERSION, FARMANAGERVERSION_MAJOR и другие
*/
#include <plugin.hpp>

//Версия сборки плагина
#define PLUGIN_BUILD 0
//Описание плагина
#define PLUGIN_DESC L"Hello World Plugin for Far Manager"
//Имя плагина
#define PLUGIN_NAME L"HelloWorld"
//Имя dll-ки
#define PLUGIN_FILENAME L"HelloWorld.dll"
//Автор плагина
#define PLUGIN_AUTHOR L"Toolkas"
//Полная версия плагина
#define PLUGIN_VERSION MAKEFARVERSION(FARMANAGERVERSION_MAJOR,FARMANAGERVERSION_MINOR,FARMANAGERVERSION_REVISION,PLUGIN_BUILD,VS_RELEASE)

Заголовочный файл guid.hpp
Этот заголовочный файл присваивает GUID плагину и его элементам интерфейса:
/*
 Директива препроцессора, которая говорит,
 что содержимое этого файла нужно включить в итоговый код программы лишь один раз
*/
#pragma once

/*
Подключаем заголовочный файл initguid.h от компании Microsoft
для использования макроса DEFINE_GUID, позволяющего объявить новый GUID.
GUID - это глобальный уникальный идентификатор, который можно присвоить вашему плагину, 
сервису, документу и чему угодно.
Чтобы сгенерировать новый GUID можно воспользоваться любым online-сервисом, 
например этим https://www.guidgenerator.com/online-guid-generator.aspx.

Если хотите больше узнать про GUID, начните с статьи на wiki:
https://ru.wikipedia.org/wiki/GUID
*/
#include <initguid.h>

// {40D43854-8283-452B-8878-4332C681B244}
DEFINE_GUID(MainGuid, 0x40d43854, 0x8283, 0x452b, 0x88, 0x78, 0x43, 0x32, 0xc6, 0x81, 0xb2, 0x44);

// {A0FB9EB5-EDEE-42C3-A37F-549DE8C597B4}
DEFINE_GUID(MenuGuid, 0xa0fb9eb5, 0xedee, 0x42c3, 0xa3, 0x7f, 0x54, 0x9d, 0xe8, 0xc5, 0x97, 0xb4);

Файлы локализации
Far Manager спроектирован как мультиязычный продукт, поэтому любые сообщения и надписи, показываемые пользователю, в том числе и текст справки, необходимо дублировать на нескольких языках.

Для локализации Far использует файлы двух форматов: HLF и LNG.

Файлы HLF
Файлы с расширением hlf - это текстовые файлы в кодировке UTF-8, которые содержат текст справки плагина. Для вызова справки плагина в Far нужно нажать F11, отобразится список плагинов, выбрать интересующий вас плагин и нажать Shift+F1. Отобразится справка по плагину, например:


Файлы справки должны лежать в той же папке, где лежит dll плагина. 
Имя файла имеет формат [Имя_плагина]_[код_языка].hlf. 
Для нашего плагина HelloWorld мы создадим справку на русском и английском языке, поэтому нам нужно создать два файла: HelloWorld_ru.hlf и HelloWorld_en.hlf.

Формат содержимого этих файлов довольно простой:
.Language=Russian,Russian (Русский) - для какого языка используется этот файл справки
.PluginContents=Здравствуй, Мир! - заголовок файла справки

@Contents - начало первой страницы
$ #Здравствуй, Мир!# - заголовок для данной страницы

Это файл справки приветливого плагина=)

Итого, содержимое файла HelloWorld_ru.hlf имеет вид:
.Language=Russian,Russian (Русский)
.PluginContents=Здравствуй, Мир!

@Contents
$ #Здравствуй, Мир!#

Это файл справки приветливого плагина=)

а содержимое файла HelloWorld_en.hlf:
.Language=English,English (English)
.PluginContents=Hello, World!

@Contents
$ #Hello, World!#

This is HelloWorld Plugin help=)
Если вызвать справку нашего плагина на русском языке, то она должна иметь вот такой вид:


Файлы LNG
Файлы с расширением lng - языковые файлы - это текстовые файлы в кодировке UTF-8, которые хранят строки, используемые плагином для вывода сообщений пользователю в диалогах, меню и т.п.
Языковые файлы должны лежать в той же папке, что и dll-файл плагина.
Имя языкового файла имеет формат [Имя_плагина]_[код_языка].lng. В нашем плагине будут два языковых файла: HelloWorld_en.lng и HelloWorld_ru.lng.

Содержимое языковых файлов имеет следующий вид:
.Language=Russian,Russian (Русский) - язык файла
...
"строка1"
...
"строка2"
...
"строка3"
...
Все строки в lng-файлах записываются в двойных кавычках. Все остальные строки, не начинающиеся с двойных кавычек игнорируются.

Для нашего плагина мы напишем такой файл HelloWorld_en.lng:
.Language=English,English

"Hello, World!"

""
"Develop in the name of good"
"   Toolkas"
""

"&Ok"

и файл HelloWorld_ru.lng:
.Language=Russian,Russian (Русский)

"Здравствуй, Мир!"

""
"Программируйте во имя добра!"
"Toolkas"
""

"Я согласен"

Для того, чтобы получить строку из файла используется функция
Info.GetMsg(&MainGuid, MsgIndex)
где MsgIndex - это номер строки в языковом файле (нумерация идет с 0).

HelloWorldLang.hpp
Поскольку строк в языковых файлов может быть много и неудобно в коде использовать обезличенные номера строк, типа
Info.GetMsg(&MainGuid, 2)
часто разработчики используют тот факт, что перечисления в C++ - это, по сути, целочисленный тип. Создадим заголовочный файл HelloWorldLang.hpp, в котором объявим перечисление:
#pragma once

//Перечисление которое хранит имена строк в языковом файле
enum {
 MTitle,

 MMessage1,
 MMessage2,
 MMessage3,
 MMessage4,

 MButton,
};

Теперь вместо вызова
Info.GetMsg(&MainGuid, 2)
можно написать
Info.GetMsg(&MainGuid, MMessage2)
что, согласитесь, выглядит понятнее.

HelloWorld.cpp
Остался последний файл, который содержит непосредственно реализацию экспортируемых функций. 
#include "stdafx.h"

#include "version.hpp"
#include "guid.hpp"
#include "HelloWorldLang.hpp"

//Статическая переменная, которая хранит структуру для доступа к Far API.
static struct PluginStartupInfo Info;

/*
Функция, которая вызывается Far-ом для того, 
чтобы плагин предоставил информацию о себе.

Если хотя-бы одно из полей структуры GlobalInfo не будет заполнено - плагин не инициализируется.
*/
void WINAPI GetGlobalInfoW(struct GlobalInfo *Info)
{
 Info->StructSize = sizeof(struct GlobalInfo);
 Info->MinFarVersion = FARMANAGERVERSION;
 Info->Version = PLUGIN_VERSION;
 Info->Guid = MainGuid;
 Info->Title = PLUGIN_NAME;
 Info->Description = PLUGIN_DESC;
 Info->Author = PLUGIN_AUTHOR;
}

/*
Используя эту функцию, Far передает плагину указатель на структуру, 
посредством которой будет в дальнейшем осуществляется доступ к Far API.
*/
void WINAPI SetStartupInfoW(const struct PluginStartupInfo *startupInfo)
{
 Info = *startupInfo;
}
 
/*
Эта функция используется Far-ом, чтобы выяснить детальную информацию о плагине.
А конкретно - плагин может зарегистрироваться в меню команд, 
которое вызывается в Far через нажатие F11.
*/
void WINAPI GetPluginInfoW(struct PluginInfo *pluginInfo)
{
 pluginInfo->StructSize = sizeof(*pluginInfo);
 pluginInfo->Flags = PF_EDITOR;

 static const wchar_t* PluginMenuStrings[1];
 PluginMenuStrings[0] = Info.GetMsg(&MainGuid, MTitle);
 pluginInfo->PluginMenu.Guids = &MenuGuid;
 pluginInfo->PluginMenu.Strings = PluginMenuStrings;
 pluginInfo->PluginMenu.Count = ARRAYSIZE(PluginMenuStrings);
}
 
/*
Эта функция вызывается при нажатии Enter в меню команд.
*/
HANDLE WINAPI OpenW(const struct OpenInfo *OInfo)
{
 const wchar_t *MsgItems[] =
 {
  Info.GetMsg(&MainGuid, MTitle),
  Info.GetMsg(&MainGuid, MMessage1),
  Info.GetMsg(&MainGuid, MMessage2),
  Info.GetMsg(&MainGuid, MMessage3),
  Info.GetMsg(&MainGuid, MMessage4),
  L"\x01",       
  Info.GetMsg(&MainGuid, MButton),
 };

 Info.Message(&MainGuid, NULL, FMSG_WARNING | FMSG_LEFTALIGN, L"Contents", 
  MsgItems, ARRAYSIZE(MsgItems), 1);                          
 return NULL;
}

Компиляция
Наша длительная подготовительная работа наконец закончена.
Но если мы запустим компиляцию прямо сейчас, то получим ошибки компиляции кода в plugin.hpp:
#ifdef FAR_USE_INTERNALS
 FarMacroValue(const string& v)    { Type=FMVT_STRING; String=v.c_str(); }
#endif // END FAR_USE_INTERNALS
Если честно, мне было лень разбираться в чем там дело, но так как я нигде не использовал макрос FAR_USE_INTERNALS, я просто закомментировал проблемную строчку=)
#ifdef FAR_USE_INTERNALS
 //FarMacroValue(const string& v)    { Type=FMVT_STRING; String=v.c_str(); }
#endif // END FAR_USE_INTERNALS

Установка плагина
Установка плагина в Far проста и прямолинейна: нужно лишь создать папку %FAR%/Plugins/HelloWorld и скопировать туда dll, файлы помощи и языковые файлы.


Теперь для загрузки нашего плагина достаточно перезапустить Far.

Проверка работы плагина
Перезапустив Far, нажмем кнопку F11. Должно отобразиться меню команд, один из пунктов которого принадлежит нашему плагину:


Теперь, если нажать Shift+F1 (вызвать справку плагина HelloWorld), мы увидим ожидаемое окно справки с текстом, который мы писали в файле HelloWorld_ru.hlf:


Закрыв окно справки нажатием кнопки Escape, запустим плагин, нажав Enter. Тогда мы увидим:


Соответственно, если переключить Far на использование английского языка (F9 -> Параметры -> Языки -> English -> English), то мы увидим справку и диалоговое сообщение на английском языке.

Если что-то пошло не так...
Если после нажатия F11 вы не видите плагин в меню, значит что-то пошло не так. Разберем типовые проблемы, которые могут возникнуть.

Проверьте битность
Битность Far и битность dll вашего плагина должна совпадать. Проверьте настройки Visual Studio и убедитесь, что это так.

Проверьте экспортируемые функции 
При запуске Far автоматически читает содержимое папки %FAR%/Plugins и пытается найти экспортируемые функции в dll-ках. Если по каким-то причинам он их не найдет - то плагин загружен не будет. 
Именно для этого мы и создавали файл HelloWorld.def и прописывали его в свойствах проекта.
Чтобы проверить, действительно ли вы экспортировали все необходимые функции, можно воспользоваться утилитой DllExportViewer, которую можно скачать тут.

Запустив утилиту и открыв ею HelloWorld.dll, вы должны увидеть следующее:


Если каких-то функций не хватает, нужно внимательно проверить настройки проекта, файл HelloWorld.def и прописан ли он в свойствах проекта.

Итог
Итак, дорогие друзья, мы написали хоть и маленький, но вполне рабочий плагин. Надеюсь, я смог увлечь вас удивительным миром Far. Исходный код проекта лежит на GitHub.

Если вас заинтересовала эта тема, вот несколько полезных ссылок:

Исследуйте, пишите плагины, исправляйте баги в Far и получайте удовольствие от этого!
Если возникнут непреодолимые проблемы - можете писать мне сюда или сюда, по возможности отвечу всем!
Всем добра и творческих успехов!


1 комментарий:

  1. " так как я нигде не использовал макрос FAR_USE_INTERNALS" тогда бы нигде ошибку и не получил

    ОтветитьУдалить