/*
cmdline.cpp

Командная строка
*/
/*
Copyright (c) 1996 Eugene Roshal
Copyright (c) 2000 Far Group
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the authors may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#include "headers.hpp"

#include "cmdline.hpp"
#include "execute.hpp"
#include "macroopcode.hpp"
#include "keys.hpp"
#include "lang.hpp"
#include "ctrlobj.hpp"
#include "manager.hpp"
#include "message.hpp"
#include "history.hpp"
#include "filepanels.hpp"
#include "panel.hpp"
#include "foldtree.hpp"
#include "treelist.hpp"
#include "fileview.hpp"
#include "fileedit.hpp"
#include "rdrwdsk.hpp"
#include "savescr.hpp"
#include "scrbuf.hpp"
#include "interf.hpp"
#include "syslog.hpp"
#include "config.hpp"
#include "ConfigOptEdit.hpp"
#include "ConfigOptSaveLoad.hpp"
#include "usermenu.hpp"
#include "datetime.hpp"
#include "pathmix.hpp"
#include "dirmix.hpp"
#include "strmix.hpp"
#include "keyboard.hpp"
#include "vmenu.hpp"
#include "CachedCreds.hpp"
#include "exitcode.hpp"
#include "GitTools.hpp"
#include "vtlog.h"
#include "vtshell.h"
#include "vtcompletor.h"
#include "Environment.h"
#include "WideMB.h"
#include "clipboard.hpp"
#include "dialog.hpp"
#include <limits>

namespace
{
void NormalizeMultilineForExec(FARString &text)
{
	const wchar_t *src = text.CPtr();
	size_t len = text.GetLength();
	FARString out;
	out.Reserve(len);

	for (size_t i = 0; i < len; ++i) {
		if (src[i] == L'\r') {
			if (i + 1 < len && src[i + 1] == L'\n')
				continue;
			out.Append(L'\n');
		} else {
			out.Append(src[i]);
		}
	}

	text = out;
}

namespace {
enum
{
	MP_BOX,
	MP_MEMO,
	MP_SEPARATOR,
	MP_BTN_CANCEL,
	MP_BTN_EXEC,
	MP_BTN_EXEC_NOASK
};

struct CmdlinePasteDlgLayout
{
	int min_width;
	int min_height;
};

static void CalcCmdlinePasteDialogLayout(const CmdlinePasteDlgLayout &layout, int &dlg_w, int &dlg_h, int &dlg_x,
		int &dlg_y)
{
	dlg_w = Max(layout.min_width, Min(ScrX - 2, Max(76, (ScrX * 3) / 4)));
	dlg_h = Max(layout.min_height, Min(ScrY - 2, Max(20, (ScrY * 2) / 3)));
	dlg_x = Max(0, (ScrX - dlg_w) / 2);
	dlg_y = Max(0, (ScrY - dlg_h) / 2);
}

static INT_PTR WINAPI CmdlinePasteDlgProc(HANDLE hDlg, int Msg, int Param1, LONG_PTR Param2)
{
	if (Msg == DN_RESIZECONSOLE) {
		auto *layout = reinterpret_cast<CmdlinePasteDlgLayout *>(SendDlgMessage(hDlg, DM_GETDLGDATA, 0, 0));
		if (!layout)
			return DefDlgProc(hDlg, Msg, Param1, Param2);

		int dlg_w = 0;
		int dlg_h = 0;
		int dlg_x = 0;
		int dlg_y = 0;
		CalcCmdlinePasteDialogLayout(*layout, dlg_w, dlg_h, dlg_x, dlg_y);

		SendDlgMessage(hDlg, DM_ENABLEREDRAW, FALSE, 0);

		COORD size = {(SHORT)dlg_w, (SHORT)dlg_h};
		SendDlgMessage(hDlg, DM_RESIZEDIALOG, 0, reinterpret_cast<LONG_PTR>(&size));

		COORD pos = {(SHORT)dlg_x, (SHORT)dlg_y};
		SendDlgMessage(hDlg, DM_MOVEDIALOG, TRUE, reinterpret_cast<LONG_PTR>(&pos));

		SMALL_RECT rect;
		rect.Left = 3;
		rect.Top = 1;
		rect.Right = (SHORT)(dlg_w - 4);
		rect.Bottom = (SHORT)(dlg_h - 2);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_BOX, reinterpret_cast<LONG_PTR>(&rect));

		rect.Left = 5;
		rect.Top = 2;
		rect.Right = (SHORT)(dlg_w - 6);
		rect.Bottom = (SHORT)(dlg_h - 5);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_MEMO, reinterpret_cast<LONG_PTR>(&rect));

		rect.Left = 0;
		rect.Right = 0;
		rect.Top = (SHORT)(dlg_h - 5);
		rect.Bottom = (SHORT)(dlg_h - 5);
		SendDlgMessage(hDlg, DM_GETITEMPOSITION, MP_SEPARATOR, reinterpret_cast<LONG_PTR>(&rect));
		rect.Top = (SHORT)(dlg_h - 4);
		rect.Bottom = (SHORT)(dlg_h - 4);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_SEPARATOR, reinterpret_cast<LONG_PTR>(&rect));

		SendDlgMessage(hDlg, DM_GETITEMPOSITION, MP_BTN_CANCEL, reinterpret_cast<LONG_PTR>(&rect));
		rect.Top = (SHORT)(dlg_h - 3);
		rect.Bottom = (SHORT)(dlg_h - 3);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_BTN_CANCEL, reinterpret_cast<LONG_PTR>(&rect));
		SendDlgMessage(hDlg, DM_GETITEMPOSITION, MP_BTN_EXEC, reinterpret_cast<LONG_PTR>(&rect));
		rect.Top = (SHORT)(dlg_h - 3);
		rect.Bottom = (SHORT)(dlg_h - 3);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_BTN_EXEC, reinterpret_cast<LONG_PTR>(&rect));
		SendDlgMessage(hDlg, DM_GETITEMPOSITION, MP_BTN_EXEC_NOASK, reinterpret_cast<LONG_PTR>(&rect));
		rect.Top = (SHORT)(dlg_h - 3);
		rect.Bottom = (SHORT)(dlg_h - 3);
		SendDlgMessage(hDlg, DM_SETITEMPOSITION, MP_BTN_EXEC_NOASK, reinterpret_cast<LONG_PTR>(&rect));

		SendDlgMessage(hDlg, DM_ENABLEREDRAW, TRUE, 0);
		return TRUE;
	}

	return DefDlgProc(hDlg, Msg, Param1, Param2);
}
} // namespace

int ShowMultilinePasteDialog(FARString &text)
{
	static const wchar_t kCmdlineMemoFilename[] = L"cmdline.bash";
	const int min_width = 40;
	const int min_height = 12;
	const int dlg_w = Max(min_width, Min(ScrX - 2, Max(76, (ScrX * 3) / 4)));
	const int dlg_h = Max(min_height, Min(ScrY - 2, Max(20, (ScrY * 2) / 3)));
	const int sep_y = dlg_h - 4;
	const int btn_y = dlg_h - 3;
//	const int dlg_w = Max(min_width, Min(ScrX - 2, 76));
//	const int dlg_h = Max(min_height, Min(ScrY - 2, 20));

	DialogDataEx DlgData[] = {
		{DI_DOUBLEBOX, 3, 1, (short)(dlg_w - 4), (short)(dlg_h - 2), {}, 0, Msg::MultilinePaste},
		{DI_MEMOEDIT,  5, 2, (short)(dlg_w - 6), (short)(dlg_h - 5), {}, DIF_FOCUS, L""},
		{DI_TEXT,      0, (short)sep_y, 0, (short)sep_y, {}, DIF_SEPARATOR, L""},
		{DI_BUTTON,    0, (short)btn_y, 0, (short)btn_y, {}, DIF_CENTERGROUP, Msg::HCancel},
		{DI_BUTTON,    0, (short)btn_y, 0, (short)btn_y, {}, DIF_CENTERGROUP | DIF_DEFAULT, Msg::HExecute},
		{DI_BUTTON,    0, (short)btn_y, 0, (short)btn_y, {}, DIF_CENTERGROUP, Msg::HExecuteNoAsk}
	};

	MakeDialogItemsEx(DlgData, DlgItems);
	DlgItems[MP_MEMO].strData = text;
	DlgItems[MP_MEMO].UserData = (DWORD_PTR)kCmdlineMemoFilename;

	CmdlinePasteDlgLayout layout = {min_width, min_height};
	Dialog Dlg(DlgItems, ARRAYSIZE(DlgItems), CmdlinePasteDlgProc, reinterpret_cast<LONG_PTR>(&layout));
	Dlg.SetPosition(-1, -1, dlg_w, dlg_h);
	Dlg.Process();

	int exit_code = Dlg.GetExitCode();
	if (exit_code == MP_BTN_EXEC || exit_code == MP_BTN_EXEC_NOASK) {
		int len = (int)SendDlgMessage((HANDLE)&Dlg, DM_GETTEXTLENGTH, MP_MEMO, 0);
		if (len > 0) {
			FARString edited;
			wchar_t *buf = edited.GetBuffer(len + 1);
			FarDialogItemData data = {(size_t)len, buf};
			SendDlgMessage((HANDLE)&Dlg, DM_GETTEXT, MP_MEMO, (LONG_PTR)&data);
			edited.ReleaseBuffer(len);
			text = edited;
		} else {
			text = DlgItems[MP_MEMO].strData;
		}
		NormalizeMultilineForExec(text);
		RemoveTrailingSpaces(text);
		return (exit_code == MP_BTN_EXEC) ? 1 : 2;
	}

	return 0;
}
} // namespace

CommandLine::CommandLine()
	:
	CmdStr(CtrlObject->Cp(), CtrlObject->CmdHistory, 0,
			(Opt.CmdLine.AutoComplete ? EditControl::EC_ENABLEAUTOCOMPLETE : 0)
					| EditControl::EC_ENABLEFNCOMPLETE
					| EditControl::EC_ENABLEFNCOMPLETE_ESCAPED),
//	BackgroundScreen(nullptr),
	LastCmdPartLength(-1),
	PushDirStackSize(0),
	m_multilineExtraLines(0),
	m_multilineActiveLine(-1)
{
	CmdStr.SetEditBeyondEnd(FALSE);
	SetPersistentBlocks(Opt.CmdLine.EditBlock);
	SetDelRemovesBlocks(Opt.CmdLine.DelRemovesBlocks);
}

CommandLine::~CommandLine()
{
	BackgroundConsole.Discard(); // dont show it
}

void CommandLine::SetVisible(bool Visible)
{
	ScreenObject::SetVisible(Visible);
	bool CurVisible;
	DWORD CurSize;
	GetCursorType(CurVisible, CurSize);
	SetCursorType(Visible, CurSize);
}

void CommandLine::SetPersistentBlocks(int Mode)
{
	CmdStr.SetPersistentBlocks(Mode);
}

void CommandLine::SetDelRemovesBlocks(int Mode)
{
	CmdStr.SetDelRemovesBlocks(Mode);
}

void CommandLine::SetAutoComplete(int Mode)
{
	if (Mode) {
		CmdStr.EnableAC(true);
	} else {
		CmdStr.DisableAC(true);
	}
}

void CommandLine::DisplayObject()
{
	_OT(SysLog(L"[%p] CommandLine::DisplayObject()", this));
	if (!IsVisible())
		return;

	const bool has_multiline = !m_multilineLines.empty();
	const int input_y = Y2;
	if (has_multiline) {
		int view_lines = 0;
		const int view_top = GetMultilineViewTop(view_lines);
		const int view_y = input_y - view_lines + 1;
		for (int i = 0; i < view_lines; ++i) {
			const int line_index = view_top + i;
			const int y = view_y + i;
			FARString prompt;
			GetMultilinePrompt(line_index, prompt);
			const int prompt_len = (int)prompt.CellsCount();
			GotoXY(X1, y);
			SetFarColor(COL_COMMANDLINEPREFIX);
			Text(prompt);
			FARString line;
			if (line_index != m_multilineActiveLine)
				line = m_multilineLines[line_index];
			line.FitToCells(std::max(0, X2 - (X1 + prompt_len) + 1));
			GotoXY(X1 + prompt_len, y);
			SetFarColor(COL_COMMANDLINE);
			Text(line);
			if (y != input_y) {
				GotoXY(X2 + 1, y);
				Text(L" ");
			}
		}
		FARString active_prompt;
		GetMultilinePrompt(m_multilineActiveLine, active_prompt);
		const int active_y = view_y + m_multilineActiveLine - view_top;
		CmdStr.SetObjectColor(FarColorToReal(COL_COMMANDLINE), FarColorToReal(COL_COMMANDLINESELECTED));
		CmdStr.SetPosition(X1 + (int)active_prompt.CellsCount(), active_y, X2, active_y);
	} else {
		FARString prompt;
		GetPrompt(prompt);
		TruncPathStr(prompt, (X2 - X1) / 2);
		GotoXY(X1, input_y);
		SetFarColor(COL_COMMANDLINEPREFIX);
		Text(prompt);
		CmdStr.SetObjectColor(FarColorToReal(COL_COMMANDLINE), FarColorToReal(COL_COMMANDLINESELECTED));
		CmdStr.SetPosition(X1 + (int)prompt.CellsCount(), input_y, X2, input_y);
	}

	CmdStr.Show();

	DrawComboBoxMark(0x2191, input_y);
}

void CommandLine::DrawComboBoxMark(wchar_t MarkChar, int y)
{
	wchar_t MarkWz[2] = {MarkChar, 0};
	GotoXY(X2 + 1, y);
	SetFarColor(COL_COMMANDLINEPREFIX);
	Text(MarkWz);
}

void CommandLine::SetCurPos(int Pos, int LeftPos)
{
	if (!m_multilineLines.empty()) {
		SyncActiveMultilineLine();
		int remaining = std::max(0, Pos);
		for (int i = 0; i < (int)m_multilineLines.size(); ++i) {
			const int length = (int)m_multilineLines[i].GetLength();
			if (remaining <= length || i + 1 == (int)m_multilineLines.size()) {
				if (i == m_multilineActiveLine)
					CmdStr.SetCurPos(std::min(remaining, length));
				else
					SetActiveMultilineLine(i, std::min(remaining, length));
				CmdStr.SetLeftPos(LeftPos);
				Show();
				return;
			}
			remaining-= length + 1;
		}
	}
	CmdStr.SetLeftPos(LeftPos);
	CmdStr.SetCurPos(Pos);
	CmdStr.Redraw();
}

int CommandLine::GetCurPos()
{
	if (m_multilineLines.empty())
		return CmdStr.GetCurPos();
	return GetMultilineLineOffset(m_multilineActiveLine) + CmdStr.GetCurPos();
}

bool CommandLine::IsNotEmpty() const
{
	if (CmdStr.CalcRTrimmedStrSize() > 0)
		return true;

	for (int i = 0; i < (int)m_multilineLines.size(); ++i) {
		if (i == m_multilineActiveLine)
			continue;
		FARString line = m_multilineLines[i];
		RemoveTrailingSpaces(line);
		if (!line.IsEmpty())
			return true;
	}

	return false;
}

bool CommandLine::IsContinuationLine(const FARString &line) const
{
	size_t backslashes = 0;
	for (size_t i = line.GetLength(); i > 0 && line[i - 1] == L'\\'; --i)
		++backslashes;
	return (backslashes & 1) != 0;
}

bool CommandLine::AddHereDocDelimiters(const FARString &line)
{
	bool added = false;
	bool in_single_quotes = false;
	bool in_double_quotes = false;
	bool escaped = false;
	int arithmetic_depth = 0;
	int conditional_depth = 0;
	const size_t length = line.GetLength();

	for (size_t i = 0; i < length; ++i) {
		const wchar_t ch = line[i];
		if (escaped) {
			escaped = false;
			continue;
		}
		if (!in_single_quotes && !in_double_quotes) {
			if (arithmetic_depth) {
				if (ch == L'(' && i + 1 < length && line[i + 1] == L'(') {
					++arithmetic_depth;
					++i;
				} else if (ch == L')' && i + 1 < length && line[i + 1] == L')') {
					--arithmetic_depth;
					++i;
				}
				continue;
			}
			if (conditional_depth) {
				if (ch == L']' && i + 1 < length && line[i + 1] == L']') {
					--conditional_depth;
					++i;
				}
				continue;
			}
			if (ch == L'(' && i + 1 < length && line[i + 1] == L'(') {
				++arithmetic_depth;
				++i;
				continue;
			}
			if (ch == L'[' && i + 1 < length && line[i + 1] == L'[') {
				++conditional_depth;
				++i;
				continue;
			}
			if (ch == L'#' && (i == 0 || IsSpace(line[i - 1])))
				break;
		}
		if (ch == L'\\' && !in_single_quotes) {
			escaped = true;
			continue;
		}
		if (ch == L'\'' && !in_double_quotes) {
			in_single_quotes = !in_single_quotes;
			continue;
		}
		if (ch == L'"' && !in_single_quotes) {
			in_double_quotes = !in_double_quotes;
			continue;
		}
		if (in_single_quotes || in_double_quotes || ch != L'<' || i + 1 >= length || line[i + 1] != L'<')
			continue;

		size_t pos = i + 2;
		if (pos < length && line[pos] == L'<') { // here-string (<<<), not a heredoc
			i = pos;
			continue;
		}

		HereDocDelimiter delimiter;
		delimiter.strip_tabs = pos < length && line[pos] == L'-';
		if (delimiter.strip_tabs)
			++pos;
		while (pos < length && IsSpace(line[pos]))
			++pos;
		if (pos >= length)
			break;

		if (line[pos] == L'\'' || line[pos] == L'"') {
			const wchar_t quote = line[pos++];
			while (pos < length && line[pos] != quote)
				delimiter.text.Append(line[pos++]);
			if (pos == length || delimiter.text.IsEmpty())
				break;
			++pos;
		} else {
			while (pos < length) {
				const wchar_t word_ch = line[pos];
				if (IsSpace(word_ch) || wcschr(L";|&<>()", word_ch))
					break;
				if (word_ch == L'\\' && pos + 1 < length)
					++pos;
				delimiter.text.Append(line[pos++]);
			}
			if (delimiter.text.IsEmpty())
				continue;
		}

		m_hereDocDelimiters.emplace_back(delimiter);
		added = true;
		i = pos ? pos - 1 : pos;
	}

	return added;
}

bool CommandLine::IsHereDocTerminator(const FARString &line) const
{
	if (m_hereDocDelimiters.empty())
		return false;

	size_t start = 0;
	if (m_hereDocDelimiters.front().strip_tabs)
		while (start < line.GetLength() && line[start] == L'\t')
			++start;
	const FARString &delimiter = m_hereDocDelimiters.front().text;
	return line.Equal(start, line.GetLength() - start, delimiter.CPtr(), delimiter.GetLength());
}

FARString CommandLine::BuildMultilineCommand(const FARString &line) const
{
	FARString result;
	const int count = (int)m_multilineLines.size();
	const int active = (m_multilineActiveLine >= 0 && m_multilineActiveLine < count)
		? m_multilineActiveLine
		: (count > 0 ? count - 1 : -1);
	for (int i = 0; i < count; ++i) {
		if (i == active)
			result+= line;
		else
			result+= m_multilineLines[i];
		if (i + 1 < count)
			result+= L'\n';
	}
	if (count == 0)
		result = line;
	return result;
}

void CommandLine::ApplyMultilineText(const FARString &text, BOOL Redraw)
{
	FARString normalized(text);
	NormalizeMultilineForExec(normalized);
	m_multilineLines.clear();
	m_hereDocDelimiters.clear();
	m_multilineActiveLine = -1;
	FARString current;
	for (size_t i = 0; i < normalized.GetLength(); ++i) {
		wchar_t ch = normalized[i];
		if (ch == L'\n') {
			m_multilineLines.emplace_back(current);
			current.Clear();
		} else {
			current.Append(ch);
		}
	}
	m_multilineLines.emplace_back(current);
	if (m_multilineLines.size() <= 1) {
		CmdStr.SetString(m_multilineLines[0].CPtr());
		ClearMultilineState();
		if (Redraw)
			Show();
		return;
	}
	m_multilineActiveLine = (int)m_multilineLines.size() - 1;
	CmdStr.SetString(m_multilineLines[m_multilineActiveLine].CPtr());
	UpdateMultilineLayout();
	if (Redraw)
		Show();
}

void CommandLine::ClearMultilineState()
{
	m_multilineLines.clear();
	m_hereDocDelimiters.clear();
	m_multilineActiveLine = -1;
	SetMultilineExtraLines(0);
}

void CommandLine::SetMultilineExtraLines(int extra_lines)
{
	const int new_extra_lines = std::max(0, std::min(extra_lines, GetMaxVisibleMultilineLines() - 1));
	if (new_extra_lines == m_multilineExtraLines)
		return;
	m_multilineExtraLines = new_extra_lines;
	if (CtrlObject && CtrlObject->Cp()) {
		auto *cp = CtrlObject->Cp();
		cp->SetPanelPositions(cp->LeftPanel->IsFullScreen(), cp->RightPanel->IsFullScreen(), Opt.PanelsDisposition);
	}
}

int CommandLine::GetMaxVisibleMultilineLines() const
{
	constexpr int min_panel_height = 6;
	const int keybar = Opt.ShowKeyBar ? 1 : 0;
	int available_extra = 0;

	if (!Opt.PanelsDisposition) {
		const int panel_top = Opt.ShowMenuBar ? 1 : 0;
		const int panel_bottom = ScrY - 1 - keybar;
		available_extra = panel_bottom - panel_top + 1 - min_panel_height;
	} else {
		const int left_bottom = (ScrY - (Opt.ShowMenuBar ? 1 : 0)) / 2 - keybar
				- Opt.LeftHeightDecrement / 2;
		const int right_top = left_bottom + 1 - Opt.WidthDecrement;
		const int right_bottom = ScrY - keybar - 1;
		available_extra = right_bottom - right_top + 1 - min_panel_height;
	}

	return std::max(1, std::max(0, available_extra) + 1);
}

int CommandLine::GetExtraLines() const
{
	return std::min(m_multilineExtraLines, GetMaxVisibleMultilineLines() - 1);
}

int CommandLine::GetMultilineLineOffset(int line) const
{
	int offset = 0;
	for (int i = 0; i < line && i < (int)m_multilineLines.size(); ++i)
		offset+= (int)m_multilineLines[i].GetLength() + 1;
	return offset;
}

int CommandLine::GetMultilineViewTop(int &visible_lines) const
{
	const int total_lines = (int)m_multilineLines.size();
	visible_lines = std::min(total_lines, std::max(1, Y2 - Y1 + 1));
	const int max_top = std::max(0, total_lines - visible_lines);
	return std::min(std::max(0, m_multilineActiveLine - visible_lines + 1), max_top);
}

void CommandLine::GetMultilinePrompt(int line, FARString &prompt)
{
	if (line == 0) {
		GetPrompt(prompt);
		TruncPathStr(prompt, (X2 - X1) / 2);
	} else {
		prompt = line == m_multilineActiveLine ? L"> " : L": ";
	}
}

void CommandLine::SyncActiveMultilineLine()
{
	if (m_multilineActiveLine >= 0 && m_multilineActiveLine < (int)m_multilineLines.size()) {
		FARString current;
		CmdStr.GetString(current);
		m_multilineLines[m_multilineActiveLine] = current;
	}
}

void CommandLine::UpdateMultilineLayout()
{
	const int count = (int)m_multilineLines.size();
	if (count <= 1) {
		if (count == 1)
			CmdStr.SetString(m_multilineLines[0].CPtr());
		ClearMultilineState();
		return;
	}
	if (m_multilineActiveLine < 0 || m_multilineActiveLine >= count)
		m_multilineActiveLine = count - 1;
	SetMultilineExtraLines(std::min(count, GetMaxVisibleMultilineLines()) - 1);
}

void CommandLine::ContinueMultilineInput(const FARString &line)
{
	if (m_multilineLines.empty()) {
		m_multilineLines.emplace_back(line);
		m_multilineActiveLine = 0;
	} else {
		m_multilineLines[m_multilineActiveLine] = line;
	}

	const int insert_pos = m_multilineActiveLine + 1;
	m_multilineLines.insert(m_multilineLines.begin() + insert_pos, FARString());
	m_multilineActiveLine = insert_pos;
	CmdStr.SetString(L"", FALSE);
	UpdateMultilineLayout();
	Show();
}

void CommandLine::SetActiveMultilineLine(int line, int cursor_pos)
{
	if (line < 0 || line >= (int)m_multilineLines.size())
		return;
	SyncActiveMultilineLine();
	m_multilineActiveLine = line;
	CmdStr.DisableAC();
	CmdStr.SetString(m_multilineLines[line].CPtr());
	CmdStr.SetLeftPos(0);
	CmdStr.Select(-1, 0);
	CmdStr.SetCurPos(std::max(0, std::min(cursor_pos, CmdStr.GetLength())));
	CmdStr.RevertAC();
}

bool CommandLine::MoveMultilineLine(int delta)
{
	if (m_multilineLines.empty())
		return false;
	const int count = (int)m_multilineLines.size();
	const int next = m_multilineActiveLine + delta;
	if (next < 0 || next >= count)
		return true;

	const int desired_pos = CmdStr.GetCurPos();
	SetActiveMultilineLine(next, desired_pos);
	Show();
	return true;
}

int64_t CommandLine::VMProcess(MacroOpcode OpCode, void *vParam, int64_t iParam)
{
	if (OpCode >= MCODE_C_CMDLINE_BOF && OpCode <= MCODE_C_CMDLINE_SELECTED)
		OpCode = static_cast<MacroOpcode>(OpCode - MCODE_C_CMDLINE_BOF + MCODE_C_BOF);

	if (OpCode >= MCODE_C_BOF && OpCode <= MCODE_C_SELECTED) {
		switch (OpCode) {
			case MCODE_C_EMPTY:
				return !IsNotEmpty();
			case MCODE_C_BOF:
				return GetCurPos() == 0;
			case MCODE_C_EOF: {
				FARString command;
				GetString(command);
				return GetCurPos() >= (int)command.GetLength();
			}
			default:
				return CmdStr.VMProcess(OpCode, vParam, iParam);
		}
	}

	if (OpCode == MCODE_V_CMDLINE_ITEMCOUNT || OpCode == MCODE_V_CMDLINE_CURPOS)
		OpCode = static_cast<MacroOpcode>(OpCode - MCODE_V_CMDLINE_ITEMCOUNT + MCODE_V_ITEMCOUNT);

	if (OpCode == MCODE_V_ITEMCOUNT) {
		FARString command;
		GetString(command);
		return command.GetLength();
	}
	if (OpCode == MCODE_V_CURPOS)
		return GetCurPos() + 1;

	if (OpCode == MCODE_F_EDITOR_SEL) {
		int64_t result = CmdStr.VMProcess(MCODE_F_EDITOR_SEL, vParam, iParam);
		const int action = (int)((INT_PTR)vParam);
		if (!m_multilineLines.empty() && action == 0 && result > 0 && (iParam == 1 || iParam == 3))
			result+= GetMultilineLineOffset(m_multilineActiveLine);
		return result;
	}

	return 0;
}

void CommandLine::ProcessTabCompletion()
{
	if (CtrlObject->Cp()->ActivePanel->GetMode() == PLUGIN_PANEL)
		  return;	// silent workaround for https://github.com/elfmz/far2l/issues/3485

	FARString strStr;
	CmdStr.GetString(strStr);
	// show all possibilities on double tab on same input string
	const bool possibilities = !strLastCompletionCmdStr.IsEmpty() && strStr == strLastCompletionCmdStr;
	strLastCompletionCmdStr = strStr;

	if (!strStr.IsEmpty()) {
		std::string cmd = strStr.GetMB();
		VTCompletor vtc;
		if (possibilities) {
			std::vector<std::string> possibilities;
			if (vtc.GetPossibilities(cmd, possibilities) && !possibilities.empty()) {
				// fprintf(stderr, "Possibilities: ");
				CmdStr.ShowCustomCompletionList(possibilities);
			}
		} else {
			if (vtc.ExpandCommand(cmd)) {
				strStr = cmd;
				CmdStr.SetString(strStr);
				CmdStr.Show();
			}
		}
	}
}

std::string CommandLine::GetConsoleLog(HANDLE con_hnd, bool colored)
{
	bool vtshell_busy = VTShell_Busy();
	if (!vtshell_busy) {
		++ProcessShowClock;
		ShowBackground();
		Redraw();
		ScrBuf.Flush();
	}
	const std::string &histfile = VTLog::GetAsFile(con_hnd, colored);
	if (!vtshell_busy) {
		--ProcessShowClock;
		Redraw();
		ScrBuf.Flush();
	}
	return histfile;
}

void CommandLine::ChangeDirFromHistory(bool PluginPath, int SelectType, FARString strDir, FARString strFile)
{
	if (SelectType == 2)
		CtrlObject->FolderHistory->SetAddMode(false, HISTORY_REMOVE_DUPS_CASE_INSENSITIVE, true);

	// пусть плагин сам прыгает... ;-)
	Panel *Panel = CtrlObject->Cp()->ActivePanel;

	if (SelectType == 6)
		Panel = CtrlObject->Cp()->GetAnotherPanel(Panel);

	if (!PluginPath || !CtrlObject->Plugins.ProcessCommandLine(strDir, Panel, true)) {
		if (Panel->GetMode() == PLUGIN_PANEL || CheckShortcutFolder(strDir, false)) {
			Panel->SetCurDir(strDir, PluginPath ? FALSE : TRUE);
			//fprintf(stderr, "=== ChangeDirFromHistory():\n  strDir=\"%ls\"\n  strFile=\"%ls\"\n", strDir.CPtr(), strFile.CPtr());
			if ( !strFile.IsEmpty() && !strFile.Contains(LGOOD_SLASH) ) // only local file, not in another directory
				Panel->GoToFile(strFile);
			// restore current directory to active panel path
			if (SelectType == 6) {
				CtrlObject->Cp()->ActivePanel->SetCurPath();
			}
			Panel->Redraw();
			CtrlObject->FolderHistory->SetAddMode(true, HISTORY_REMOVE_DUPS_CASE_INSENSITIVE, true);
		}
	}
}

void CommandLine::ProcessKey_ClearTerminalHistory()
{
	if (!Opt.Confirm.ClearVT || Message(MSG_WARNING, 2,
			Msg::ClearTerminalTitle, Msg::ClearTerminalQuestion, Msg::Ok, Msg::Cancel) == 0) {
		ClearScreen(FarColorToReal(COL_COMMANDLINEUSERSCREEN));
		SaveBackground();
		VTLog::Reset(NULL);
		ShowBackground();
		Redraw();
		//		ShellUpdatePanels(CtrlObject->Cp()->ActivePanel, FALSE);
		CtrlObject->MainKeyBar->Refresh(Opt.ShowKeyBar);
		//		CmdExecute(L"reset", true, false, true, false, false, false);
	}
}

void CommandLine::ProcessKey_ShowFolderTree()
{
	Panel *ActivePanel = CtrlObject->Cp()->ActivePanel;
	// TODO: здесь можно добавить проверку, что мы в корне диска и отсутствие файла Tree.Far...
	FARString strStr;
	FolderTree::Present(strStr, MODALTREE_ACTIVE, TRUE, FALSE);
	CtrlObject->Cp()->RedrawKeyBar();

	if (!strStr.IsEmpty()) {
		ActivePanel->SetCurDir(strStr, TRUE);
		ActivePanel->Show();
		if (ActivePanel->GetType() == TREE_PANEL)
			ActivePanel->ProcessKey(KEY_ENTER);
	} else {
		// TODO: ... а здесь проверить факт изменения/появления файла Tree.Far и мы опять же в корне (чтобы лишний раз не апдейтить панель)
		ActivePanel->Update(UPDATE_KEEP_SELECTION);
		ActivePanel->Redraw();
		Panel *AnotherPanel = CtrlObject->Cp()->GetAnotherPanel(ActivePanel);
		if (AnotherPanel->NeedUpdatePanel(ActivePanel)) {
			AnotherPanel->Update(UPDATE_KEEP_SELECTION);	//|UPDATE_SECONDARY);
			AnotherPanel->Redraw();
		}
	}
}

void CommandLine::ProcessKey_ShowFolderHistory()
{
	FARString strStr;
	int Type;
	int SelectType = CtrlObject->FolderHistory->Select(Msg::FolderHistoryTitle, L"HistoryFolders", strStr, Type);
	/* SelectType =
		0 - Esc
		1 - Enter
		2 - Shift-Enter
		3 - Ctrl-Enter
		6 - Ctrl-Shift-Enter - на пассивную панель со сменой позиции
	*/
	switch (SelectType) {
		case 1: case 2: case 6: ChangeDirFromHistory(Type == 1, SelectType, strStr); break;
		case 3: SetString(strStr); break;
	}
}

