Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active August 26, 2024 14:08
Show Gist options
  • Save johnd0e/3269d681b8af5a4cc552ddfaf5fdf6b2 to your computer and use it in GitHub Desktop.
Save johnd0e/3269d681b8af5a4cc552ddfaf5fdf6b2 to your computer and use it in GitHub Desktop.
Far Manager Macro: bring ChatGPT to Far (bito.ai code assistant)
local Info = Info or package.loaded.regscript or function(...) return ... end --luacheck: ignore 113/Info
local nfo = Info { _filename or ...,
name = "bito.ai code assistant";
description = "bring ChatGPT to Far";
version = "0.10+popen"; --http://semver.org/lang/ru/
author = "jd";
url = "https://forum.farmanager.com/viewtopic.php?t=13283";
download = "https://github.com/gitbito/CLI/raw/main/version-3.6/bito.exe",
www = "https://bito.ai/",
webchat = "https://alpha.bito.ai/bitoai/home",
prompthub = "https://prompthub.bito.ai/",
id = "E8B4B673-212E-495D-A19D-FEFDB9DE8AF4";
minfarversion = {3,0,0,6279,0}; --actl: LuaMacro 810
--disabled = false;
options = {
linewrap = 80,
};
}
if not nfo or nfo.disabled then return end
local O = nfo.options
local F = far.Flags
local idProgress = win.Uuid"3E5021C5-47C7-4446-8E3B-13D3D9052FD8"
local function progress (text, title)
local len = math.max(text:len(), title and title:len() or 0, 7)
local items = {
--[[01]] {F.DI_SINGLEBOX,0,0,len+4,3,0,0,0, 0, title},
--[[02]] {F.DI_TEXT, 2,1, 0,1,0,0,0,F.DIF_CENTERGROUP, text},
}
return far.DialogInit(idProgress, -1, -1, len+4, 3, nil, items, F.FDLG_NONMODAL)
end
local function _words (getchunk)
local buf, eof = "", nil
return function()
while true do
if not eof then
local chunk = getchunk()
if chunk then
buf = buf..chunk
end
eof = not chunk
end
if buf == "" then return end
local space, word, other = buf:match("^(%s*)(%S+)(%s.*)")
if space then
buf = other
return space, word
elseif eof then
space, word = buf:match("(%s*)(.*)") -- will always match
buf = ""
return space, word
end
end
end
end
local function writeFile (pathname, content)
local fp = assert(io.open(pathname, "w"))
fp:write(content)
fp:close()
end
local todel = {}
local function cleanup ()
for _, pathname in ipairs(todel) do
win.DeleteFile(pathname)
end
todel = {}
end
local root = win.GetEnv("FARLOCALPROFILE").."\\"
local function tmpfile (filename, content)
local pathname = root..filename
writeFile(pathname, content)
table.insert(todel, pathname)
return pathname
end
local outputFile = "bito.md"
local outputFullPath = root..outputFile
local function openOutput (mode)
local CP = 65001
local curModal = bit64.band(actl.GetWindowInfo().Flags, F.WIF_MODAL)==F.WIF_MODAL
local opened
for i=actl.GetWindowCount(),1,-1 do
local wi = actl.GetWindowInfo(i)
if wi.Type==F.WTYPE_EDITOR and wi.Name==outputFullPath then
opened = true
if curModal then editor.Quit(wi.Id) end
break
end
end
if mode=="existing" then
if not (opened or win.GetFileAttr(outputFullPath)) then
mf.beep(); return
end
elseif not opened then
win.DeleteFile(outputFullPath)
end
local res
if not curModal then
local tryNotModal = F.EF_DISABLEHISTORY +F.EF_NONMODAL +F.EF_IMMEDIATERETURN +F.EF_OPENMODE_USEEXISTING
res = editor.Editor(outputFullPath, nil, nil, nil, nil, nil, tryNotModal, nil, nil, CP)
end
if curModal or res==F.EEC_LOADING_INTERRUPTED then
editor.Editor(outputFullPath, nil, nil, nil, nil, nil, F.EF_DISABLEHISTORY +F.EF_OPENMODE_NEWIFOPEN, nil, nil, CP)
end
end
local function isOpened (filename)
for i=1,actl.GetWindowCount() do
if actl.GetWindowInfo(i).Name==filename then
return true
end
end
end
local function check (key)
repeat
local k = win.ExtractKeyEx()
if k and far.InputRecordToName(k)==key then return true end
until not k
end
local _prompt = "Ask any technical question / use {{%code%}} as seltext placeholder"
local idInput = win.Uuid"58DD9ECD-CFFA-472E-BFD7-042295C86CAE"
local function bito (prompt)
prompt = prompt or far.InputBox(idInput, nfo.name, _prompt, "bito.ai prompt", nil, nil, nil, F.FIB_NONE)
if not prompt then return end
local ctx = Editor.SelValue
local cmd = ('"%s" -c "%s" -f "%s" -p "%s"'):format(
"bito.exe",
root.."ctx.bito",
tmpfile("file.bito", ctx=="" and " " or ctx),
tmpfile("prompt.bito", prompt)
)
if not isOpened(outputFullPath) then
win.DeleteFile(root.."ctx.bito")
end
far.Timer(0, function (t) -- workaround for https://bugs.farmanager.com/view.php?id=3044
t:Close()
local wi = actl.GetWindowInfo()
assert(wi.Type==F.WTYPE_EDITOR, "oops, editor has not been opened")
local Id = wi.Id
editor.SetTitle(Id, "Fetching response...")
local modal = bit64.band(F.WIF_MODAL, wi.Flags)~=0
local hDlg = not modal and progress("Waiting for data..")
if modal then
far.Message("Waiting for data..", "", "", "")
end
editor.UndoRedo(Id, F.EUR_BEGIN)
local ei = editor.GetInfo(Id)
local s = editor.GetString(Id, ei.TotalLines)
editor.SetPosition(Id, ei.TotalLines, s.StringLength+1)
local i = ei.TotalLines
if s.StringLength>0 then
editor.InsertString(Id)
editor.InsertString(Id)
i = i+2
editor.SetPosition(Id, i)
end
editor.InsertText(Id, "> "..prompt.."\n\n")
far.Text()
local pipe = io.popen(('"%s" 2>&1'):format(cmd), "r")
local function getchunkUtf8()
if check"Esc" then pipe:close(); return end
local chunk = pipe:read(5)
if chunk then
while not chunk:isvalid() do
local extra = pipe:read(1)
if extra then chunk = chunk..extra; else break; end
end
return chunk
end
pipe:close()
end
local start = Far.UpTime
--[[
for line in pipe:lines() do
editor.SetString(Id, i, line,"\n")
editor.InsertString(Id)
i = i+1
editor.Redraw(Id)
end
--]]
local linewrap = O.linewrap or ei.WindowSizeX-5
local autowrap = bit64.band(ei.Options, F.EOPT_AUTOINDENT)~=0
if autowrap then editor.SetParam(Id, F.ESPT_AUTOINDENT, 0) end
local code = false
for space,word in _words(getchunkUtf8) do
if start then
if hDlg then
hDlg:send(F.DM_SETTEXT, 2, "Streaming data..")
hDlg:send(F.DM_SETTEXT, 1, (math.ceil((Far.UpTime-start)/100)/10).." s")
end
start = false
cleanup()
end
editor.InsertText(Id, space)
if not code and editor.GetInfo(Id).CurPos + word:len() > linewrap then
editor.InsertText(Id, "\n")
end
editor.InsertText(Id, word)
editor.Redraw(Id)
local backticks = space:match"\n" and word:match("^```")
or space=="" and editor.GetString(Id,nil,3):match"^%s*```%S*$"
if backticks then code = not code end
end
if autowrap then editor.SetParam(Id, F.ESPT_AUTOINDENT, 1) end
editor.UndoRedo(Id, F.EUR_END)
editor.SaveFile(Id)
if hDlg then hDlg:send(F.DM_CLOSE) end
editor.SetTitle(Id, "bito.ai response:")
end)
openOutput()
end
if Macro then
Macro { description="Ask BitoAI";
area="Common"; key="CtrlB";
id="4AFE2367-4DAC-4A74-B1EE-9F14C42991CB";
action=function()
mf.acall(bito)
end;
}
Macro { description="Ask BitoAI: reopen output";
area="Common"; key="CtrlB:Double";
id="69EEB3AE-9EF3-4B51-B4A0-2585DFA30136";
action=function()
openOutput("existing")
end;
}
local codeStart,codeEnd = "^%s*```%S+$", "^%s*```$"
Macro { description="Ask BitoAI: copy code / paragraphs";
area="Editor"; key="CtrlShiftIns";
filemask=outputFile;
id="1EF69A5A-D048-42E9-BD35-7D1AE92329DE";
action=function()
if not Object.Selected then
local ei = editor.GetInfo()
local id = ei.EditorID
local start,finish
for i=ei.CurLine,1,-1 do
local line = editor.GetString(id,i,3)
if line:match(codeStart) then
start = i; break
elseif line:match(codeEnd) and i~=ei.CurLine then
return
end
end
if not start then return end
local from = ei.CurLine + (start==ei.CurLine and 1 or 0)
for i=from,ei.TotalLines do
local line = editor.GetString(id,i,3)
if line:match(codeEnd) then
finish = i; break
elseif line:match(codeStart) then -- error
return
end
end
if not finish then
return
end
editor.Select(id, F.BTYPE_STREAM, start+1, 1, 0, finish-start)
end
mf.beep()
far.CopyToClipboard((Editor.SelValue:gsub(" \r?\n"," ")))
end;
}
Macro { description="Ask BitoAI: unwrap text";
area="Editor"; key="AltF2";
filemask=outputFile;
id="A70AD88D-A68D-43DF-8D28-5E69547C7925";
action=function()
local ei = editor.GetInfo()
local id = ei.EditorID
local n = 0
for i=1, ei.TotalLines do
local line = editor.GetString(id,i,0)
if line.StringText:match"%S $" then
editor.SetString(id, i, line.StringText, "")
n = n+1
end
end
if n>0 and editor.SaveFile(id, ei.FileName) then --reload
local title = editor.GetTitle(id)
editor.Quit(id)
local EFLAGS = {EF_NONMODAL=1, EF_IMMEDIATERETURN=1, EF_DISABLEHISTORY=1}
editor.Editor(ei.FileName, title, nil,nil,nil,nil,EFLAGS,nil,nil,65001)
else
mf.beep()
end
end;
}
return
end
if _cmdline=="" then
sh.acall(bito)
elseif _cmdline then
bito(_cmdline)
else
return bito
end

