Не нравится мне, что так извращаться приходится, но вот каков получился результат.
Функционал: При любом переходе в директорию или перемещении курсора текущее состояние панелей автосохраняется. При запуске нового экземпляра Фара он будет запущен с теми же путями, что и последний используемый Фар.
Требования: Клиент sqlite3.exe для командной строки, я использую из состава Cygwin, можно любой другой сборки. Путь прописан в макросе в переменной SQLITE. Чтоб совсем не тормозило, надо запускать команду через os.execute в фоне, я использую Cygwin-овский setsid, вроде бы ещё есть некий emSpawn (не пробовал).
Как работает: Таймер проверяет панели раз в 50 мс. Если путь или текущий файл на одной из панелей изменились, либо состоялся переход на др. панель, либо панели поменялись местами - начинается обратный отсчёт тиков таймера (автосохранение состояния делается не моментально после каждого изменения, чтоб не грузить излишне систему). Если через 6 тиков (~300 мс) таймера других обновлений не было, состояние панелей сохраняется. Если же на любом тике состояние опять изменилось, отсчёт начинается сначала (с 6). Это позволяет, к примеру, зажать Enter на директории, либо Up/Down на длинном списке файлов, и избежать неприятных задержек. Далее текущее состояние панелей оформляется в виде SQL-запроса, и он применяется через os.execute клиентом sqlite3 к localconfig.db в Far Profile. Чтобы это совсем не тормозило, хорошо бы запускать команду в фоновом режиме, я использую Cygwin-овский setsid.exe, ещё есть некий emSpawn специально для Lua (не пробовал). Тогда вообще не чувствуется никаких задержек. Загрузка CPU в среднем не изменилась.
-- Poll timer interval in milliseconds.
local POLL_INTERVAL = 50
-- Commit delay in poll timer ticks. If set to 0, state is committed immediately.
local COMMIT_DELAY_TICKS = 6
--local SQLITE = 'C:\\cygwin\\bin\\setsid.exe C:\\cygwin\\bin\\sqlite3.exe' -- Run sqlite3 in background (returns immediately, best performance).
local SQLITE = 'C:\\cygwin\\bin\\sqlite3.exe' -- Run sqlite3 directly (blocks execution until transaction has finished).
local LOCALCONFIG_DB = win.GetEnv('FARPROFILE') .. '\\localconfig.db'
local function CommitPanelState()
local leftPanel = APanel.Left and APanel or PPanel
local rightPanel = APanel.Left and PPanel or APanel
local sql = "BEGIN TRANSACTION; DELETE FROM 'general_config'; " ..
"INSERT INTO 'general_config' VALUES('Panel.Left','CurFile','" .. leftPanel.Current:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Left','Folder','" .. leftPanel.Path0:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Right','CurFile','" .. rightPanel.Current:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Right','Folder','" .. rightPanel.Path0:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel','LeftFocus'," .. (APanel.Left and 1 or 0) .. "); " ..
"COMMIT;"
os.execute(SQLITE .. ' "' .. LOCALCONFIG_DB .. '" "' .. sql .. '"')
end
local lastActiveLeft = APanel.Left
local lastLeftPath = APanel.Left and APanel.Path0 or PPanel.Path0
local lastLeftCurrent = APanel.Left and APanel.Current or PPanel.Current
local lastRightPath = APanel.Left and PPanel.Path0 or APanel.Path0
local lastRightCurrent = APanel.Left and PPanel.Current or APanel.Current
local commitPendingTicks = -1
local function UpdatePanelState()
local leftPanel = APanel.Left and APanel or PPanel
local rightPanel = APanel.Left and PPanel or APanel
local modified = false
if APanel.Left ~= lastActiveLeft then
lastActiveLeft = APanel.Left
modified = true
end
if (leftPanel.Type == 0) and ((leftPanel.Path0 ~= lastLeftPath) or (leftPanel.Current ~= lastLeftCurrent)) then
lastLeftPath = leftPanel.Path0
lastLeftCurrent = leftPanel.Current
modified = true
end
if (rightPanel.Type == 0) and ((rightPanel.Path0 ~= lastRightPath) or (rightPanel.Current ~= lastRightCurrent)) then
lastRightPath = rightPanel.Path0
lastRightCurrent = rightPanel.Current
modified = true
end
if modified then
commitPendingTicks = COMMIT_DELAY_TICKS + 1
end
if commitPendingTicks >= 0 then
commitPendingTicks = commitPendingTicks - 1
if commitPendingTicks <= 0 then
commitPendingTicks = -1
CommitPanelState()
end
end
end
local timer = far.Timer(POLL_INTERVAL, function(timer)
timer.Enabled = false
UpdatePanelState()
timer.Enabled = true
end)
Event { description="PanelChangeDir: Clean up timers on exit"; group="ExitFAR";
uid="351F4780-2799-47BF-984A-EB429E8B431E";
action=function()
timer:Close()
end;
}
Event { description="PanelChangeDir: Listen to key events"; group="ConsoleInput";
action=function()
local f = io.open('R:\\pcd.txt', 'a')
f:write('.')
f:close()
end;
}
Попробовал для теста вот такой код. Файл pcd.txt живо наполняется точками (скажем, 40 в секунду), даже если никакие клавиши не нажимать. От такого эвента проку ноль... Или помимо group="ConsoleInput" ещё какие-то условия надо указать?
там не только клавиатура, там весь консольный ввод, включая мышь. всё, кроме нажатий на кнопки клавиатуры и мыши можно точно фильтровать.
ещё можно проверять текущее окно, если не панели, то тоже фильтровать, хотя тут не 100%.
zg, есть ли какие-то принципиальные сложности реализовать нормальное событие смены директории в панели (экспортируемую функцию для плагинов), или просто ни у кого руки не доходят?
Last edited by Shmuel on Wed 09 Nov, 2016 14:39, edited 1 time in total.
Shmuel wrote:zg, есть ли какие-то принципиальные сложности реализовать нормальное событие смены директории в панели (экспортируемую функцию для плагинов), или просто ни у кого руки не доходят?
Что надо добавить в Event { group="ConsoleInput";, чтобы события приходили только на клавиатуру?
А по-моему, было бы здорово иметь событие о смене директории. В хелпе о нём написать предупреждение, чего нельзя делать и о чём не писать на багтрекер.
Вообще я мечтаю о какой-нибудь стандартной ф-ции навроде trace(message) (actionscript), console.log(message) (javascript) и т.д. Чтоб при разработке макросов писать дебаг инфу в какой-то стандартный дебаг лог, который в другом окошке можно удобно `tail -f`-ить. Если есть уже такая фича, то извиняюсь, и буду оч рад о ней узнать.
Last edited by gene.pavlovsky on Wed 09 Nov, 2016 15:51, edited 1 time in total.
gene.pavlovsky wrote:А по-моему, было бы здорово иметь событие о смене директории. В хелпе о нём написать предупреждение, чего нельзя делать и о чём не писать на багтрекер.
смена директории на панели происходит много где в фаре: собственно навигация по панелям, история, фолдер шоткатс, ctrl-f10 в редакторе. но это не так страшно. плагин/макрос могут сменить директорию в любой момент. например во время таскания диалога по экрану. что там нельзя вызывать, что можно — я лично не знаю.
можно конечно разрешить только безопасные функции, но слова надо подкреплять делом, так как хелп никто не читает и особо продвинутые модераторы будут использовать сверх того, что разрешено, а потом возмущаться, что в каком-то билде это перестало работать. а стандартного механизма ограничить вызов части апи в какой-то момент — нет.
Модуль для отладки чрезвычайно полезная вещь, большое спасибо за наводку.
В MacroEx подсмотрел на тему эвентов, и наваял вот что.
При отсутствии эвентов клавиатуры или мыши таймер проверяет панели раз в 5 секунд.
При нажатии на клавиши / кнопки мыши таймер ставится на 250 мс после эвента. Если подряд идёт много эвентов, таймер каждый раз
запускается заново, то есть если зажать и держать вверх/вниз/ввод, панели проверяться не будут. Через 250 мс после последнего эвента панели проверяются. Если состояние изменилось - запускается второй таймер на 250 мс, если за это время не пришло новых эвентов, изменения коммитятся в localconfig.db.
По идее скрипт стал более оптимизирован, на практие разницы никакой не чувствуется )
-- Poll timer interval in milliseconds.
local POLL_INTERVAL = 5000
-- Poll timer interval in milliseconds (on input event).
local POLL_BOOST_INTERVAL = 250
-- Commit delay in millisconds.
local COMMIT_DELAY = 250
--local SQLITE = 'C:\\cygwin\\bin\\setsid.exe C:\\cygwin\\bin\\sqlite3.exe' -- Run sqlite3 in background (returns immediately, best performance).
local SQLITE = 'C:\\cygwin\\bin\\sqlite3.exe' -- Run sqlite3 directly (blocks execution until transaction has finished).
local LOCALCONFIG_DB = win.GetEnv('FARPROFILE') .. '\\localconfig.db'
local function CommitPanelState()
local leftPanel = APanel.Left and APanel or PPanel
local rightPanel = APanel.Left and PPanel or APanel
local sql = "BEGIN TRANSACTION; DELETE FROM 'general_config'; " ..
"INSERT INTO 'general_config' VALUES('Panel.Left','CurFile','" .. leftPanel.Current:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Left','Folder','" .. leftPanel.Path0:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Right','CurFile','" .. rightPanel.Current:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel.Right','Folder','" .. rightPanel.Path0:gsub("'", "''") .. "'); " ..
"INSERT INTO 'general_config' VALUES('Panel','LeftFocus'," .. (APanel.Left and 1 or 0) .. "); " ..
"COMMIT;"
os.execute(SQLITE .. ' "' .. LOCALCONFIG_DB .. '" "' .. sql .. '"')
end
--local dbg = require "dbgprint".filter("FAR:"," ")
local pollTimer
local commitTimer = far.Timer(COMMIT_DELAY, function(timer)
timer.Enabled = false
pollTimer.Enabled = false
-- dbg.print('commit')
CommitPanelState()
pollTimer.Interval = POLL_INTERVAL
pollTimer.Enabled = true
end)
commitTimer.Enabled = false
local lastActiveLeft = APanel.Left
local lastLeftPath = APanel.Left and APanel.Path0 or PPanel.Path0
local lastLeftCurrent = APanel.Left and APanel.Current or PPanel.Current
local lastRightPath = APanel.Left and PPanel.Path0 or APanel.Path0
local lastRightCurrent = APanel.Left and PPanel.Current or APanel.Current
local function UpdatePanelState()
local leftPanel = APanel.Left and APanel or PPanel
local rightPanel = APanel.Left and PPanel or APanel
local modified = false
if APanel.Left ~= lastActiveLeft then
lastActiveLeft = APanel.Left
modified = true
end
if (leftPanel.Type == 0) and ((leftPanel.Path0 ~= lastLeftPath) or (leftPanel.Current ~= lastLeftCurrent)) then
lastLeftPath = leftPanel.Path0
lastLeftCurrent = leftPanel.Current
modified = true
end
if (rightPanel.Type == 0) and ((rightPanel.Path0 ~= lastRightPath) or (rightPanel.Current ~= lastRightCurrent)) then
lastRightPath = rightPanel.Path0
lastRightCurrent = rightPanel.Current
modified = true
end
if modified then
-- dbg.print('modified')
commitTimer.Interval = COMMIT_DELAY
commitTimer.Enabled = true
end
end
pollTimer = far.Timer(POLL_INTERVAL, function(timer)
timer.Enabled = false
UpdatePanelState()
timer.Interval = POLL_INTERVAL
timer.Enabled = true
end)
Event { description="PanelChangeDir: Clean up timers on exit"; group="ExitFAR";
uid="351F4780-2799-47BF-984A-EB429E8B431E";
action=function()
pollTimer:Close()
commitTimer:Close()
end;
}
Event { description="PanelChangeDir: Listen to input events"; group="ConsoleInput";
action=function(Rec)
if ((Rec.EventType == far.Flags.KEY_EVENT) and (Rec.VirtualKeyCode ~= 0)) or
((Rec.EventType == far.Flags.MOUSE_EVENT) and (Rec.EventFlags ~= far.Flags.MOUSE_MOVED)) then
commitTimer.Enabled = false
pollTimer.Interval = POLL_BOOST_INTERVAL
end
end;
}
gene.pavlovsky wrote: При отсутствии эвентов клавиатуры или мыши таймер проверяет панели раз в 5 секунд.
Перестраховка?
gene.pavlovsky wrote:Через 250 мс после последнего эвента панели проверяются. Если состояние изменилось - запускается второй таймер на 250 мс, если за это время не пришло новых эвентов, изменения коммитятся в localconfig.db.
А просто проверять (и сразу записывать) через 500 мс после последнего события не то же самое будет?
P.S.
Вместо sqlite можно попробовать far.exe /import
Last edited by John Doe on Thu 10 Nov, 2016 13:50, edited 1 time in total.