void CommandLine::ProcessKey_ShowCommandsHistory()
{
	FARString strStr;
	int Type;
	// $ 19.09.2000 SVS - При выборе из History (по Alt-F8) плагин не получал управление!
	int SelectType = CtrlObject->CmdHistory->Select(Msg::HistoryTitle, L"History", strStr, Type);
	// BUGBUG, magic numbers
	if (SelectType == 8) {
		size_t p1 = 0;
		size_t p2 = 0;
		//fprintf(stderr, "=== Alt-F8: strStr=\"%ls\"\n", strStr.CPtr());
		if (strStr.Pos(p1, L'\n')) {
			strStr.Pos(p2, L'\n', p1 + 1);
			//fprintf(stderr, "=== Alt-F8: p1=%lu p2=%lu\n", p1, p2);
			ChangeDirFromHistory(Type == 1, 1, strStr.SubStr(0, p1),
				strStr.SubStr(p1 + 1, p2 > 0 ? p2 - p1 - 1 : -1) );
			if( p2 > 0 ) {
				strStr.Remove(0, p2 + 1);
				SetString(strStr);
			}
		} else {
			ChangeDirFromHistory(Type == 1, 1, strStr);
		}

	} else if ((SelectType > 0 && SelectType <= 3) || SelectType == 7) {
		if (SelectType < 3 || SelectType == 7) {
			CmdStr.DisableAC();
		}
		SetString(strStr);
		SetCurPos((int)strStr.GetLength());
		if (SelectType < 3 || SelectType == 7) {
			ProcessKey(SelectType == 7 ? static_cast<int>(KEY_CTRLALTENTER)
									: (SelectType == 1 ? static_cast<int>(KEY_ENTER)
														: static_cast<int>(KEY_SHIFTENTER)));
			CmdStr.RevertAC();
		}
	}
}

