Rewrite resize logic, introduce "direct mode", and tweak scroll scraping.
* Unfreeze the console while changing the buffer size. Changing the buffer size hangs conhost.exe. See: - https://github.com/rprichard/winpty/issues/31 - https://wpdev.uservoice.com/forums/266908-command-prompt/suggestions/9941292-conhost-exe-hangs-in-win10-if-setconsolescreenbuff * Detect buffer size changes and switch to a "direct mode". Direct mode makes no attempt to track incremental console changes. Instead, the content of the current console window is printed. This mode is intended for full-screen apps that resize the console. * Reopen CONOUT$, which detects apps that change the active screen buffer. Fixes https://github.com/rprichard/winpty/issues/34. * In the scroll scraping (scrollingScrapeOutput), consider a line changed if the new content is truncated relative to the content previously output. Previously, we only compared against the line-buffer up to the current console width. e.g. If this: |C:\Program| turns into: |C:\Prog| |ram | we previously left |C:\Program| in the line-buffer for the first line and did not re-output the first line. We *should* reoutput the first line at this point so that, if the line scrolls upward, and the terminal is later expanded, we will have output an "Erase in Line" CSI command to clear the obscured "ram" text. We need to update the line-buffer for the sake of Windows 10 combined with terminals like xterm and putty. On such a terminal, if the terminal later widened, Windows 10 will restore the console to the first state. At that point, we need to reoutput the line, because xterm and putty do not save and restore truncated line content extending past the current terminal width.
This commit is contained in:
parent
6c9c7bb985
commit
d4640890cf
297
agent/Agent.cc
297
agent/Agent.cc
@ -38,7 +38,6 @@
|
||||
|
||||
const int SC_CONSOLE_MARK = 0xFFF2;
|
||||
const int SC_CONSOLE_SELECT_ALL = 0xFFF5;
|
||||
const int SYNC_MARKER_LEN = 16;
|
||||
|
||||
static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType)
|
||||
{
|
||||
@ -74,7 +73,9 @@ Agent::Agent(LPCWSTR controlPipeName,
|
||||
m_terminal(NULL),
|
||||
m_childProcess(NULL),
|
||||
m_childExitCode(-1),
|
||||
m_syncCounter(0)
|
||||
m_syncCounter(0),
|
||||
m_directMode(false),
|
||||
m_ptySize(initialCols, initialRows)
|
||||
{
|
||||
trace("Agent starting...");
|
||||
|
||||
@ -82,16 +83,16 @@ Agent::Agent(LPCWSTR controlPipeName,
|
||||
|
||||
m_console = new Win32Console;
|
||||
m_console->setSmallFont();
|
||||
m_console->reposition(
|
||||
Coord(initialCols, BUFFER_LINE_COUNT),
|
||||
SmallRect(0, 0, initialCols, initialRows));
|
||||
m_console->moveWindow(SmallRect(0, 0, 1, 1));
|
||||
m_console->resizeBuffer(Coord(initialCols, BUFFER_LINE_COUNT));
|
||||
m_console->moveWindow(SmallRect(0, 0, initialCols, initialRows));
|
||||
m_console->setCursorPosition(Coord(0, 0));
|
||||
m_console->setTitle(m_currentTitle);
|
||||
|
||||
m_controlSocket = makeSocket(controlPipeName);
|
||||
m_dataSocket = makeSocket(dataPipeName);
|
||||
m_terminal = new Terminal(m_dataSocket);
|
||||
m_consoleInput = new ConsoleInput(m_console, this);
|
||||
m_consoleInput = new ConsoleInput(this);
|
||||
|
||||
resetConsoleTracking(false);
|
||||
|
||||
@ -196,6 +197,11 @@ void Agent::handlePacket(ReadBuffer &packet)
|
||||
result = handleStartProcessPacket(packet);
|
||||
break;
|
||||
case AgentMsg::SetSize:
|
||||
// TODO: I think it might make sense to collapse consecutive SetSize
|
||||
// messages. i.e. The terminal process can probably generate SetSize
|
||||
// messages faster than they can be processed, and some GUIs might
|
||||
// generate a flood of them, so if we can read multiple SetSize packets
|
||||
// at once, we can ignore the early ones.
|
||||
result = handleSetSizePacket(packet);
|
||||
break;
|
||||
case AgentMsg::GetExitCode:
|
||||
@ -314,8 +320,9 @@ void Agent::onPollTimeout()
|
||||
|
||||
// Scrape for output *after* the above exit-check to ensure that we collect
|
||||
// the child process's final output.
|
||||
if (!m_dataSocket->isClosed())
|
||||
scrapeOutput();
|
||||
if (!m_dataSocket->isClosed()) {
|
||||
syncConsoleContentAndSize(false);
|
||||
}
|
||||
|
||||
if (m_closingDataSocket &&
|
||||
!m_dataSocket->isClosed() &&
|
||||
@ -328,9 +335,8 @@ void Agent::onPollTimeout()
|
||||
// Detect window movement. If the window moves down (presumably as a
|
||||
// result of scrolling), then assume that all screen buffer lines down to
|
||||
// the bottom of the window are dirty.
|
||||
void Agent::markEntireWindowDirty()
|
||||
void Agent::markEntireWindowDirty(const SmallRect &windowRect)
|
||||
{
|
||||
SmallRect windowRect = m_console->windowRect();
|
||||
m_dirtyLineCount = std::max(m_dirtyLineCount,
|
||||
windowRect.top() + windowRect.height());
|
||||
}
|
||||
@ -366,40 +372,182 @@ void Agent::scanForDirtyLines()
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::resizeWindow(int cols, int rows)
|
||||
// Clear lines in the line buffer. The `firstRow` parameter is in
|
||||
// screen-buffer coordinates.
|
||||
void Agent::clearBufferLines(
|
||||
const int firstRow,
|
||||
const int count,
|
||||
const WORD attributes)
|
||||
{
|
||||
freezeConsole();
|
||||
ASSERT(!m_directMode);
|
||||
CHAR_INFO blankLine[MAX_CONSOLE_WIDTH];
|
||||
memset(&blankLine, 0, sizeof(blankLine));
|
||||
for (int col = 0; col < MAX_CONSOLE_WIDTH; ++col) {
|
||||
blankLine[col].Char.UnicodeChar = L' ';
|
||||
blankLine[col].Attributes = attributes;
|
||||
}
|
||||
for (int row = firstRow; row < firstRow + count; ++row) {
|
||||
int bufRow = (row + m_scrolledCount) % BUFFER_LINE_COUNT;
|
||||
memcpy(&m_bufferData[bufRow], &blankLine, sizeof(blankLine));
|
||||
}
|
||||
}
|
||||
|
||||
Coord bufferSize = m_console->bufferSize();
|
||||
SmallRect windowRect = m_console->windowRect();
|
||||
Coord newBufferSize(cols, bufferSize.Y);
|
||||
SmallRect newWindowRect;
|
||||
// This function is called with the console frozen, and the console is still
|
||||
// frozen when it returns.
|
||||
void Agent::resizeImpl(const ConsoleScreenBufferInfo &origInfo)
|
||||
{
|
||||
const int cols = m_ptySize.X;
|
||||
const int rows = m_ptySize.Y;
|
||||
|
||||
// This resize behavior appears to match what happens when I resize the
|
||||
// console window by hand.
|
||||
if (windowRect.top() + windowRect.height() == bufferSize.Y ||
|
||||
windowRect.top() + rows >= bufferSize.Y) {
|
||||
// Lock the bottom of the new window to the bottom of the buffer if either
|
||||
// - the window was already at the bottom of the buffer, OR
|
||||
// - there isn't enough room.
|
||||
newWindowRect = SmallRect(0, newBufferSize.Y - rows, cols, rows);
|
||||
} else {
|
||||
// Keep the top of the window where it is.
|
||||
newWindowRect = SmallRect(0, windowRect.top(), cols, rows);
|
||||
{
|
||||
//
|
||||
// To accommodate Windows 10, erase all lines up to the top of the
|
||||
// visible window. It's hard to tell whether this is strictly
|
||||
// necessary. It ensures that the sync marker won't move downward,
|
||||
// and it ensures that we won't repeat lines that have already scrolled
|
||||
// up into the scrollback.
|
||||
//
|
||||
// It *is* possible for these blank lines to reappear in the visible
|
||||
// window (e.g. if the window is made taller), but because we blanked
|
||||
// the lines in the line buffer, we still don't output them again.
|
||||
//
|
||||
const Coord origBufferSize = origInfo.bufferSize();
|
||||
const SmallRect origWindowRect = origInfo.windowRect();
|
||||
|
||||
if (!m_directMode) {
|
||||
m_console->clearLines(0, origWindowRect.Top, origInfo);
|
||||
clearBufferLines(0, origWindowRect.Top, origInfo.wAttributes);
|
||||
if (m_syncRow != -1) {
|
||||
createSyncMarker(m_syncRow);
|
||||
}
|
||||
}
|
||||
|
||||
const Coord finalBufferSize(
|
||||
cols,
|
||||
// If there was previously no scrollback (e.g. a full-screen app
|
||||
// in direct mode) and we're reducing the window height, then
|
||||
// reduce the console buffer's height too.
|
||||
(origWindowRect.height() == origBufferSize.Y)
|
||||
? rows
|
||||
: std::max<int>(rows, origBufferSize.Y));
|
||||
const bool cursorWasInWindow =
|
||||
origInfo.cursorPosition().Y >= origWindowRect.Top &&
|
||||
origInfo.cursorPosition().Y <= origWindowRect.Bottom;
|
||||
|
||||
// Step 1: move the window.
|
||||
const int tmpWindowWidth = std::min(origBufferSize.X, finalBufferSize.X);
|
||||
const int tmpWindowHeight = std::min<int>(origBufferSize.Y, rows);
|
||||
SmallRect tmpWindowRect(
|
||||
0,
|
||||
std::min<int>(origBufferSize.Y - tmpWindowHeight,
|
||||
origWindowRect.Top),
|
||||
tmpWindowWidth,
|
||||
tmpWindowHeight);
|
||||
if (cursorWasInWindow) {
|
||||
tmpWindowRect = tmpWindowRect.ensureLineIncluded(
|
||||
origInfo.cursorPosition().Y);
|
||||
}
|
||||
m_console->moveWindow(tmpWindowRect);
|
||||
|
||||
// Step 2: resize the buffer.
|
||||
unfreezeConsole();
|
||||
m_console->resizeBuffer(finalBufferSize);
|
||||
}
|
||||
|
||||
if (m_dirtyWindowTop != -1 && m_dirtyWindowTop < windowRect.top())
|
||||
markEntireWindowDirty();
|
||||
m_dirtyWindowTop = newWindowRect.top();
|
||||
// Step 3: expand the window to its full size.
|
||||
{
|
||||
freezeConsole();
|
||||
const ConsoleScreenBufferInfo info = m_console->bufferInfo();
|
||||
const bool cursorWasInWindow =
|
||||
info.cursorPosition().Y >= info.windowRect().Top &&
|
||||
info.cursorPosition().Y <= info.windowRect().Bottom;
|
||||
|
||||
SmallRect finalWindowRect(
|
||||
0,
|
||||
std::min<int>(info.bufferSize().Y - rows,
|
||||
info.windowRect().Top),
|
||||
cols,
|
||||
rows);
|
||||
|
||||
//
|
||||
// Once a line in the screen buffer is "dirty", it should stay visible
|
||||
// in the console window, so that we continue to update its content in
|
||||
// the terminal. This code is particularly (only?) necessary on
|
||||
// Windows 10, where making the buffer wider can rewrap lines and move
|
||||
// the console window upward.
|
||||
//
|
||||
if (!m_directMode && m_dirtyLineCount > finalWindowRect.Bottom + 1) {
|
||||
// In theory, we avoid ensureLineIncluded, because, a massive
|
||||
// amount of output could have occurred while the console was
|
||||
// unfrozen, so that the *top* of the window is now below the
|
||||
// dirtiest tracked line.
|
||||
finalWindowRect = SmallRect(
|
||||
0, m_dirtyLineCount - rows,
|
||||
cols, rows);
|
||||
}
|
||||
|
||||
// Highest priority constraint: ensure that the cursor remains visible.
|
||||
if (cursorWasInWindow) {
|
||||
finalWindowRect = finalWindowRect.ensureLineIncluded(
|
||||
info.cursorPosition().Y);
|
||||
}
|
||||
|
||||
m_console->moveWindow(finalWindowRect);
|
||||
m_dirtyWindowTop = finalWindowRect.Top;
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::resizeWindow(const int cols, const int rows)
|
||||
{
|
||||
if (cols < 1 ||
|
||||
cols > MAX_CONSOLE_WIDTH ||
|
||||
rows < 1 ||
|
||||
rows > BUFFER_LINE_COUNT - 1) {
|
||||
trace("resizeWindow: invalid size: cols=%d,rows=%d", cols, rows);
|
||||
return;
|
||||
}
|
||||
m_ptySize = Coord(cols, rows);
|
||||
syncConsoleContentAndSize(true);
|
||||
}
|
||||
|
||||
void Agent::syncConsoleContentAndSize(bool forceResize)
|
||||
{
|
||||
reopenConsole();
|
||||
freezeConsole();
|
||||
syncConsoleTitle();
|
||||
|
||||
const ConsoleScreenBufferInfo info = m_console->bufferInfo();
|
||||
|
||||
// If an app resizes the buffer height, then we enter "direct mode", where
|
||||
// we stop trying to track incremental console changes.
|
||||
const bool newDirectMode = (info.bufferSize().Y != BUFFER_LINE_COUNT);
|
||||
if (newDirectMode != m_directMode) {
|
||||
trace("Entering %s mode", newDirectMode ? "direct" : "scrolling");
|
||||
resetConsoleTracking();
|
||||
m_directMode = newDirectMode;
|
||||
|
||||
// When we switch from direct->scrolling mode, make sure the console is
|
||||
// the right size.
|
||||
if (!m_directMode) {
|
||||
forceResize = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_directMode) {
|
||||
directScrapeOutput(info);
|
||||
} else {
|
||||
scrollingScrapeOutput(info);
|
||||
}
|
||||
|
||||
if (forceResize) {
|
||||
resizeImpl(info);
|
||||
}
|
||||
|
||||
m_console->reposition(newBufferSize, newWindowRect);
|
||||
unfreezeConsole();
|
||||
}
|
||||
|
||||
void Agent::scrapeOutput()
|
||||
void Agent::syncConsoleTitle()
|
||||
{
|
||||
freezeConsole();
|
||||
|
||||
std::wstring newTitle = m_console->title();
|
||||
if (newTitle != m_currentTitle) {
|
||||
std::string command = std::string("\x1b]0;") +
|
||||
@ -407,9 +555,47 @@ void Agent::scrapeOutput()
|
||||
m_dataSocket->write(command.c_str());
|
||||
m_currentTitle = newTitle;
|
||||
}
|
||||
}
|
||||
|
||||
const Coord cursor = m_console->cursorPosition();
|
||||
void Agent::directScrapeOutput(const ConsoleScreenBufferInfo &info)
|
||||
{
|
||||
const Coord cursor = info.cursorPosition();
|
||||
const SmallRect windowRect = m_console->windowRect();
|
||||
const int stopLine = std::min(windowRect.height(), m_ptySize.Y);
|
||||
bool sawModifiedLine = false;
|
||||
|
||||
for (int line = 0; line < stopLine; ++line) {
|
||||
CHAR_INFO curLine[MAX_CONSOLE_WIDTH]; // TODO: bufoverflow
|
||||
const int w = std::min(windowRect.width(), m_ptySize.X);
|
||||
m_console->read(SmallRect(0, windowRect.top() + line, w, 1), curLine);
|
||||
|
||||
// TODO: The memcpy can overflow the m_bufferData buffer.
|
||||
CHAR_INFO (&bufLine)[MAX_CONSOLE_WIDTH] =
|
||||
m_bufferData[line % BUFFER_LINE_COUNT];
|
||||
if (sawModifiedLine ||
|
||||
line > m_maxBufferedLine ||
|
||||
memcmp(curLine, bufLine, sizeof(CHAR_INFO) * w) != 0) {
|
||||
//trace("sent line %d", line);
|
||||
m_terminal->sendLine(line, curLine, windowRect.width());
|
||||
memset(bufLine, 0, sizeof(bufLine));
|
||||
memcpy(bufLine, curLine, sizeof(CHAR_INFO) * w);
|
||||
for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) {
|
||||
bufLine[col].Attributes = curLine[w - 1].Attributes;
|
||||
bufLine[col].Char.UnicodeChar = ' ';
|
||||
}
|
||||
m_maxBufferedLine = std::max(m_maxBufferedLine, line);
|
||||
sawModifiedLine = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_terminal->finishOutput(std::pair<int, int>(cursor.X,
|
||||
cursor.Y - windowRect.top()));
|
||||
}
|
||||
|
||||
void Agent::scrollingScrapeOutput(const ConsoleScreenBufferInfo &info)
|
||||
{
|
||||
const Coord cursor = info.cursorPosition();
|
||||
const SmallRect windowRect = info.windowRect();
|
||||
|
||||
if (m_syncRow != -1) {
|
||||
// If a synchronizing marker was placed into the history, look for it
|
||||
@ -426,7 +612,7 @@ void Agent::scrapeOutput()
|
||||
m_scrolledCount += (m_syncRow - markerRow);
|
||||
m_syncRow = markerRow;
|
||||
// If the buffer has scrolled, then the entire window is dirty.
|
||||
markEntireWindowDirty();
|
||||
markEntireWindowDirty(windowRect);
|
||||
}
|
||||
}
|
||||
|
||||
@ -438,7 +624,7 @@ void Agent::scrapeOutput()
|
||||
if (m_dirtyWindowTop != -1) {
|
||||
if (windowRect.top() > m_dirtyWindowTop) {
|
||||
// The window has moved down, presumably as a result of scrolling.
|
||||
markEntireWindowDirty();
|
||||
markEntireWindowDirty(windowRect);
|
||||
} else if (windowRect.top() < m_dirtyWindowTop) {
|
||||
// The window has moved upward. This is generally not expected to
|
||||
// happen, but the CMD/PowerShell CLS command will move the window
|
||||
@ -457,10 +643,10 @@ void Agent::scrapeOutput()
|
||||
// Note that it's possible for all the lines on the current window to
|
||||
// be non-dirty.
|
||||
|
||||
int firstLine = std::min(m_scrapedLineCount,
|
||||
windowRect.top() + m_scrolledCount);
|
||||
int stopLine = std::min(m_dirtyLineCount,
|
||||
windowRect.top() + windowRect.height()) +
|
||||
const int firstLine = std::min(m_scrapedLineCount,
|
||||
windowRect.top() + m_scrolledCount);
|
||||
const int stopLine = std::min(m_dirtyLineCount,
|
||||
windowRect.top() + windowRect.height()) +
|
||||
m_scrolledCount;
|
||||
|
||||
bool sawModifiedLine = false;
|
||||
@ -468,22 +654,26 @@ void Agent::scrapeOutput()
|
||||
for (int line = firstLine; line < stopLine; ++line) {
|
||||
CHAR_INFO curLine[MAX_CONSOLE_WIDTH]; // TODO: bufoverflow
|
||||
const int w = windowRect.width();
|
||||
ASSERT(w >= 1 && w <= MAX_CONSOLE_WIDTH);
|
||||
m_console->read(SmallRect(0, line - m_scrolledCount, w, 1), curLine);
|
||||
|
||||
CHAR_INFO trailing;
|
||||
memset(&trailing, 0, sizeof(trailing));
|
||||
trailing.Attributes = curLine[w - 1].Attributes;
|
||||
trailing.Char.UnicodeChar = L' ';
|
||||
for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) {
|
||||
curLine[col] = trailing;
|
||||
}
|
||||
|
||||
// TODO: The memcpy can overflow the m_bufferData buffer.
|
||||
CHAR_INFO (&bufLine)[MAX_CONSOLE_WIDTH] =
|
||||
m_bufferData[line % BUFFER_LINE_COUNT];
|
||||
if (sawModifiedLine ||
|
||||
line > m_maxBufferedLine ||
|
||||
memcmp(curLine, bufLine, sizeof(CHAR_INFO) * w) != 0) {
|
||||
memcmp(curLine, bufLine, sizeof(curLine)) != 0) {
|
||||
//trace("sent line %d", line);
|
||||
m_terminal->sendLine(line, curLine, windowRect.width());
|
||||
memset(bufLine, 0, sizeof(bufLine));
|
||||
memcpy(bufLine, curLine, sizeof(CHAR_INFO) * w);
|
||||
for (int col = w; col < MAX_CONSOLE_WIDTH; ++col) {
|
||||
bufLine[col].Attributes = curLine[w - 1].Attributes;
|
||||
bufLine[col].Char.UnicodeChar = L' ';
|
||||
}
|
||||
memcpy(bufLine, curLine, sizeof(curLine));
|
||||
m_maxBufferedLine = std::max(m_maxBufferedLine, line);
|
||||
sawModifiedLine = true;
|
||||
}
|
||||
@ -501,8 +691,14 @@ void Agent::scrapeOutput()
|
||||
|
||||
m_terminal->finishOutput(std::pair<int, int>(cursor.X,
|
||||
cursor.Y + m_scrolledCount));
|
||||
}
|
||||
|
||||
unfreezeConsole();
|
||||
void Agent::reopenConsole()
|
||||
{
|
||||
// Reopen CONOUT. The application may have changed the active screen
|
||||
// buffer. (See https://github.com/rprichard/winpty/issues/34)
|
||||
delete m_console;
|
||||
m_console = new Win32Console();
|
||||
}
|
||||
|
||||
void Agent::freezeConsole()
|
||||
@ -553,7 +749,8 @@ void Agent::createSyncMarker(int row)
|
||||
|
||||
// Clear the lines around the marker to ensure that Windows 10's rewrapping
|
||||
// does not affect the marker.
|
||||
m_console->clearLines(row - 1, SYNC_MARKER_LEN + 1);
|
||||
m_console->clearLines(row - 1, SYNC_MARKER_LEN + 1,
|
||||
m_console->bufferInfo());
|
||||
|
||||
// Write a new marker.
|
||||
m_syncCounter++;
|
||||
|
@ -25,15 +25,19 @@
|
||||
#include <windows.h>
|
||||
#include "EventLoop.h"
|
||||
#include "DsrSender.h"
|
||||
#include "Coord.h"
|
||||
#include "SmallRect.h"
|
||||
|
||||
class Win32Console;
|
||||
class ConsoleInput;
|
||||
class Terminal;
|
||||
class ReadBuffer;
|
||||
class NamedPipe;
|
||||
struct ConsoleScreenBufferInfo;
|
||||
|
||||
const int BUFFER_LINE_COUNT = 3000; // TODO: Use something like 9000.
|
||||
const int MAX_CONSOLE_WIDTH = 500;
|
||||
const int SYNC_MARKER_LEN = 16;
|
||||
|
||||
class Agent : public EventLoop, public DsrSender
|
||||
{
|
||||
@ -61,10 +65,16 @@ protected:
|
||||
virtual void onPipeIo(NamedPipe *namedPipe);
|
||||
|
||||
private:
|
||||
void markEntireWindowDirty();
|
||||
void markEntireWindowDirty(const SmallRect &windowRect);
|
||||
void scanForDirtyLines();
|
||||
void clearBufferLines(int firstRow, int count, WORD attributes);
|
||||
void resizeImpl(const ConsoleScreenBufferInfo &origInfo);
|
||||
void resizeWindow(int cols, int rows);
|
||||
void scrapeOutput();
|
||||
void syncConsoleContentAndSize(bool forceResize);
|
||||
void syncConsoleTitle();
|
||||
void directScrapeOutput(const ConsoleScreenBufferInfo &info);
|
||||
void scrollingScrapeOutput(const ConsoleScreenBufferInfo &info);
|
||||
void reopenConsole();
|
||||
void freezeConsole();
|
||||
void unfreezeConsole();
|
||||
void syncMarkerText(CHAR_INFO *output);
|
||||
@ -84,6 +94,8 @@ private:
|
||||
int m_syncRow;
|
||||
int m_syncCounter;
|
||||
|
||||
bool m_directMode;
|
||||
Coord m_ptySize;
|
||||
int m_scrapedLineCount;
|
||||
int m_scrolledCount;
|
||||
int m_maxBufferedLine;
|
||||
|
@ -61,8 +61,8 @@ ConsoleInput::KeyDescriptor ConsoleInput::keyDescriptorTable[] = {
|
||||
{ ESC"OF", VK_END, 0, 0, }, // gnome-terminal
|
||||
};
|
||||
|
||||
ConsoleInput::ConsoleInput(Win32Console *console, DsrSender *dsrSender) :
|
||||
m_console(console),
|
||||
ConsoleInput::ConsoleInput(DsrSender *dsrSender) :
|
||||
m_console(new Win32Console),
|
||||
m_dsrSender(dsrSender),
|
||||
m_dsrSent(false),
|
||||
lastWriteTick(0)
|
||||
@ -166,6 +166,11 @@ ConsoleInput::ConsoleInput(Win32Console *console, DsrSender *dsrSender) :
|
||||
}
|
||||
}
|
||||
|
||||
ConsoleInput::~ConsoleInput()
|
||||
{
|
||||
delete m_console;
|
||||
}
|
||||
|
||||
void ConsoleInput::writeInput(const std::string &input)
|
||||
{
|
||||
trace("writeInput: %d bytes", input.size());
|
||||
|
@ -31,7 +31,8 @@ class DsrSender;
|
||||
class ConsoleInput
|
||||
{
|
||||
public:
|
||||
ConsoleInput(Win32Console *console, DsrSender *dsrSender);
|
||||
ConsoleInput(DsrSender *dsrSender);
|
||||
~ConsoleInput();
|
||||
void writeInput(const std::string &input);
|
||||
void flushIncompleteEscapeCode();
|
||||
|
||||
|
@ -85,6 +85,18 @@ struct SmallRect : SMALL_RECT
|
||||
std::max(0, y2 - y1 + 1));
|
||||
}
|
||||
|
||||
SmallRect ensureLineIncluded(SHORT line) const
|
||||
{
|
||||
const SHORT h = height();
|
||||
if (line < Top) {
|
||||
return SmallRect(Left, line, width(), h);
|
||||
} else if (line > Bottom) {
|
||||
return SmallRect(Left, line - h + 1, width(), h);
|
||||
} else {
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
|
||||
SHORT top() const { return Top; }
|
||||
SHORT left() const { return Left; }
|
||||
SHORT width() const { return Right - Left + 1; }
|
||||
|
@ -65,8 +65,11 @@ void Terminal::setConsoleMode(int mode)
|
||||
|
||||
void Terminal::reset(bool sendClearFirst, int newLine)
|
||||
{
|
||||
if (sendClearFirst && !m_consoleMode)
|
||||
if (sendClearFirst && !m_consoleMode) {
|
||||
// 1;1H ==> move cursor to top-left position
|
||||
// 2J ==> clear the entire screen
|
||||
m_output->write(CSI"1;1H"CSI"2J");
|
||||
}
|
||||
m_remoteLine = newLine;
|
||||
m_cursorHidden = false;
|
||||
m_cursorPos = std::pair<int, int>(0, newLine);
|
||||
|
@ -55,12 +55,15 @@ namespace {
|
||||
Win32Console::Win32Console() : m_titleWorkBuf(16)
|
||||
{
|
||||
m_conin = GetStdHandle(STD_INPUT_HANDLE);
|
||||
m_conout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
m_conout = CreateFileW(L"CONOUT$",
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
NULL, OPEN_EXISTING, 0, NULL);
|
||||
ASSERT(m_conout != INVALID_HANDLE_VALUE);
|
||||
}
|
||||
|
||||
Win32Console::~Win32Console()
|
||||
{
|
||||
CloseHandle(m_conin);
|
||||
CloseHandle(m_conout);
|
||||
}
|
||||
|
||||
@ -314,14 +317,12 @@ void Win32Console::dumpConsoleFont(const char *prefix)
|
||||
}
|
||||
}
|
||||
|
||||
void Win32Console::clearLines(int row, int count)
|
||||
void Win32Console::clearLines(
|
||||
int row,
|
||||
int count,
|
||||
const ConsoleScreenBufferInfo &info)
|
||||
{
|
||||
// TODO: error handling
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (!GetConsoleScreenBufferInfo(m_conout, &info)) {
|
||||
trace("GetConsoleScreenBufferInfo failed");
|
||||
}
|
||||
const int width = SmallRect(info.srWindow).width();
|
||||
DWORD actual = 0;
|
||||
if (!FillConsoleOutputCharacterW(
|
||||
@ -372,42 +373,6 @@ void Win32Console::moveWindow(const SmallRect &rect)
|
||||
}
|
||||
}
|
||||
|
||||
void Win32Console::reposition(const Coord &newBufferSize,
|
||||
const SmallRect &newWindowRect)
|
||||
{
|
||||
// Windows has one API for resizing the screen buffer and a different one
|
||||
// for resizing the window. It seems that either API can fail if the
|
||||
// window does not fit on the screen buffer.
|
||||
|
||||
const SmallRect origWindowRect(windowRect());
|
||||
const SmallRect origBufferRect(Coord(), bufferSize());
|
||||
|
||||
ASSERT(!newBufferSize.isEmpty());
|
||||
SmallRect bufferRect(Coord(), newBufferSize);
|
||||
ASSERT(bufferRect.contains(newWindowRect));
|
||||
|
||||
SmallRect tempWindowRect = origWindowRect.intersected(bufferRect);
|
||||
if (tempWindowRect.width() <= 0) {
|
||||
tempWindowRect.setLeft(newBufferSize.X - 1);
|
||||
tempWindowRect.setWidth(1);
|
||||
}
|
||||
if (tempWindowRect.height() <= 0) {
|
||||
tempWindowRect.setTop(newBufferSize.Y - 1);
|
||||
tempWindowRect.setHeight(1);
|
||||
}
|
||||
|
||||
// Alternatively, if we can immediately use the new window size,
|
||||
// do that instead.
|
||||
if (origBufferRect.contains(newWindowRect))
|
||||
tempWindowRect = newWindowRect;
|
||||
|
||||
if (tempWindowRect != origWindowRect)
|
||||
moveWindow(tempWindowRect);
|
||||
resizeBuffer(newBufferSize);
|
||||
if (newWindowRect != tempWindowRect)
|
||||
moveWindow(newWindowRect);
|
||||
}
|
||||
|
||||
Coord Win32Console::cursorPosition()
|
||||
{
|
||||
return bufferInfo().dwCursorPosition;
|
||||
|
@ -35,15 +35,9 @@ struct ConsoleScreenBufferInfo : CONSOLE_SCREEN_BUFFER_INFO
|
||||
memset(this, 0, sizeof(*this));
|
||||
}
|
||||
|
||||
Coord bufferSize() const
|
||||
{
|
||||
return dwSize;
|
||||
}
|
||||
|
||||
SmallRect windowRect() const
|
||||
{
|
||||
return srWindow;
|
||||
}
|
||||
Coord bufferSize() const { return dwSize; }
|
||||
SmallRect windowRect() const { return srWindow; }
|
||||
Coord cursorPosition() const { return dwCursorPosition; }
|
||||
};
|
||||
|
||||
class Win32Console
|
||||
@ -63,7 +57,7 @@ private:
|
||||
bool setSmallConsoleFontVista();
|
||||
void dumpConsoleFont(const char *prefix);
|
||||
public:
|
||||
void clearLines(int row, int count);
|
||||
void clearLines(int row, int count, const ConsoleScreenBufferInfo &info);
|
||||
|
||||
// Buffer and window sizes.
|
||||
ConsoleScreenBufferInfo bufferInfo();
|
||||
@ -71,7 +65,6 @@ public:
|
||||
SmallRect windowRect();
|
||||
void resizeBuffer(const Coord &size);
|
||||
void moveWindow(const SmallRect &rect);
|
||||
void reposition(const Coord &bufferSize, const SmallRect &windowRect);
|
||||
|
||||
// Cursor.
|
||||
Coord cursorPosition();
|
||||
|
91
misc/BufferResizeTests.cc
Executable file
91
misc/BufferResizeTests.cc
Executable file
@ -0,0 +1,91 @@
|
||||
#include <windows.h>
|
||||
#include <cassert>
|
||||
|
||||
#include "../shared/DebugClient.cc"
|
||||
#include "TestUtil.cc"
|
||||
|
||||
void dumpInfoToTrace() {
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
assert(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info));
|
||||
trace("win=(%d,%d,%d,%d)",
|
||||
(int)info.srWindow.Left,
|
||||
(int)info.srWindow.Top,
|
||||
(int)info.srWindow.Right,
|
||||
(int)info.srWindow.Bottom);
|
||||
trace("buf=(%d,%d)",
|
||||
(int)info.dwSize.X,
|
||||
(int)info.dwSize.Y);
|
||||
trace("cur=(%d,%d)",
|
||||
(int)info.dwCursorPosition.X,
|
||||
(int)info.dwCursorPosition.Y);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc == 1) {
|
||||
startChildProcess(L"CHILD");
|
||||
return 0;
|
||||
}
|
||||
|
||||
setWindowPos(0, 0, 1, 1);
|
||||
|
||||
if (false) {
|
||||
// Reducing the buffer height can move the window up.
|
||||
setBufferSize(80, 25);
|
||||
setWindowPos(0, 20, 80, 5);
|
||||
Sleep(2000);
|
||||
setBufferSize(80, 10);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
// Reducing the buffer height moves the window up and the buffer
|
||||
// contents up too.
|
||||
setBufferSize(80, 25);
|
||||
setWindowPos(0, 20, 80, 5);
|
||||
setCursorPos(0, 20);
|
||||
printf("TEST1\nTEST2\nTEST3\nTEST4\n");
|
||||
fflush(stdout);
|
||||
Sleep(2000);
|
||||
setBufferSize(80, 10);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
// Reducing the buffer width can move the window left.
|
||||
setBufferSize(80, 25);
|
||||
setWindowPos(40, 0, 40, 25);
|
||||
Sleep(2000);
|
||||
setBufferSize(60, 25);
|
||||
}
|
||||
|
||||
if (false) {
|
||||
// Sometimes the buffer contents are shifted up; sometimes they're
|
||||
// shifted down. It seems to depend on the cursor position?
|
||||
|
||||
// setBufferSize(80, 25);
|
||||
// setWindowPos(0, 20, 80, 5);
|
||||
// setCursorPos(0, 20);
|
||||
// printf("TESTa\nTESTb\nTESTc\nTESTd\nTESTe");
|
||||
// fflush(stdout);
|
||||
// setCursorPos(0, 0);
|
||||
// printf("TEST1\nTEST2\nTEST3\nTEST4\nTEST5");
|
||||
// fflush(stdout);
|
||||
// setCursorPos(0, 24);
|
||||
// Sleep(5000);
|
||||
// setBufferSize(80, 24);
|
||||
|
||||
setBufferSize(80, 20);
|
||||
setWindowPos(0, 10, 80, 10);
|
||||
setCursorPos(0, 18);
|
||||
|
||||
printf("TEST1\nTEST2");
|
||||
fflush(stdout);
|
||||
setCursorPos(0, 18);
|
||||
|
||||
Sleep(2000);
|
||||
setBufferSize(80, 18);
|
||||
}
|
||||
|
||||
dumpInfoToTrace();
|
||||
Sleep(30000);
|
||||
|
||||
return 0;
|
||||
}
|
54
misc/ChangeScreenBuffer.cc
Executable file
54
misc/ChangeScreenBuffer.cc
Executable file
@ -0,0 +1,54 @@
|
||||
// A test program for CreateConsoleScreenBuffer / SetConsoleActiveScreenBuffer
|
||||
//
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <io.h>
|
||||
#include <cassert>
|
||||
|
||||
#include "../shared/DebugClient.cc"
|
||||
#include "TestUtil.cc"
|
||||
|
||||
int main()
|
||||
{
|
||||
HANDLE origBuffer = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
HANDLE childBuffer = CreateConsoleScreenBuffer(
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
|
||||
|
||||
SetConsoleActiveScreenBuffer(childBuffer);
|
||||
|
||||
while (true) {
|
||||
char buf[1024];
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
|
||||
assert(GetConsoleScreenBufferInfo(origBuffer, &info));
|
||||
trace("child.size=(%d,%d)", (int)info.dwSize.X, (int)info.dwSize.Y);
|
||||
trace("child.cursor=(%d,%d)", (int)info.dwCursorPosition.X, (int)info.dwCursorPosition.Y);
|
||||
trace("child.window=(%d,%d,%d,%d)",
|
||||
(int)info.srWindow.Left, (int)info.srWindow.Top,
|
||||
(int)info.srWindow.Right, (int)info.srWindow.Bottom);
|
||||
trace("child.maxSize=(%d,%d)", (int)info.dwMaximumWindowSize.X, (int)info.dwMaximumWindowSize.Y);
|
||||
|
||||
int ch = getch();
|
||||
sprintf(buf, "%02x\n", ch);
|
||||
DWORD actual = 0;
|
||||
WriteFile(childBuffer, buf, strlen(buf), &actual, NULL);
|
||||
if (ch == 0x1b/*ESC*/ || ch == 0x03/*CTRL-C*/)
|
||||
break;
|
||||
|
||||
if (ch == 'b') {
|
||||
setBufferSize(origBuffer, 40, 25);
|
||||
} else if (ch == 'w') {
|
||||
setWindowPos(origBuffer, 1, 1, 38, 23);
|
||||
} else if (ch == 'c') {
|
||||
setCursorPos(origBuffer, 10, 10);
|
||||
}
|
||||
}
|
||||
|
||||
SetConsoleActiveScreenBuffer(origBuffer);
|
||||
|
||||
return 0;
|
||||
}
|
5
misc/DumpLines.py
Executable file
5
misc/DumpLines.py
Executable file
@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
|
||||
for i in range(1, int(sys.argv[1]) + 1):
|
||||
print i, "X" * 78
|
11
misc/SetCursorPos.cc
Executable file
11
misc/SetCursorPos.cc
Executable file
@ -0,0 +1,11 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include "TestUtil.cc"
|
||||
#include "../shared/DebugClient.cc"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int col = atoi(argv[1]);
|
||||
int row = atoi(argv[2]);
|
||||
setCursorPos(col, row);
|
||||
return 0;
|
||||
}
|
@ -29,26 +29,35 @@ static void startChildProcess(const wchar_t *args) {
|
||||
&sui, &pi);
|
||||
}
|
||||
|
||||
static void setBufferSize(int x, int y) {
|
||||
static void setBufferSize(HANDLE conout, int x, int y) {
|
||||
COORD size = { x, y };
|
||||
HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
BOOL success = SetConsoleScreenBufferSize(conout, size);
|
||||
trace("setBufferSize: (%d,%d), result=%d", x, y, success);
|
||||
}
|
||||
|
||||
static void setWindowPos(int x, int y, int w, int h) {
|
||||
static void setWindowPos(HANDLE conout, int x, int y, int w, int h) {
|
||||
SMALL_RECT r = { x, y, x + w - 1, y + h - 1 };
|
||||
HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
BOOL success = SetConsoleWindowInfo(conout, /*bAbsolute=*/TRUE, &r);
|
||||
trace("setWindowPos: (%d,%d,%d,%d), result=%d", x, y, w, h, success);
|
||||
}
|
||||
|
||||
static void setCursorPos(int x, int y) {
|
||||
static void setCursorPos(HANDLE conout, int x, int y) {
|
||||
COORD coord = { x, y };
|
||||
HANDLE conout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
SetConsoleCursorPosition(conout, coord);
|
||||
}
|
||||
|
||||
static void setBufferSize(int x, int y) {
|
||||
setBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), x, y);
|
||||
}
|
||||
|
||||
static void setWindowPos(int x, int y, int w, int h) {
|
||||
setWindowPos(GetStdHandle(STD_OUTPUT_HANDLE), x, y, w, h);
|
||||
}
|
||||
|
||||
static void setCursorPos(int x, int y) {
|
||||
setCursorPos(GetStdHandle(STD_OUTPUT_HANDLE), x, y);
|
||||
}
|
||||
|
||||
static void countDown(int sec) {
|
||||
for (int i = sec; i > 0; --i) {
|
||||
printf("%d.. ", i);
|
||||
@ -58,7 +67,7 @@ static void countDown(int sec) {
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static void fillBox(int x, int y, int w, int h, char ch, int attributes=7) {
|
||||
static void writeBox(int x, int y, int w, int h, char ch, int attributes=7) {
|
||||
CHAR_INFO info = { 0 };
|
||||
info.Char.AsciiChar = ch;
|
||||
info.Attributes = attributes;
|
||||
@ -71,7 +80,15 @@ static void fillBox(int x, int y, int w, int h, char ch, int attributes=7) {
|
||||
}
|
||||
|
||||
static void setChar(int x, int y, char ch, int attributes=7) {
|
||||
fillBox(x, y, 1, 1, ch, attributes);
|
||||
writeBox(x, y, 1, 1, ch, attributes);
|
||||
}
|
||||
|
||||
static void fillChar(int x, int y, int repeat, char ch) {
|
||||
COORD coord = { x, y };
|
||||
DWORD actual = 0;
|
||||
FillConsoleOutputCharacterA(
|
||||
GetStdHandle(STD_OUTPUT_HANDLE),
|
||||
ch, repeat, coord, &actual);
|
||||
}
|
||||
|
||||
static void repeatChar(int count, char ch) {
|
||||
|
@ -25,7 +25,7 @@ int main(int argc, char *argv[]) {
|
||||
repeatChar(WIDTH * 5, '.');
|
||||
repeatChar(10, '\n');
|
||||
setWindowPos(0, 20, WIDTH, 20);
|
||||
fillBox(0, 5, 1, 10, '|');
|
||||
writeBox(0, 5, 1, 10, '|');
|
||||
|
||||
Sleep(120000);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user