Release v0.6.45: fix Far snippets and ship portable Utilities folder
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// Explicit Windows integration test. Opens an isolated Far profile and never
|
||||
// types into an existing Far window. Requires Far Manager at its default path.
|
||||
#include "app.h"
|
||||
#include <cstdlib>
|
||||
#include <cwctype>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
|
||||
namespace ks {
|
||||
struct InputRegressionAccess {
|
||||
static void runQueued(App &app) {
|
||||
std::function<void()> job;
|
||||
{ std::lock_guard lock(app.jobsMutex);
|
||||
if (app.jobs.empty()) throw std::runtime_error("snippet did not queue");
|
||||
job=std::move(app.jobs.front());app.jobs.pop_front(); }
|
||||
auto future=std::async(std::launch::async,[job=std::move(job)] {
|
||||
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);job();CoUninitialize();
|
||||
});
|
||||
while(future.wait_for(std::chrono::milliseconds(5))!=std::future_status::ready) {
|
||||
MSG message{};while(PeekMessageW(&message,nullptr,0,0,PM_REMOVE)) {
|
||||
TranslateMessage(&message);DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
future.get();app.replayTyping();
|
||||
}
|
||||
};
|
||||
}
|
||||
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int) {
|
||||
auto base=std::filesystem::path(ks::executable()).parent_path();
|
||||
std::ofstream log(base/L"far-snippets.log");
|
||||
int checks=0;
|
||||
auto require=[&](bool ok,const char *message) {
|
||||
if(!ok)throw std::runtime_error(message);
|
||||
++checks;log<<"PASS "<<message<<'\n';log.flush();
|
||||
};
|
||||
auto folder=std::filesystem::temp_directory_path()/(L"ks-far-snippets-"+ks::newSnippetId());
|
||||
std::filesystem::create_directories(folder);
|
||||
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
|
||||
ks::Clip saved;bool captured=false;PROCESS_INFORMATION child{};
|
||||
HANDLE input=INVALID_HANDLE_VALUE,output=INVALID_HANDLE_VALUE;
|
||||
HWND farWindow{};
|
||||
HWND owner=CreateWindowExW(0,L"STATIC",L"Far snippet fixture clipboard",0,0,0,0,0,HWND_MESSAGE,nullptr,instance,nullptr);
|
||||
auto originalForeground=GetForegroundWindow();
|
||||
int result=1;
|
||||
try {
|
||||
saved=ks::readClipboard(owner,128*1024*1024);captured=true;
|
||||
auto sentinel=ks::textClip(L"KSwitcher Far fixture clipboard");
|
||||
sentinel.formats.push_back({0,L"ExcludeClipboardContentFromMonitorProcessing",ks::Bytes{0}});
|
||||
ks::writeClipboard(owner,sentinel);
|
||||
const auto programFiles=_wgetenv(L"ProgramFiles");
|
||||
if(!programFiles)throw std::runtime_error("ProgramFiles is unavailable");
|
||||
auto farExecutable=std::filesystem::path(programFiles)/L"Far Manager/Far.exe";
|
||||
std::wstring command=L"\""+farExecutable.wstring()+L"\" -m -p -s \""+folder.wstring()+L"\\profile\" \""+folder.wstring()+L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
|
||||
require(CreateProcessW(nullptr,command.data(),nullptr,nullptr,FALSE,CREATE_NEW_CONSOLE,nullptr,folder.c_str(),&startup,&child),"start isolated Far without plugins/macros");
|
||||
CloseHandle(child.hThread);Sleep(1800);
|
||||
require(AttachConsole(child.dwProcessId),"attach isolated console for test-only input and observation");
|
||||
input=CreateFileW(L"CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
|
||||
output=CreateFileW(L"CONOUT$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
|
||||
require(input!=INVALID_HANDLE_VALUE && output!=INVALID_HANDLE_VALUE,"open isolated console buffers");
|
||||
auto target=GetConsoleWindow();farWindow=target;ShowWindow(target,SW_SHOW);
|
||||
require(ks::activateWindow(target),"activate only the isolated Far window");
|
||||
auto key=[&](WORD vk,wchar_t c=0,DWORD mods=0) {
|
||||
INPUT_RECORD records[2]{};
|
||||
for(int i=0;i<2;++i) {
|
||||
auto &r=records[i];r.EventType=KEY_EVENT;
|
||||
r.Event.KeyEvent={BOOL(i==0),1,vk,WORD(MapVirtualKeyW(vk,MAPVK_VK_TO_VSC)),{c},mods};
|
||||
}
|
||||
DWORD written{};
|
||||
if(!WriteConsoleInputW(input,records,2,&written)||written!=2)throw std::runtime_error("test input failed");
|
||||
Sleep(60);
|
||||
};
|
||||
auto position=[&] {CONSOLE_SCREEN_BUFFER_INFO info{};if(!GetConsoleScreenBufferInfo(output,&info))throw std::runtime_error("console position unavailable");return info.dwCursorPosition;};
|
||||
auto row=[&] {
|
||||
CONSOLE_SCREEN_BUFFER_INFO info{};GetConsoleScreenBufferInfo(output,&info);
|
||||
std::wstring text(info.dwSize.X,L' ');DWORD count{};
|
||||
if(!ReadConsoleOutputCharacterW(output,text.data(),DWORD(text.size()),{0,info.dwCursorPosition.Y},&count))throw std::runtime_error("console row unavailable");
|
||||
return text;
|
||||
};
|
||||
{
|
||||
ks::App app(instance,false,folder/L"data");app.window=owner;
|
||||
app.settings.history=false;app.settings.autoCorrect=false;app.settings.writeLog=true;
|
||||
auto prepare=[&](wchar_t last=L'r') {
|
||||
if(!ks::activateWindow(target))throw std::runtime_error("isolated Far lost foreground");
|
||||
key(VK_ESCAPE);key('Y',0,LEFT_CTRL_PRESSED);key(VK_OEM_1,L':',SHIFT_PRESSED);key(WORD(towupper(last)),last);
|
||||
app.run.reset();app.run.window=target;app.run.layout=ks::windowLayout(target);
|
||||
app.run.at=GetTickCount64();app.run.keys=L":r";
|
||||
};
|
||||
auto expand=[&](const std::wstring &replacement) {
|
||||
app.settings.snippets={{L":r",replacement}};
|
||||
app.transform(0,0);ks::InputRegressionAccess::runQueued(app);Sleep(120);
|
||||
};
|
||||
for(int panels=0;panels<2;++panels) {
|
||||
prepare();auto before=position();auto original=row();expand(L"rider .");
|
||||
auto expected=original;expected.replace(before.X-2,2,L"rider .");expected.resize(original.size());
|
||||
// Far's rightmost scroll indicator is redrawn rather than shifted.
|
||||
require(row().substr(0,before.X+5)==expected.substr(0,before.X+5),"actual snippet replaces :r with rider .");
|
||||
require(position().X==before.X+5,"successful expansion leaves cursor after replacement");
|
||||
require(ks::readClipboard(owner).text()==sentinel.text(),"snippet restores original clipboard");
|
||||
prepare(L'x');before=position();original=row();expand(L"rider .{enter}");
|
||||
require(row()==original && position().X==before.X,"mismatched trigger restores original caret and never executes Enter");
|
||||
prepare();before=position();original=row();expand(L"left{cursor}right");
|
||||
require(row().substr(before.X-2,9)==L"leftright" && position().X==before.X+2,"cursor marker uses Far selection semantics");
|
||||
prepare();before=position();original=row();expand(L"first\nsecond{enter}");
|
||||
require(row()==original && position().X==before.X,"multiline console template rejected before changing text or caret");
|
||||
prepare();before=position();original=row();expand(L"echo KSWITCHER_FAR_FIXTURE{enter}");
|
||||
for(int wait=0;wait<30 && position().X!=before.X-2;++wait)Sleep(100);
|
||||
require(position().X==before.X-2 && row().substr(before.X-2,2)==L" ",
|
||||
"verified Enter runs only the harmless fixture echo and returns to an empty command line");
|
||||
key('O',0,LEFT_CTRL_PRESSED);
|
||||
}
|
||||
app.window=nullptr;
|
||||
}
|
||||
log<<"Passed "<<checks<<" Far snippet integration checks\n";result=0;
|
||||
}catch(const std::exception &e){log<<"FAIL "<<e.what()<<" win32="<<GetLastError()<<'\n';}
|
||||
// A real user or clipboard synchronizer can copy during this explicit test.
|
||||
// Never replace that newer content with the fixture's original snapshot.
|
||||
const auto restoreSequence=GetClipboardSequenceNumber();
|
||||
const bool restoreClipboard=GetClipboardOwner()==owner || (farWindow && ks::ownsClipboard(farWindow));
|
||||
if(input!=INVALID_HANDLE_VALUE)CloseHandle(input);
|
||||
if(output!=INVALID_HANDLE_VALUE)CloseHandle(output);
|
||||
FreeConsole();
|
||||
if(child.hProcess){TerminateProcess(child.hProcess,0);WaitForSingleObject(child.hProcess,5000);CloseHandle(child.hProcess);}
|
||||
if(captured && restoreClipboard && GetClipboardSequenceNumber()==restoreSequence)
|
||||
try{ks::writeClipboard(owner,saved);}catch(...){result=1;}
|
||||
DestroyWindow(owner);
|
||||
if(IsWindow(originalForeground))ks::activateWindow(originalForeground);
|
||||
CoUninitialize();return result;
|
||||
}
|
||||
Reference in New Issue
Block a user