void CommandLine::AddHistory(const wchar_t* Str)
{
	if (!(Opt.ExcludeCmdHistory & EXCLUDECMDHISTORY_NOTCMDLINE)) {
		FARString strCurDirFromPanel;
		CtrlObject->Cp()->ActivePanel->GetCurDirPluginAware(strCurDirFromPanel);
		CtrlObject->CmdHistory->AddToHistoryExtra(Str, strCurDirFromPanel);
	}
}

int CommandLine::ProcessKey_Enter(FarKey Key)
{
	Panel *ActivePanel = CtrlObject->Cp()->ActivePanel;
	FARString strStr;
	CmdStr.Select(-1, 0);
	CmdStr.Show();
	CmdStr.GetString(strStr);
	if (m_hereDocDelimiters.empty())
		RemoveTrailingSpaces(strStr, true); // RemoveTrailingSpaces and taking into account last escaping symbol

	if (!m_hereDocDelimiters.empty()) {
		if (IsHereDocTerminator(strStr)) {
			m_hereDocDelimiters.erase(m_hereDocDelimiters.begin());
			if (m_hereDocDelimiters.empty()) {
				m_multilineLines[m_multilineActiveLine] = strStr;
			} else {
				ContinueMultilineInput(strStr);
				return TRUE;
			}
		} else {
			ContinueMultilineInput(strStr);
			return TRUE;
		}
	}

	if (AddHereDocDelimiters(strStr) || IsContinuationLine(strStr)) {
		ContinueMultilineInput(strStr);
		return TRUE;
	}

	if (!m_multilineLines.empty()) {
		if (!IsNotEmpty()) {
			CmdStr.SetString(L"", FALSE);
			ClearMultilineState();
			Show();
			return TRUE;
		}
		strStr = BuildMultilineCommand(strStr);
		ClearMultilineState();
	}

	if (strStr.IsEmpty())
		return FALSE;

	ActivePanel->SetCurPath();

	FARString strCurDirFromPanel;
	ActivePanel->GetCurDirPluginAware(strCurDirFromPanel);

	AddHistory(strStr);

	Redraw();
	if ( ProcessFarCommands(strStr.CPtr()) ) {
		CmdStr.SetString(L"", FALSE);
		Show();
		return TRUE;
	}

	// ProcessOSAliases(strStr);

	if (ActivePanel->ProcessPluginEvent(FE_COMMAND, (void *)strStr.CPtr())) {
		strCurDir = strCurDirFromPanel;
		Show();
		ActivePanel->SetTitle();

	} else {
		CmdExecute(strStr, Key == KEY_SHIFTENTER || Key == KEY_SHIFTNUMENTER, false, false, false,
				Key == KEY_CTRLALTENTER || Key == KEY_CTRLALTNUMENTER);
	}
	return TRUE;
}