Макрос для взаимодействия с ChatGPT в Фаре.

Назначен на Ctrl+B. Также может быть запущен в качестве скрипта LuaShell.

Ctrl+B:Double позволяет в любой момент открыть окно с выводом повторно.

Дополнительные макросы, активные в окне с ответом:

  • Ctrl+Shift+Ins:
    • скопировать выделенный текст, склеив свёрнутые строки обратно в параграфы;
    • при отсутствии выделения ищет и обрабатывает блок кода под курсором.
  • Alt+F2 полностью убрать форматирование (свёртку).

Установка

  • [s]модуль Execute, положить в modules.[/s]
  • bito.exe https://github.com/gitbito/CLI/blob/main/version-3.6/bito.exe, где угодно в %PATH% (или в %FARHOME%). До использования в макросе bito.exe необходимо (однократно) запустить, и последовать инструкциям для аутентификации. Внимание: файл для архитектуры x64.

Примечания:

  • Контекст чата (предыдущие вопросы и ответы) сохраняется, и таким образом можно продолжать чат.
  • Файл контекста хранится отдельно от ответа в редакторе, поэтому изменения в редакторе на него не влияют.
  • Контекст сохраняется до тех пор, пока открыто окно с ответом. Т.е. пока нужно продолжать чат, требуется держать его открытым, а когда требуется сменить тему, надо его закрыть.
  • Текст форматируется по заданной ширине, изменить можно в тексте скрипта, или через cfgscript/ScriptsBrowser. Блоки кода не форматируются.