int CommandLine::ProcessKey(FarKey Key)
{
	switch (Key) {
		case KEY_MSWHEEL_UP | KEY_CTRL | KEY_SHIFT:
			ViewConsoleHistory(NULL, false, true);
			return TRUE;
		case KEY_CTRLSHIFTF3: case KEY_F3:
			ViewConsoleHistory(NULL, false, false);
			return TRUE;
		case KEY_CTRLSHIFTF4: case KEY_F4:
			EditConsoleHistory(NULL, false);
			return TRUE;
		case KEY_F8:
			ProcessKey_ClearTerminalHistory();
			return TRUE;
		case KEY_F2:
			UserMenu::Present(false);
			return TRUE;
		case KEY_SHIFTF9:
			ConfigOptSave(true);
			return TRUE;
		case KEY_F10:
			FrameManager->ExitMainLoop(TRUE);
			return TRUE;
		case KEY_ALTF10:
			ProcessKey_ShowFolderTree();
			return TRUE;
		case KEY_F11:
			CtrlObject->Plugins.CommandsMenu(FALSE, FALSE, 0);
			return TRUE;
		case KEY_ALTF11:
			ShowViewEditHistory();
			CtrlObject->Cp()->Redraw();
			return TRUE;
		case KEY_ALTF12:
			ProcessKey_ShowFolderHistory();
			return TRUE;

		case KEY_ALTF8:
			ProcessKey_ShowCommandsHistory();
			return TRUE;
	}

	if (!IsVisible())
		return FALSE;

	return ProcessKeyIfVisible(Key);

}

int CommandLine::ProcessKeyIfVisible(FarKey Key)
{ // this handles key events only when CmdLine is visible
	switch (Key) {
		case KEY_NUMENTER:
		case KEY_SHIFTNUMENTER:
		case KEY_ENTER:
		case KEY_SHIFTENTER:
		case KEY_CTRLALTENTER:
		case KEY_CTRLALTNUMENTER:
			return ProcessKey_Enter(Key);

		case KEY_CTRLEND: case KEY_CTRLNUMPAD1:
			if (CmdStr.GetCurPos() == CmdStr.GetLength()) {
				if (LastCmdPartLength == -1)
					CmdStr.GetString(strLastCmdStr);

				FARString strStr = strLastCmdStr;
				int CurCmdPartLength = (int)strStr.GetLength();
				CtrlObject->CmdHistory->GetSimilar(strStr, LastCmdPartLength);

				if (LastCmdPartLength == -1) {
					CmdStr.GetString(strLastCmdStr);
					LastCmdPartLength = CurCmdPartLength;
				}
				CmdStr.DisableAC();
				SetString(strStr);
				Select(LastCmdPartLength, static_cast<int>(strStr.GetLength()));
				CmdStr.RevertAC();
				return TRUE;
			}
			break;

		case KEY_TAB: case KEY_SHIFTTAB:
			ProcessTabCompletion();
			return TRUE;
	}

	if (!m_multilineLines.empty()) {
		int sel_start = -1;
		int sel_end = 0;
		CmdStr.GetSelection(sel_start, sel_end);
		const bool has_selection = !(sel_start == -1 && sel_end == 0);
		if (Key == KEY_UP || Key == KEY_NUMPAD8)
			return MoveMultilineLine(-1);
		if (Key == KEY_DOWN || Key == KEY_NUMPAD2)
			return MoveMultilineLine(1);
		if (!has_selection) {
			if (Key == KEY_LEFT || Key == KEY_NUMPAD4) {
				if (CmdStr.GetCurPos() == 0 && m_multilineActiveLine > 0) {
					const int previous = m_multilineActiveLine - 1;
					SetActiveMultilineLine(previous, (int)m_multilineLines[previous].GetLength());
					Show();
					return TRUE;
				}
			}
			if (Key == KEY_RIGHT || Key == KEY_NUMPAD6) {
				if (CmdStr.GetCurPos() == CmdStr.GetLength()
						&& m_multilineActiveLine + 1 < (int)m_multilineLines.size()) {
					SetActiveMultilineLine(m_multilineActiveLine + 1, 0);
					Show();
					return TRUE;
				}
			}
			if (Key == KEY_DEL) {
				if (CmdStr.GetCurPos() == CmdStr.GetLength()
						&& m_multilineActiveLine + 1 < (int)m_multilineLines.size()) {
					SyncActiveMultilineLine();
					FARString current = m_multilineLines[m_multilineActiveLine];
					FARString next = m_multilineLines[m_multilineActiveLine + 1];
					const int new_pos = (int)current.GetLength();
					current+= next;
					m_multilineLines[m_multilineActiveLine] = current;
					m_multilineLines.erase(m_multilineLines.begin() + m_multilineActiveLine + 1);
					CmdStr.DisableAC();
					CmdStr.SetString(current.CPtr());
					CmdStr.SetLeftPos(0);
					CmdStr.Select(-1, 0);
					CmdStr.SetCurPos(new_pos);
					CmdStr.RevertAC();
					UpdateMultilineLayout();
					Show();
					return TRUE;
				}
			}
		}
		if (!has_selection && Key == KEY_BS) {
			if (CmdStr.GetCurPos() == 0 && m_multilineActiveLine > 0) {
				SyncActiveMultilineLine();
				FARString prev = m_multilineLines[m_multilineActiveLine - 1];
				FARString current = m_multilineLines[m_multilineActiveLine];
				const int new_pos = (int)prev.GetLength();
				prev+= current;
				m_multilineLines[m_multilineActiveLine - 1] = prev;
				m_multilineLines.erase(m_multilineLines.begin() + m_multilineActiveLine);
				m_multilineActiveLine--;
				CmdStr.DisableAC();
				CmdStr.SetString(prev.CPtr());
				CmdStr.SetLeftPos(0);
				CmdStr.Select(-1, 0);
				CmdStr.SetCurPos(new_pos);
				CmdStr.RevertAC();
				UpdateMultilineLayout();
				Show();
				return TRUE;
			}
		}
	}

	if (Key != KEY_NONE) {
		strLastCompletionCmdStr.Clear();
	}

	if (Key == KEY_UP || Key == KEY_NUMPAD8) {
		if (CtrlObject->Cp()->LeftPanel->IsVisible() || CtrlObject->Cp()->RightPanel->IsVisible())
			return FALSE;

		Key = KEY_CTRLE;
	} else if (Key == KEY_DOWN || Key == KEY_NUMPAD2) {
		if (CtrlObject->Cp()->LeftPanel->IsVisible() || CtrlObject->Cp()->RightPanel->IsVisible())
			return FALSE;

		Key = KEY_CTRLX;
	}

	// $ 25.03.2002 VVM + При погашенных панелях колесом крутим историю
	if (!CtrlObject->Cp()->LeftPanel->IsVisible() && !CtrlObject->Cp()->RightPanel->IsVisible()) {
		switch (Key) {
			case KEY_MSWHEEL_UP:
				Key = KEY_CTRLE;
				break;
			case KEY_MSWHEEL_DOWN:
				Key = KEY_CTRLX;
				break;
			case KEY_MSWHEEL_LEFT:
				Key = KEY_CTRLS;
				break;
			case KEY_MSWHEEL_RIGHT:
				Key = KEY_CTRLD;
				break;
		}
	}

	FARString strStr;
	const wchar_t *PStr;
	switch (Key) {
		case KEY_CTRLE:
		case KEY_CTRLX:
			if (Key == KEY_CTRLE) {
				CtrlObject->CmdHistory->GetPrev(strStr);
			} else {
				CtrlObject->CmdHistory->GetNext(strStr);
			}
			CmdStr.DisableAC();
			SetString(strStr);
			CmdStr.RevertAC();
			return TRUE;

		case KEY_ESC:
			if (Key == KEY_ESC) {
				if (!m_multilineLines.empty()) {
					CmdStr.SetString(L"", FALSE);
					ClearMultilineState();
					Show();
					return TRUE;
				}
				// $ 24.09.2000 SVS - Если задано поведение по "Несохранению при Esc", то позицию в хистори не меняем и ставим в первое положение.
				if (Opt.CmdHistoryRule)
					CtrlObject->CmdHistory->ResetPosition();

				PStr = L"";
			}

			SetString(PStr);
			return TRUE;
		case KEY_CTRLU:
			CmdStr.Select(-1, 0);
			CmdStr.Show();
			return TRUE;
		case KEY_OP_XLAT: {
			// 13.12.2000 SVS - ! Для CmdLine - если нет выделения, преобразуем всю строку (XLat)
			CmdStr.Xlat(Opt.XLat.Flags & XLAT_CONVERTALLCMDLINE ? TRUE : FALSE);

			// иначе неправильно работает ctrl-end
			CmdStr.GetString(strLastCmdStr);
			LastCmdPartLength = (int)strLastCmdStr.GetLength();

			return TRUE;
		}
		/*
			дополнительные клавиши для выделения в ком строке.
			ВНИМАНИЕ!
			Для сокращения кода этот кусок должен стоять перед "default"
		*/
		case KEY_ALTSHIFTLEFT:
		case KEY_ALTSHIFTNUMPAD4:
		case KEY_ALTSHIFTRIGHT:
		case KEY_ALTSHIFTNUMPAD6:
		case KEY_ALTSHIFTEND:
		case KEY_ALTSHIFTNUMPAD1:
		case KEY_ALTSHIFTHOME:
		case KEY_ALTSHIFTNUMPAD7:
			Key&= ~KEY_ALT;
		default:

			// Сбрасываем выделение на некоторых клавишах
			if (!Opt.CmdLine.EditBlock) {
				static FarKey UnmarkKeys[] = {KEY_LEFT, KEY_NUMPAD4, KEY_CTRLS, KEY_RIGHT, KEY_NUMPAD6,
						KEY_CTRLD, KEY_CTRLLEFT, KEY_CTRLNUMPAD4, KEY_CTRLRIGHT, KEY_CTRLNUMPAD6,
						KEY_CTRLHOME, KEY_CTRLNUMPAD7, KEY_CTRLEND, KEY_CTRLNUMPAD1, KEY_HOME, KEY_NUMPAD7,
						KEY_END, KEY_NUMPAD1};

				for (size_t I = 0; I < ARRAYSIZE(UnmarkKeys); I++)
					if (Key == UnmarkKeys[I]) {
						CmdStr.Select(-1, 0);
						break;
					}
			}

			if (Key == KEY_CTRLD)
				Key = KEY_RIGHT;

			if (Key == KEY_CTRLV || Key == KEY_SHIFTINS || Key == KEY_SHIFTNUMPAD0 || Key == KEY_OP_PLAINTEXT) {
				FARString clip;
				if (Key == KEY_OP_PLAINTEXT && !GPastedText.IsEmpty()) {
					clip = GPastedText;
				} else {
					wchar_t *p = PasteFromClipboard();
					if (p) {
						clip = p;
						free(p);
					}
				}

				const wchar_t *ClipText = clip.CPtr();
				if (ClipText && wcschr(ClipText, L'\n') && wcschr(ClipText, L'\n')[1] != L'\0') {
					GetString(strStr);
					const int cursor_pos = GetCurPos();
					FARString strToExec = strStr.SubStr(0, cursor_pos) + ClipText + strStr.SubStr(cursor_pos);
					NormalizeMultilineForExec(strToExec);
					RemoveTrailingSpaces(strToExec);
					if (Opt.CmdLine.AskOnMultilinePaste)
					{
						int res = ShowMultilinePasteDialog(strToExec);

						if (res == 0)
							break;

						if (res == 2)
							Opt.CmdLine.AskOnMultilinePaste = false;
					}

					AddHistory(strToExec);
					ExecString(strToExec);
					break;
				}
			}

			if (!CmdStr.ProcessKey(Key))
				break;

			LastCmdPartLength = -1;

			if (Key == KEY_CTRLSHIFTEND || Key == KEY_CTRLSHIFTNUMPAD1) {
				CmdStr.EnableAC();
				CmdStr.AutoComplete(true, false);
				CmdStr.RevertAC();
			}

			return TRUE;
	}

	return FALSE;
}

BOOL CommandLine::SetCurDir(const wchar_t *CurDir)
{
	if (StrCmp(strCurDir, CurDir) || !TestCurrentDirectory(CurDir)) {
		strCurDir = CurDir;

		if (CtrlObject->Cp()->ActivePanel->GetMode() != PLUGIN_PANEL)
			PrepareDiskPath(strCurDir);
	}

	return TRUE;
}

int CommandLine::GetCurDir(FARString &strCurDir)
{
	strCurDir = CommandLine::strCurDir;
	return (int)strCurDir.GetLength();
}

void CommandLine::GetString(FARString &strStr)
{
	if (m_multilineLines.empty()) {
		CmdStr.GetString(strStr);
		return;
	}

	FARString active_line;
	CmdStr.GetString(active_line);
	strStr = BuildMultilineCommand(active_line);
}

void CommandLine::SetString(const wchar_t *Str, BOOL Redraw)
{
	if (!IsVisible())
		return;

	FARString normalized(Str ? Str : L"");
	NormalizeMultilineForExec(normalized);
	size_t newline_pos = 0;
	if (normalized.Pos(newline_pos, L'\n')) {
		LastCmdPartLength = -1;
		ApplyMultilineText(normalized, Redraw);
		return;
	}

	if (!m_multilineLines.empty())
		ClearMultilineState();

	LastCmdPartLength = -1;
	CmdStr.SetString(normalized);
	CmdStr.SetLeftPos(0);
	if (Redraw)
		CmdStr.Show();
}

void CommandLine::ExecString(const wchar_t *Str, bool SeparateWindow, bool DirectRun, bool WaitForIdle,
		bool Silent, bool RunAs)
{
	CmdStr.DisableAC();
	SetString(Str);
	CmdStr.RevertAC();
	CmdExecute(Str, SeparateWindow, DirectRun, WaitForIdle, Silent, RunAs);
}

void CommandLine::InsertString(const wchar_t *Str)
{
	if (!IsVisible() || !Str)
		return;

	LastCmdPartLength = -1;
	FARString insertion(Str);
	NormalizeMultilineForExec(insertion);
	size_t newline_pos = 0;
	if (insertion.Pos(newline_pos, L'\n')) {
		FARString command;
		GetString(command);
		const int cursor_pos = GetCurPos();
		command = command.SubStr(0, cursor_pos) + insertion + command.SubStr(cursor_pos);
		SetString(command);
		SetCurPos(cursor_pos + (int)insertion.GetLength());
		return;
	}

	CmdStr.DisableAC();
	CmdStr.InsertString(insertion);
	CmdStr.Show();
	CmdStr.RevertAC();
}

void CommandLine::GetSelection(int &Start, int &End)
{
	CmdStr.GetSelection(Start, End);
	if (!m_multilineLines.empty() && Start >= 0) {
		const int offset = GetMultilineLineOffset(m_multilineActiveLine);
		Start+= offset;
		End+= offset;
	}
}

void CommandLine::Select(int Start, int End)
{
	if (m_multilineLines.empty() || Start < 0) {
		CmdStr.Select(Start, End);
		return;
	}

	SyncActiveMultilineLine();
	for (int i = 0; i < (int)m_multilineLines.size(); ++i) {
		const int offset = GetMultilineLineOffset(i);
		const int length = (int)m_multilineLines[i].GetLength();
		if (Start >= offset && End >= Start && End <= offset + length) {
			if (i == m_multilineActiveLine)
				CmdStr.SetCurPos(std::min(End - offset, length));
			else
				SetActiveMultilineLine(i, std::min(End - offset, length));
			CmdStr.Select(Start - offset, End - offset);
			Show();
			return;
		}
	}

	CmdStr.Select(-1, 0);
}

void CommandLine::ResizeConsole()
{
	if (!m_multilineLines.empty()) {
		UpdateMultilineLayout();
		Show();
	}
}

int CommandLine::ProcessMouse(MOUSE_EVENT_RECORD *MouseEvent)
{
	if (MouseEvent->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED
			&& MouseEvent->dwMousePosition.X == X2 + 1 && MouseEvent->dwMousePosition.Y == Y2) {
		return ProcessKey(KEY_ALTF8);
	}

	if (!m_multilineLines.empty() && MouseEvent->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED
			&& MouseEvent->dwMousePosition.X >= X1 && MouseEvent->dwMousePosition.X <= X2) {
		int visible_lines = 0;
		const int view_top = GetMultilineViewTop(visible_lines);
		const int view_y = Y2 - visible_lines + 1;
		if (MouseEvent->dwMousePosition.Y >= view_y && MouseEvent->dwMousePosition.Y <= Y2) {
			const int line = view_top + MouseEvent->dwMousePosition.Y - view_y;
			FARString prompt;
			GetMultilinePrompt(line, prompt);
			const int editor_x = X1 + (int)prompt.CellsCount();
			SetActiveMultilineLine(line, 0);
			Show();
			if (MouseEvent->dwMousePosition.X < editor_x) {
				CmdStr.SetCurPos(0);
				CmdStr.Show();
				return TRUE;
			}

			MOUSE_EVENT_RECORD edit_event = *MouseEvent;
			const int active_view_top = GetMultilineViewTop(visible_lines);
			edit_event.dwMousePosition.Y = Y2 - visible_lines + 1 + line - active_view_top;
			return CmdStr.ProcessMouse(&edit_event);
		}
	}

	int r = CmdStr.ProcessMouse(MouseEvent);
	if (r == 0) {
		if (MouseEvent->dwButtonState & FROM_LEFT_1ST_BUTTON_PRESSED) {
			WINPORT(BeginConsoleAdhocQuickEdit)();
		}
	}
	return r;
}

void CommandLine::GetPrompt(FARString &strDestStr)
{
	FARString strExpandedFormatStr;
	if (Opt.CmdLine.UsePromptFormat) {
		FARString strFormatStr;
		strFormatStr = Opt.CmdLine.strPromptFormat;
		apiExpandEnvironmentStrings(strFormatStr, strExpandedFormatStr);

	} else {	// default prompt
		strExpandedFormatStr = "$p$# ";
	}

	constexpr wchar_t ChrFmt[][2] = {
			{L'A', L'&'},		// $A - & (Ampersand)
			{L'B', L'|'},		// $B - | (pipe)
			{L'C', L'('},		// $C - ( (Left parenthesis)
			{L'F', L')'},		// $F - ) (Right parenthesis)
			{L'G', L'>'},		// $G - > (greater-than sign)
			{L'L', L'<'},		// $L - < (less-than sign)
			{L'Q', L'='},		// $Q - = (equal sign)
			{L'S', L' '},		// $S - (space)
			{L'$', L'$'},		// $$ - $ (dollar sign)
	};

	const wchar_t *Format = strExpandedFormatStr;
	while (*Format) {
		if (*Format == L'$') {
			wchar_t Chr = Upper(*++Format);
			size_t I;

			for (I = 0; I < ARRAYSIZE(ChrFmt); ++I) {
				if (ChrFmt[I][0] == Chr) {
					strDestStr+= ChrFmt[I][1];
					break;
				}
			}

			if (I == ARRAYSIZE(ChrFmt)) {
				switch (Chr) {
						/*
						эти не раелизованы
						$E - Escape code (ASCII code 27)
						$V - Windows XP version number
						$_ - Carriage return and linefeed
						$M - Отображение полного имени удаленного диска, связанного с именем текущего диска, или пустой строки, если текущий диск не является сетевым.
						*/
					case L'+':		// $+ - Отображение нужного числа знаков плюс (+) в зависимости от текущей глубины стека каталогов PUSHD, по одному знаку на каждый сохраненный путь.
					{
						if (PushDirStackSize) {
							strDestStr.Append(L'+', PushDirStackSize);
						}

						break;
					}
					case L'H':		// $H - Backspace (erases previous character)
					{
						if (!strDestStr.IsEmpty())
							strDestStr.Truncate(strDestStr.GetLength() - 1);

						break;
					}
					case L'@':		// $@xx - Admin
					{
						wchar_t lb = *++Format;
						wchar_t rb = *++Format;
						if (Opt.IsUserAdmin) {
							strDestStr+= lb;
							strDestStr+= Msg::ConfigCmdlinePromptFormatAdmin;
							strDestStr+= rb;
						}
						break;
					}
					case L'D':		// $D - Current date
					case L'T':		// $T - Current time
					{
						FARString strDateTime;
						MkStrFTime(strDateTime, (Chr == L'D' ? L"%D" : L"%T"));
						strDestStr+= strDateTime;
						break;
					}
					case L'R':		// $R - Current drive and path, always full
					{
						strDestStr+= strCurDir;
						break;
					}
					case L'P':		// $P - Current drive and path, shortened home
					{
						const auto &strHome = CachedHomeDir();
						if (strCurDir.Begins(strHome)) {
							strDestStr+= L'~';
							strDestStr+= strCurDir.CPtr() + strHome.GetLength();
						} else {
							strDestStr+= strCurDir;
						}
						break;
					}
					case L'#':		// # or $ - depending of user root or not
					{
						strDestStr+= Opt.IsUserAdmin ? L"#" : L"$";
						break;
					}
					case L'U':		// User name
					{
						strDestStr+= CachedUserName();
						break;
					}
					case L'N':		// Host name
					{
						strDestStr+= CachedComputerName();
						break;
					}
					case L'Z':		// Git Branch
					{
						strDestStr+= GetGitBranchName(strCurDir);
						break;
					}
				}
			}

			Format++;
		} else {
			strDestStr+= *(Format++);
		}
	}
}

void CommandLine::ShowViewEditHistory()
{
	FARString strStr;
	int Type;
	int SelectType = CtrlObject->ViewHistory->Select(Msg::ViewHistoryTitle, L"HistoryViews", strStr, Type);
	/*
	SelectType =
		0 - Esc
		1 - Enter
		2 - Shift-Enter
		3 - Ctrl-Enter
		9 - Ctrl-F10 - jump in panel to directory & file
	*/

	if (SelectType == 1 || SelectType == 2) {
		if (SelectType != 2)
			CtrlObject->ViewHistory->AddToHistory(strStr, Type);

		CtrlObject->ViewHistory->SetAddMode(false, HISTORY_REMOVE_DUPS_CASE_SENSITIVE, true);

		switch (Type) {
			case 0:		// вьювер
			{
				new FileViewer(std::make_shared<FileHolder>(strStr), TRUE);
				break;
			}
			case 1:		// обычное открытие в редакторе
			case 4:		// открытие с локом
			{
				// пусть файл создается
				FileEditor *FEdit =
						new FileEditor(std::make_shared<FileHolder>(strStr), CP_AUTODETECT, FFILEEDIT_CANNEWFILE | FFILEEDIT_ENABLEF6);

				if (Type == 4)
					FEdit->SetLockEditor(TRUE);

				break;
			}
			// 2 и 3 - заполняется в ProcessExternal
			case 2:
			case 3: {
				if (strStr.At(0) != L'@') {
					ExecString(strStr);
					if (Type > 2) {
						WaitForClose(strStr.CPtr());
					}
				} else {
					SaveScreen SaveScr;
					CtrlObject->Cp()->LeftPanel->CloseFile();
					CtrlObject->Cp()->RightPanel->CloseFile();
					Execute(strStr.CPtr() + 1);
					if (Type > 2) {
						WaitForClose(strStr.CPtr() + 1);
					}
				}

				break;
			}
		}

		CtrlObject->ViewHistory->SetAddMode(true, HISTORY_REMOVE_DUPS_CASE_SENSITIVE, true);
	}
	else if (SelectType == 3)		// скинуть из истории в ком.строку?
		SetString(strStr);
	else if (SelectType == 9)		// goto in panel to directory & file
		CtrlObject->Cp()->GoToFile(strStr);
}

void CommandLine::SaveBackground()
{
	fprintf(stderr, "CommandLine::SaveBackground\n");
	ScrBuf.Flush();
	BackgroundConsole.Fork(NULL);
	DWORD mode = 0; // set ENABLE_PROCESSED_OUTPUT to enable lines recomposing for forked console
	if (WINPORT(GetConsoleMode)(BackgroundConsole.Handle(), &mode)) {
		WINPORT(SetConsoleMode)(BackgroundConsole.Handle(), mode | ENABLE_PROCESSED_OUTPUT);
	}
}

void CommandLine::ShowBackground(bool showanyway)
{
	if ((!IsVisible() && !showanyway) || !BackgroundConsole) {
		fprintf(stderr, "CommandLine::ShowBackground - skip\n");
		return;
	}

	fprintf(stderr, "CommandLine::ShowBackground\n");
	ScrBuf.FillBufWithRecompose(BackgroundConsole.Handle());
}

void CommandLine::Show()
{
	if (IsVisible())
		ScreenObject::Show();
}

void CommandLine::RedrawWithoutComboBoxMark()
{
	if (!IsVisible())
		return;

	Redraw();
	// erase \x2191 character...
	DrawComboBoxMark(L' ', Y2);
}

bool CommandLine::ProcessFarCommands(const wchar_t *CmdLine)
{
	bool b_far, b_edit = false, b_view = false;
	std::string::size_type p;
	std::wstring str_command(CmdLine);
	auto expandString = [](const std::wstring &Filename) -> std::wstring {
		std::string new_path_mb;
		StrWide2MB(Filename, new_path_mb);
		Environment::ExpandString(new_path_mb, true);
		FARString result(new_path_mb);
		UnEscapeSpace(result);
		return result.GetWide();
	};

	StrTrim(str_command);

	b_far = StrStartsFrom(str_command, L"far:");
	if (!b_far) {
		b_edit = StrStartsFrom(str_command, L"edit:");
		if (!b_edit) {
			b_view = StrStartsFrom(str_command, L"view:");
			if (!b_view) {
				return false; // not found any available prefixes
			}
		}
	}

	if (b_far && str_command == L"far:config") {
		ConfigOptEdit();
		return true; // prefix correct and was processed
	}

	if (b_far && str_command == L"far:about") {
		void FarAbout(PluginManager &Plugins);
		FarAbout(CtrlObject->Plugins);
		return true; // prefix correct and was processed
	}

	p = b_edit ? 5 // wcslen(L"edit:")
		: ( (b_far && (StrStartsFrom(str_command, L"far:edit:") || StrStartsFrom(str_command, L"far:edit ") || str_command == L"far:edit"))
			? 9 // wcslen(L"far:edit:") or wcslen(L"far:edit ")
			: 0 );
	if (p > 0) {
		int StartLine = -1, StartChar = -1;
		// check location of optional parametrs with line and column
		std::string::size_type p1 = std::string::npos, p2 = std::string::npos;
		if (str_command[p-1] == L':' && p < str_command.length() && str_command[p] == L'[') {
			p2 = str_command.find(L"]", p+1);
			if (p2 != std::string::npos) {
				p1 = p;
				p = p2 + 1;
			}
		}
		// check filename
		p = str_command.find_first_not_of(L" \t", p);
		if (p != std::string::npos) { // after spaces found filename or command
			if (str_command[p]==L'<') { // redirect command
				p = str_command.find_first_not_of(L" \t", p+1);
				if (p != std::string::npos) {
					new FileEditor(
						std::make_shared<FileHolder>(
							ExecuteCommandAndGrabItsOutput( str_command.substr(p,std::string::npos).c_str() ) ),
						CP_AUTODETECT, FFILEEDIT_ENABLEF6 | FFILEEDIT_DISABLEHISTORY);
				}
			}
			else { // filename
				const std::wstring filename = expandString(str_command.substr(p,std::string::npos));
				// optional parametrs with line and column to numbers
				if (p1 != std::string::npos) {
					p1 = str_command.find_first_not_of(L" \t", p1+1);
					if (p1 != std::string::npos && p1 < p2) {
						if (iswdigit(str_command[p1])) {
							StartLine = _wtoi(str_command.substr(p1).c_str());
							StartChar = 1;
						}
						p = str_command.find_first_of(L",:", p1);
						if (p != std::string::npos && p < p2) {
							p = str_command.find_first_not_of(L" \t", p+1);
							if (p != std::string::npos && p < p2 && iswdigit(str_command[p])) {
								StartChar = _wtoi(str_command.substr(p).c_str());
								if (StartLine < 0)
									StartLine = 1;
							}
						}
					}
				}
				new FileEditor(
					std::make_shared<FileHolder>( filename.c_str() ),
					CP_AUTODETECT, FFILEEDIT_CANNEWFILE | FFILEEDIT_ENABLEF6, StartLine, StartChar);
			}
		}
		else // new empty file
			new FileEditor(
				std::make_shared<FileHolder>( Msg::NewFileName.CPtr() ),
				CP_AUTODETECT, FFILEEDIT_CANNEWFILE | FFILEEDIT_ENABLEF6);
		return true; // prefix correct and was processed
	}

	p = b_view ? 5 // wcslen(L"view:")
		: ( (b_far && (StrStartsFrom(str_command, L"far:view:") || StrStartsFrom(str_command, L"far:view ")))
			? 9 // wcslen(L"far:view:") or wcslen(L"far:view ")
			: 0 );
	if (p > 0) {
		p = str_command.find_first_not_of(L" \t", p);
		if (p != std::string::npos) { // after spaces found filename or command
			if (str_command[p]==L'<') { // redirect command
				p = str_command.find_first_not_of(L" \t", p+1);
				if (p != std::string::npos) {
					new FileViewer(std::make_shared<FileHolder>(
							ExecuteCommandAndGrabItsOutput( str_command.substr(p,std::string::npos).c_str() ) ),
					TRUE/*EnableSwitch*/, TRUE/*DisableHistory*/, FALSE/*DisableEdit*/);
				}
			}
			else { // filename
				const std::wstring filename = expandString(str_command.substr(p,std::string::npos));
				new FileViewer(std::make_shared<FileHolder>( filename.c_str() ),
					TRUE/*EnableSwitch*/, FALSE/*DisableHistory*/, FALSE/*DisableEdit*/);
			}
		}
		return true; // anyway prefix correct and was processed
	}

	return false; // not found any available prefixes
}

HANDLE CommandLine::GetBackgroundConsole()
{
	return BackgroundConsole.Handle();
}

CmdLineVisibleScope::CmdLineVisibleScope()
{
	if (CtrlObject && CtrlObject->CmdLine && !CtrlObject->CmdLine->IsVisible()) {
		CtrlObject->CmdLine->SetVisible(true);
		CtrlObject->CmdLine->Show();
	}
}

CmdLineVisibleScope::~CmdLineVisibleScope()
{
	FilePanels *cp = CtrlObject->Cp();
	if (cp) {
		cp->UpdateCmdLineVisibility();
	}
}