[spoiler="Подробнее"]

  • Запрос через InputBox, ранее набранные приглашения доступны в истории поля ввода.
  • К запросу добавляется выделенный в редакторе текст (в конец, или через подстановку).
  • В качестве сервиса используется bito.ai, поскольку сервис бесплатен, и имеет собственное консольное приложение. В настоящий момент имеет лимит на 20 запросов в день.
    • Версии вплоть до 3.3 не требуют настройки ключей (вход через емейл).
    • Для более новых версий нужно сгенерировать access_key, после чего запустить bito.exe config -e, и вставить его в конфиг.
    • Версии новее чем 3.6 в начало вывода вставляют информацию об активной модели, поэтому не рекомендуются.

Примеры того что можно делать можно посмотреть на https://bito.ai/ Они ориентированы на работу с кодом, вот например стандартные шаблоны: [spoiler]

| Template Name       | Purpose                                                                               |
| -                   | -                                                                                     |
| Explain Code        | Explains what the code does and how it works.                                         |
| Generate Comment    | Generate a comment for the selected code                                              |
| Performance Check   | Checks code for the performance, and rewrites the code with suggested optimization.   |
| Security Check      | Check code for the basic security checks, and rewrites the code with suggested fixes. |
| Style Check         | Check the code for the common style issues, and rewrites with suggested fixes.        |
| Improve Readability | Refactor the code for better readability                                              |
| Clean Code          | Remove debug statements                                                               |
| Generate Unit Tests | Generate the unit tests for the selected code,                                        |

[/spoiler]

Однако на самом деле можно попросить его и текст перевести, и песню написать. И конечно же можно использовать русский язык (для верности настроить тут). Результат и его качество прямо зависит от того как построен запрос, об этом в сети информации более чем достаточно (начать можно например с https://docs.bito.ai/feature-guides/custom-prompt-templates) [/spoiler]

[spoiler=Код]https://gist.github.com/johnd0e/3269d681b8af5a4cc552ddfaf5fdf6b2[/spoiler]

Скачать: https://gist.github.com/johnd0e/3269d681b8af5a4cc552ddfaf5fdf6b2/archive/main.zip

[mod=John Doe]Разработка данного скрипта прекращена в пользу Ask AI: https://forum.farmanager.com/viewtopic.php?t=13447 [/mod]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment