Files
nzbget/daemon/frontend/NCursesFrontend.cpp
Andrey Prygunkov 9e2d8544da #126: full use of class BString
1) replaced characters arrays with class BString through the whole
program. The string formatting code has become much cleaner.
2) class Util returns error message via CString instead of character
buffers.
3) few more places to use CString.
2015-12-19 18:43:52 +01:00

1403 lines
34 KiB
C++

/*
* This file is part of nzbget
*
* Copyright (C) 2004 Sven Henkel <sidddy@users.sourceforge.net>
* Copyright (C) 2007-2015 Andrey Prygunkov <hugbug@users.sourceforge.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $Revision$
* $Date$
*
*/
#include "nzbget.h"
#ifndef DISABLE_CURSES
// "ncurses.h" contains many global defines such as for "OK" or "clear" which we sure don't want
// everywhere in the project. For that reason we include "ncurses.h" directly here instead of
// putting it into global header file "nzbget.h".
#ifdef HAVE_NCURSES_H
#include <ncurses.h>
#endif
#ifdef HAVE_NCURSES_NCURSES_H
#include <ncurses/ncurses.h>
#endif
#include "NCursesFrontend.h"
#include "Options.h"
#include "Util.h"
#ifndef WIN32
// curses.h on Solaris declares "clear()" via DEFINE. That causes problems, because
// it also affects calls to deque's method "clear()", producing compiler errors.
// We use function "curses_clear()" to call macro "clear" of curses, then
// undefine macro "clear".
void curses_clear()
{
clear();
}
#undef clear
#endif
extern void ExitProc();
static const int NCURSES_COLORPAIR_TEXT = 1;
static const int NCURSES_COLORPAIR_INFO = 2;
static const int NCURSES_COLORPAIR_WARNING = 3;
static const int NCURSES_COLORPAIR_ERROR = 4;
static const int NCURSES_COLORPAIR_DEBUG = 5;
static const int NCURSES_COLORPAIR_DETAIL = 6;
static const int NCURSES_COLORPAIR_STATUS = 7;
static const int NCURSES_COLORPAIR_KEYBAR = 8;
static const int NCURSES_COLORPAIR_INFOLINE = 9;
static const int NCURSES_COLORPAIR_TEXTHIGHL = 10;
static const int NCURSES_COLORPAIR_CURSOR = 11;
static const int NCURSES_COLORPAIR_HINT = 12;
static const int MAX_SCREEN_WIDTH = 512;
#ifdef WIN32
static const int COLOR_BLACK = 0;
static const int COLOR_BLUE = FOREGROUND_BLUE;
static const int COLOR_RED = FOREGROUND_RED;
static const int COLOR_GREEN = FOREGROUND_GREEN;
static const int COLOR_WHITE = FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_GREEN;
static const int COLOR_MAGENTA = FOREGROUND_RED | FOREGROUND_BLUE;
static const int COLOR_CYAN = FOREGROUND_BLUE | FOREGROUND_GREEN;
static const int COLOR_YELLOW = FOREGROUND_RED | FOREGROUND_GREEN;
static const int READKEY_EMPTY = 0;
#define KEY_DOWN VK_DOWN
#define KEY_UP VK_UP
#define KEY_PPAGE VK_PRIOR
#define KEY_NPAGE VK_NEXT
#define KEY_END VK_END
#define KEY_HOME VK_HOME
#define KEY_BACKSPACE VK_BACK
#else
static const int READKEY_EMPTY = ERR;
#endif
NCursesFrontend::NCursesFrontend()
{
m_screenHeight = 0;
m_screenWidth = 0;
m_inputNumberIndex = 0;
m_inputMode = normal;
m_summary = true;
m_fileList = true;
m_neededLogEntries = 0;
m_queueWinTop = 0;
m_queueWinHeight = 0;
m_queueWinClientHeight = 0;
m_messagesWinTop = 0;
m_messagesWinHeight = 0;
m_messagesWinClientHeight = 0;
m_selectedQueueEntry = 0;
m_queueScrollOffset = 0;
m_showNzbname = g_Options->GetCursesNzbName();
m_showTimestamp = g_Options->GetCursesTime();
m_groupFiles = g_Options->GetCursesGroup();
m_queueWindowPercentage = 50;
m_dataUpdatePos = 0;
m_updateNextTime = false;
m_lastEditEntry = -1;
m_lastPausePars = false;
// Setup curses
#ifdef WIN32
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
m_screenBuffer = NULL;
m_oldScreenBuffer = NULL;
m_colorAttr.clear();
CONSOLE_CURSOR_INFO ConsoleCursorInfo;
GetConsoleCursorInfo(hConsole, &ConsoleCursorInfo);
ConsoleCursorInfo.bVisible = false;
SetConsoleCursorInfo(hConsole, &ConsoleCursorInfo);
if (IsRemoteMode())
{
SetConsoleTitle("NZBGet - remote mode");
}
else
{
SetConsoleTitle("NZBGet");
}
m_useColor = true;
#else
m_window = initscr();
if (m_window == NULL)
{
printf("ERROR: m_pWindow == NULL\n");
exit(-1);
}
keypad(stdscr, true);
nodelay((WINDOW*)m_window, true);
noecho();
curs_set(0);
m_useColor = has_colors();
#endif
if (m_useColor)
{
#ifndef WIN32
start_color();
#endif
init_pair(0, COLOR_WHITE, COLOR_BLUE);
init_pair(NCURSES_COLORPAIR_TEXT, COLOR_WHITE, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_INFO, COLOR_GREEN, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_WARNING, COLOR_MAGENTA, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_ERROR, COLOR_RED, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_DEBUG, COLOR_WHITE, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_DETAIL, COLOR_GREEN, COLOR_BLACK);
init_pair(NCURSES_COLORPAIR_STATUS, COLOR_BLUE, COLOR_WHITE);
init_pair(NCURSES_COLORPAIR_KEYBAR, COLOR_WHITE, COLOR_BLUE);
init_pair(NCURSES_COLORPAIR_INFOLINE, COLOR_WHITE, COLOR_BLUE);
init_pair(NCURSES_COLORPAIR_TEXTHIGHL, COLOR_BLACK, COLOR_CYAN);
init_pair(NCURSES_COLORPAIR_CURSOR, COLOR_BLACK, COLOR_YELLOW);
init_pair(NCURSES_COLORPAIR_HINT, COLOR_WHITE, COLOR_RED);
}
}
NCursesFrontend::~NCursesFrontend()
{
#ifdef WIN32
free(m_screenBuffer);
free(m_oldScreenBuffer);
m_colorAttr.clear();
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO ConsoleCursorInfo;
GetConsoleCursorInfo(hConsole, &ConsoleCursorInfo);
ConsoleCursorInfo.bVisible = true;
SetConsoleCursorInfo(hConsole, &ConsoleCursorInfo);
#else
keypad(stdscr, false);
echo();
curs_set(1);
endwin();
#endif
printf("\n");
SetHint(NULL);
}
void NCursesFrontend::Run()
{
debug("Entering NCursesFrontend-loop");
m_dataUpdatePos = 0;
while (!IsStopped())
{
// The data (queue and log) is updated each m_iUpdateInterval msec,
// but the window is updated more often for better reaction on user's input
bool updateNow = false;
int key = ReadConsoleKey();
if (key != READKEY_EMPTY)
{
// Update now and next if a key is pressed.
updateNow = true;
m_updateNextTime = true;
}
else if (m_updateNextTime)
{
// Update due to key being pressed during previous call.
updateNow = true;
m_updateNextTime = false;
}
else if (m_dataUpdatePos <= 0)
{
updateNow = true;
m_updateNextTime = false;
}
if (updateNow)
{
Update(key);
}
if (m_dataUpdatePos <= 0)
{
m_dataUpdatePos = m_updateInterval;
}
usleep(10 * 1000);
m_dataUpdatePos -= 10;
}
FreeData();
debug("Exiting NCursesFrontend-loop");
}
void NCursesFrontend::NeedUpdateData()
{
m_dataUpdatePos = 10;
m_updateNextTime = true;
}
void NCursesFrontend::Update(int key)
{
// Figure out how big the screen is
CalcWindowSizes();
if (m_dataUpdatePos <= 0)
{
FreeData();
m_neededLogEntries = m_messagesWinClientHeight;
if (!PrepareData())
{
return;
}
// recalculate frame sizes
CalcWindowSizes();
}
if (m_inputMode == editQueue)
{
int queueSize = CalcQueueSize();
if (queueSize == 0)
{
m_selectedQueueEntry = 0;
m_inputMode = normal;
}
}
//------------------------------------------
// Print Current NZBInfoList
//------------------------------------------
if (m_queueWinHeight > 0)
{
PrintQueue();
}
//------------------------------------------
// Print Messages
//------------------------------------------
if (m_messagesWinHeight > 0)
{
PrintMessages();
}
PrintStatus();
PrintKeyInputBar();
UpdateInput(key);
RefreshScreen();
}
void NCursesFrontend::CalcWindowSizes()
{
int nrRows, nrColumns;
#ifdef WIN32
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO BufInfo;
GetConsoleScreenBufferInfo(hConsole, &BufInfo);
nrRows = BufInfo.srWindow.Bottom - BufInfo.srWindow.Top + 1;
nrColumns = BufInfo.srWindow.Right - BufInfo.srWindow.Left + 1;
#else
getmaxyx(stdscr, nrRows, nrColumns);
#endif
if (nrRows != m_screenHeight || nrColumns != m_screenWidth)
{
#ifdef WIN32
m_screenBufferSize = nrRows * nrColumns * sizeof(CHAR_INFO);
m_screenBuffer = (CHAR_INFO*)realloc(m_screenBuffer, m_screenBufferSize);
memset(m_screenBuffer, 0, m_screenBufferSize);
m_oldScreenBuffer = (CHAR_INFO*)realloc(m_oldScreenBuffer, m_screenBufferSize);
memset(m_oldScreenBuffer, 0, m_screenBufferSize);
#else
curses_clear();
#endif
m_screenHeight = nrRows;
m_screenWidth = nrColumns;
}
int queueSize = CalcQueueSize();
m_queueWinTop = 0;
m_queueWinHeight = (m_screenHeight - 2) * m_queueWindowPercentage / 100;
if (m_queueWinHeight - 1 > queueSize)
{
m_queueWinHeight = queueSize > 0 ? queueSize + 1 : 1 + 1;
}
m_queueWinClientHeight = m_queueWinHeight - 1;
if (m_queueWinClientHeight < 0)
{
m_queueWinClientHeight = 0;
}
m_messagesWinTop = m_queueWinTop + m_queueWinHeight;
m_messagesWinHeight = m_screenHeight - m_queueWinHeight - 2;
m_messagesWinClientHeight = m_messagesWinHeight - 1;
if (m_messagesWinClientHeight < 0)
{
m_messagesWinClientHeight = 0;
}
}
int NCursesFrontend::CalcQueueSize()
{
int queueSize = 0;
DownloadQueue* downloadQueue = LockQueue();
if (m_groupFiles)
{
queueSize = downloadQueue->GetQueue()->size();
}
else
{
for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); it++)
{
NzbInfo* nzbInfo = *it;
queueSize += nzbInfo->GetFileList()->size();
}
}
UnlockQueue();
return queueSize;
}
void NCursesFrontend::PlotLine(const char * string, int row, int pos, int colorPair)
{
BString<1024> buffer("%-*s", m_screenWidth, string);
int len = buffer.Length();
if (len > m_screenWidth - pos && m_screenWidth - pos < MAX_SCREEN_WIDTH)
{
buffer[m_screenWidth - pos] = '\0';
}
PlotText(buffer, row, pos, colorPair, false);
}
void NCursesFrontend::PlotText(const char * string, int row, int pos, int colorPair, bool blink)
{
#ifdef WIN32
int bufPos = row * m_screenWidth + pos;
int len = strlen(string);
for (int i = 0; i < len; i++)
{
char c = string[i];
CharToOemBuff(&c, &c, 1);
m_screenBuffer[bufPos + i].Char.AsciiChar = c;
m_screenBuffer[bufPos + i].Attributes = m_colorAttr[colorPair];
}
#else
if( m_useColor )
{
attron(COLOR_PAIR(colorPair));
if (blink)
{
attron(A_BLINK);
}
}
mvaddstr(row, pos, (char*)string);
if( m_useColor )
{
attroff(COLOR_PAIR(colorPair));
if (blink)
{
attroff(A_BLINK);
}
}
#endif
}
void NCursesFrontend::RefreshScreen()
{
#ifdef WIN32
bool bufChanged = memcmp(m_screenBuffer, m_oldScreenBuffer, m_screenBufferSize);
if (bufChanged)
{
COORD BufSize;
BufSize.X = m_screenWidth;
BufSize.Y = m_screenHeight;
COORD BufCoord;
BufCoord.X = 0;
BufCoord.Y = 0;
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO BufInfo;
GetConsoleScreenBufferInfo(hConsole, &BufInfo);
WriteConsoleOutput(hConsole, m_screenBuffer, BufSize, BufCoord, &BufInfo.srWindow);
BufInfo.dwCursorPosition.X = BufInfo.srWindow.Right;
BufInfo.dwCursorPosition.Y = BufInfo.srWindow.Bottom;
SetConsoleCursorPosition(hConsole, BufInfo.dwCursorPosition);
memcpy(m_oldScreenBuffer, m_screenBuffer, m_screenBufferSize);
}
#else
// Cursor placement
wmove((WINDOW*)m_window, m_screenHeight, m_screenWidth);
// NCurses refresh
refresh();
#endif
}
#ifdef WIN32
void NCursesFrontend::init_pair(int colorNumber, WORD wForeColor, WORD wBackColor)
{
m_colorAttr.resize(colorNumber + 1);
m_colorAttr[colorNumber] = wForeColor | (wBackColor << 4);
}
#endif
void NCursesFrontend::PrintMessages()
{
int lineNr = m_messagesWinTop;
BString<1024> buffer("%s Messages", m_useColor ? "" : "*** ");
PlotLine(buffer, lineNr++, 0, NCURSES_COLORPAIR_INFOLINE);
int line = lineNr + m_messagesWinClientHeight - 1;
int linesToPrint = m_messagesWinClientHeight;
MessageList* messages = LockMessages();
// print messages from bottom
for (int i = (int)messages->size() - 1; i >= 0 && linesToPrint > 0; i--)
{
int printedLines = PrintMessage((*messages)[i], line, linesToPrint);
line -= printedLines;
linesToPrint -= printedLines;
}
if (linesToPrint > 0)
{
// too few messages, print them again from top
line = lineNr + m_messagesWinClientHeight - 1;
while (linesToPrint-- > 0)
{
PlotLine("", line--, 0, NCURSES_COLORPAIR_TEXT);
}
int linesToPrint2 = m_messagesWinClientHeight;
for (int i = (int)messages->size() - 1; i >= 0 && linesToPrint2 > 0; i--)
{
int printedLines = PrintMessage((*messages)[i], line, linesToPrint2);
line -= printedLines;
linesToPrint2 -= printedLines;
}
}
UnlockMessages();
}
int NCursesFrontend::PrintMessage(Message* Msg, int row, int maxLines)
{
const char* messageType[] = { "INFO ", "WARNING ", "ERROR ", "DEBUG ", "DETAIL "};
const int messageTypeColor[] = { NCURSES_COLORPAIR_INFO, NCURSES_COLORPAIR_WARNING,
NCURSES_COLORPAIR_ERROR, NCURSES_COLORPAIR_DEBUG, NCURSES_COLORPAIR_DETAIL };
CString text;
if (m_showTimestamp)
{
int len = strlen(text) + 50;
text = (char*)malloc(len);
time_t rawtime = Msg->GetTime();
rawtime += g_Options->GetTimeCorrection();
BString<100> time;
#ifdef HAVE_CTIME_R_3
ctime_r(&rawtime, time, time.Capacity());
#else
ctime_r(&rawtime, time);
#endif
time[strlen(time) - 1] = '\0'; // trim LF
text.Format("%s - %s", *time, Msg->GetText());
}
else
{
text = Msg->GetText();
}
// replace some special characters with spaces
for (char* p = (char*)text; *p; p++)
{
if (*p == '\n' || *p == '\r' || *p == '\b')
{
*p = ' ';
}
}
int len = strlen(text);
int winWidth = m_screenWidth - 8;
int msgLines = len / winWidth;
if (len % winWidth > 0)
{
msgLines++;
}
int lines = 0;
for (int i = msgLines - 1; i >= 0 && lines < maxLines; i--)
{
int r = row - msgLines + i + 1;
PlotLine(text + winWidth * i, r, 8, NCURSES_COLORPAIR_TEXT);
if (i == 0)
{
PlotText(messageType[Msg->GetKind()], r, 0, messageTypeColor[Msg->GetKind()], false);
}
else
{
PlotText(" ", r, 0, messageTypeColor[Msg->GetKind()], false);
}
lines++;
}
return lines;
}
void NCursesFrontend::PrintStatus()
{
int statusRow = m_screenHeight - 2;
BString<100> timeString;
int currentDownloadSpeed = m_standBy ? 0 : m_currentDownloadSpeed;
if (currentDownloadSpeed > 0 && !m_pauseDownload)
{
int64 remain_sec = (int64)(m_remainingSize / currentDownloadSpeed);
int h = (int)(remain_sec / 3600);
int m = (int)((remain_sec % 3600) / 60);
int s = (int)(remain_sec % 60);
timeString.Format(" (~ %.2d:%.2d:%.2d)", h, m, s);
}
BString<100> downloadLimit;
if (m_downloadLimit > 0)
{
downloadLimit.Format(", Limit %i KB/s", m_downloadLimit / 1024);
}
BString<100> postStatus;
if (m_postJobCount > 0)
{
postStatus.Format(", %i post-job%s", m_postJobCount, m_postJobCount > 1 ? "s" : "");
}
int averageSpeed = (int)(m_dnTimeSec > 0 ? m_allBytes / m_dnTimeSec : 0);
BString<1024> status(" %d threads, %s, %s remaining%s%s%s%s, Avg. %s",
m_threadCount, *Util::FormatSpeed(currentDownloadSpeed),
*Util::FormatSize(m_remainingSize),
*timeString, *postStatus, m_pauseDownload ? (m_standBy ? ", Paused" : ", Pausing") : "",
*downloadLimit, *Util::FormatSpeed(averageSpeed));
PlotLine(status, statusRow, 0, NCURSES_COLORPAIR_STATUS);
}
void NCursesFrontend::PrintKeyInputBar()
{
int queueSize = CalcQueueSize();
int inputBarRow = m_screenHeight - 1;
if (!m_hint.Empty())
{
time_t time = ::time(NULL);
if (time - m_startHint < 5)
{
PlotLine(m_hint, inputBarRow, 0, NCURSES_COLORPAIR_HINT);
return;
}
else
{
SetHint(NULL);
}
}
switch (m_inputMode)
{
case normal:
if (m_groupFiles)
{
PlotLine("(Q)uit | (E)dit | (P)ause | (R)ate | (W)indow | (G)roup | (T)ime", inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR);
}
else
{
PlotLine("(Q)uit | (E)dit | (P)ause | (R)ate | (W)indow | (G)roup | (T)ime | n(Z)b", inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR);
}
break;
case editQueue:
{
const char* status = NULL;
if (m_selectedQueueEntry > 0 && queueSize > 1 && m_selectedQueueEntry == queueSize - 1)
{
status = "(Q)uit | (E)xit | (P)ause | (D)elete | (U)p/(T)op";
}
else if (queueSize > 1 && m_selectedQueueEntry == 0)
{
status = "(Q)uit | (E)xit | (P)ause | (D)elete | dow(N)/(B)ottom";
}
else if (queueSize > 1)
{
status = "(Q)uit | (E)xit | (P)ause | (D)elete | (U)p/dow(N)/(T)op/(B)ottom";
}
else
{
status = "(Q)uit | (E)xit | (P)ause | (D)elete";
}
PlotLine(status, inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR);
break;
}
case downloadRate:
BString<100> hint("Download rate: %i", m_inputValue);
PlotLine(hint, inputBarRow, 0, NCURSES_COLORPAIR_KEYBAR);
// Print the cursor
PlotText(" ", inputBarRow, 15 + m_inputNumberIndex, NCURSES_COLORPAIR_CURSOR, true);
break;
}
}
void NCursesFrontend::SetHint(const char* hint)
{
m_hint = hint;
if (!m_hint.Empty())
{
m_startHint = time(NULL);
}
}
void NCursesFrontend::PrintQueue()
{
if (m_groupFiles)
{
PrintGroupQueue();
}
else
{
PrintFileQueue();
}
}
void NCursesFrontend::PrintFileQueue()
{
DownloadQueue* downloadQueue = LockQueue();
int lineNr = m_queueWinTop + 1;
int64 remaining = 0;
int64 paused = 0;
int pausedFiles = 0;
int fileNum = 0;
for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); it++)
{
NzbInfo* nzbInfo = *it;
for (FileList::iterator it2 = nzbInfo->GetFileList()->begin(); it2 != nzbInfo->GetFileList()->end(); it2++, fileNum++)
{
FileInfo* fileInfo = *it2;
if (fileNum >= m_queueScrollOffset && fileNum < m_queueScrollOffset + m_queueWinHeight -1)
{
PrintFilename(fileInfo, lineNr++, fileNum == m_selectedQueueEntry);
}
if (fileInfo->GetPaused())
{
pausedFiles++;
paused += fileInfo->GetRemainingSize();
}
remaining += fileInfo->GetRemainingSize();
}
}
if (fileNum > 0)
{
BString<1024> header(" %sFiles for downloading - %i / %i files in queue - %s / %s",
m_useColor ? "" : "*** ", fileNum,
fileNum - pausedFiles,
*Util::FormatSize(remaining), *Util::FormatSize(remaining - paused));
PrintTopHeader(header, m_queueWinTop, true);
}
else
{
lineNr--;
BString<1024> header("%s Files for downloading", m_useColor ? "" : "*** ");
PrintTopHeader(header, lineNr++, true);
PlotLine("Ready to receive nzb-job", lineNr++, 0, NCURSES_COLORPAIR_TEXT);
}
UnlockQueue();
}
void NCursesFrontend::PrintFilename(FileInfo * fileInfo, int row, bool selected)
{
int color = 0;
const char* Brace1 = "[";
const char* Brace2 = "]";
if (m_inputMode == editQueue && selected)
{
color = NCURSES_COLORPAIR_TEXTHIGHL;
if (!m_useColor)
{
Brace1 = "<";
Brace2 = ">";
}
}
else
{
color = NCURSES_COLORPAIR_TEXT;
}
const char* downloading = "";
if (fileInfo->GetActiveDownloads() > 0)
{
downloading = " *";
}
BString<100> priority;
if (fileInfo->GetNzbInfo()->GetPriority() != 0)
{
priority.Format(" [%+i]", fileInfo->GetNzbInfo()->GetPriority());
}
BString<100> completed;
if (fileInfo->GetRemainingSize() < fileInfo->GetSize())
{
completed.Format(", %i%%", (int)(100 - fileInfo->GetRemainingSize() * 100 / fileInfo->GetSize()));
}
BString<1024> nzbNiceName;
if (m_showNzbname)
{
nzbNiceName.Format("%s%c", fileInfo->GetNzbInfo()->GetName(), (int)PATH_SEPARATOR);
}
BString<1024> text("%s%i%s%s%s %s%s (%s%s)%s", Brace1, fileInfo->GetId(),
Brace2, *priority, downloading, *nzbNiceName, fileInfo->GetFilename(),
*Util::FormatSize(fileInfo->GetSize()),
*completed, fileInfo->GetPaused() ? " (paused)" : "");
PlotLine(text, row, 0, color);
}
void NCursesFrontend::PrintTopHeader(char* header, int lineNr, bool upTime)
{
BString<1024> buffer("%-*s", m_screenWidth, header);
int headerLen = strlen(header);
int charsLeft = m_screenWidth - headerLen - 2;
int time = upTime ? m_upTimeSec : m_dnTimeSec;
int d = time / 3600 / 24;
int h = (time % (3600 * 24)) / 3600;
int m = (time % 3600) / 60;
int s = time % 60;
BString<100> timeStr;
if (d == 0)
{
timeStr.Format("%.2d:%.2d:%.2d", h, m, s);
if ((int)strlen(timeStr) > charsLeft)
{
timeStr.Format("%.2d:%.2d", h, m);
}
}
else
{
timeStr.Format("%i %s %.2d:%.2d:%.2d", d, (d == 1 ? "day" : "days"), h, m, s);
if ((int)strlen(timeStr) > charsLeft)
{
timeStr.Format("%id %.2d:%.2d:%.2d", d, h, m, s);
}
if ((int)strlen(timeStr) > charsLeft)
{
timeStr.Format("%id %.2d:%.2d", d, h, m);
}
}
const char* shortCap = upTime ? " Up " : "Dn ";
const char* longCap = upTime ? " Uptime " : " Download-time ";
int timeLen = strlen(timeStr);
int shortCapLen = strlen(shortCap);
int longCapLen = strlen(longCap);
if (charsLeft - timeLen - longCapLen >= 0)
{
snprintf(buffer + m_screenWidth - timeLen - longCapLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen - longCapLen), "%s%s", longCap, *timeStr);
}
else if (charsLeft - timeLen - shortCapLen >= 0)
{
snprintf(buffer + m_screenWidth - timeLen - shortCapLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen - shortCapLen), "%s%s", shortCap, *timeStr);
}
else if (charsLeft - timeLen >= 0)
{
snprintf(buffer + m_screenWidth - timeLen, MAX_SCREEN_WIDTH - (m_screenWidth - timeLen), "%s", *timeStr);
}
PlotLine(buffer, lineNr, 0, NCURSES_COLORPAIR_INFOLINE);
}
void NCursesFrontend::PrintGroupQueue()
{
int lineNr = m_queueWinTop;
DownloadQueue* downloadQueue = LockQueue();
if (downloadQueue->GetQueue()->empty())
{
BString<1024> buffer("%s NZBs for downloading", m_useColor ? "" : "*** ");
PrintTopHeader(buffer, lineNr++, false);
PlotLine("Ready to receive nzb-job", lineNr++, 0, NCURSES_COLORPAIR_TEXT);
}
else
{
lineNr++;
ResetColWidths();
int calcLineNr = lineNr;
int i = 0;
for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); it++, i++)
{
NzbInfo* nzbInfo = *it;
if (i >= m_queueScrollOffset && i < m_queueScrollOffset + m_queueWinHeight -1)
{
PrintGroupname(nzbInfo, calcLineNr++, false, true);
}
}
int64 remaining = 0;
int64 paused = 0;
i = 0;
for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); it++, i++)
{
NzbInfo* nzbInfo = *it;
if (i >= m_queueScrollOffset && i < m_queueScrollOffset + m_queueWinHeight -1)
{
PrintGroupname(nzbInfo, lineNr++, i == m_selectedQueueEntry, false);
}
remaining += nzbInfo->GetRemainingSize();
paused += nzbInfo->GetPausedSize();
}
BString<1024> buffer(" %sNZBs for downloading - %i NZBs in queue - %s / %s",
m_useColor ? "" : "*** ", (int)downloadQueue->GetQueue()->size(),
*Util::FormatSize(remaining), *Util::FormatSize(remaining - paused));
PrintTopHeader(buffer, m_queueWinTop, false);
}
UnlockQueue();
}
void NCursesFrontend::ResetColWidths()
{
m_colWidthFiles = 0;
m_colWidthTotal = 0;
m_colWidthLeft = 0;
}
void NCursesFrontend::PrintGroupname(NzbInfo* nzbInfo, int row, bool selected, bool calcColWidth)
{
int color = NCURSES_COLORPAIR_TEXT;
char chBrace1 = '[';
char chBrace2 = ']';
if (m_inputMode == editQueue && selected)
{
color = NCURSES_COLORPAIR_TEXTHIGHL;
if (!m_useColor)
{
chBrace1 = '<';
chBrace2 = '>';
}
}
const char* downloading = "";
if (nzbInfo->GetActiveDownloads() > 0)
{
downloading = " *";
}
BString<100> priority;
if (nzbInfo->GetPriority() != 0)
{
priority.Format(" [%+i]", nzbInfo->GetPriority());
}
// Format:
// [id - id] Name Left-Files/Paused Total Left Time
// [1-2] Nzb-name 999/999 999.99 MB 999.99 MB 00:00:00
int nameLen = 0;
if (calcColWidth)
{
nameLen = m_screenWidth - 1 - 9 - 11 - 11 - 9;
}
else
{
nameLen = m_screenWidth - 1 - m_colWidthFiles - 2 - m_colWidthTotal - 2 - m_colWidthLeft - 2 - 9;
}
BString<1024> buffer;
bool printFormatted = nameLen > 20;
if (printFormatted)
{
BString<100> files("%i/%i", (int)nzbInfo->GetFileList()->size(), nzbInfo->GetPausedFileCount());
BString<1024> nameWithIds("%c%i%c%s%s %s", chBrace1, nzbInfo->GetId(), chBrace2,
*priority, downloading, nzbInfo->GetName());
int64 unpausedRemainingSize = nzbInfo->GetRemainingSize() - nzbInfo->GetPausedSize();
CString remaining = Util::FormatSize(unpausedRemainingSize);
CString total = Util::FormatSize(nzbInfo->GetSize());
BString<100> time;
int currentDownloadSpeed = m_standBy ? 0 : m_currentDownloadSpeed;
if (nzbInfo->GetPausedSize() > 0 && unpausedRemainingSize == 0)
{
time = "[paused]";
remaining = Util::FormatSize(nzbInfo->GetRemainingSize());
}
else if (currentDownloadSpeed > 0 && !m_pauseDownload)
{
int64 remain_sec = (int64)(unpausedRemainingSize / currentDownloadSpeed);
int h = (int)(remain_sec / 3600);
int m = (int)((remain_sec % 3600) / 60);
int s = (int)(remain_sec % 60);
if (h < 100)
{
time.Format("%.2d:%.2d:%.2d", h, m, s);
}
else
{
time.Format("99:99:99");
}
}
if (calcColWidth)
{
int colWidthFiles = strlen(files);
m_colWidthFiles = colWidthFiles > m_colWidthFiles ? colWidthFiles : m_colWidthFiles;
int colWidthTotal = strlen(total);
m_colWidthTotal = colWidthTotal > m_colWidthTotal ? colWidthTotal : m_colWidthTotal;
int colWidthLeft = strlen(remaining);
m_colWidthLeft = colWidthLeft > m_colWidthLeft ? colWidthLeft : m_colWidthLeft;
}
else
{
buffer.Format("%-*s %*s %*s %*s %8s", nameLen, *nameWithIds,
m_colWidthFiles, *files, m_colWidthTotal, *total, m_colWidthLeft, *remaining, *time);
}
}
else
{
buffer.Format("%c%i%c%s %s", chBrace1, nzbInfo->GetId(),
chBrace2, downloading, nzbInfo->GetName());
}
if (!calcColWidth)
{
PlotLine(buffer, row, 0, color);
}
}
bool NCursesFrontend::EditQueue(DownloadQueue::EEditAction action, int offset)
{
int ID = 0;
if (m_groupFiles)
{
DownloadQueue* downloadQueue = LockQueue();
if (m_selectedQueueEntry >= 0 && m_selectedQueueEntry < (int)downloadQueue->GetQueue()->size())
{
NzbInfo* nzbInfo = downloadQueue->GetQueue()->at(m_selectedQueueEntry);
ID = nzbInfo->GetId();
if (action == DownloadQueue::eaFilePause)
{
if (nzbInfo->GetRemainingSize() == nzbInfo->GetPausedSize())
{
action = DownloadQueue::eaFileResume;
}
else if (nzbInfo->GetPausedSize() == 0 && (nzbInfo->GetRemainingParCount() > 0) &&
!(m_lastPausePars && m_lastEditEntry == m_selectedQueueEntry))
{
action = DownloadQueue::eaFilePauseExtraPars;
m_lastPausePars = true;
}
else
{
action = DownloadQueue::eaFilePause;
m_lastPausePars = false;
}
}
}
UnlockQueue();
// map file-edit-actions to group-edit-actions
DownloadQueue::EEditAction FileToGroupMap[] = {
(DownloadQueue::EEditAction)0,
DownloadQueue::eaGroupMoveOffset,
DownloadQueue::eaGroupMoveTop,
DownloadQueue::eaGroupMoveBottom,
DownloadQueue::eaGroupPause,
DownloadQueue::eaGroupResume,
DownloadQueue::eaGroupDelete,
DownloadQueue::eaGroupPauseAllPars,
DownloadQueue::eaGroupPauseExtraPars };
action = FileToGroupMap[action];
}
else
{
DownloadQueue* downloadQueue = LockQueue();
int fileNum = 0;
for (NzbList::iterator it = downloadQueue->GetQueue()->begin(); it != downloadQueue->GetQueue()->end(); it++)
{
NzbInfo* nzbInfo = *it;
for (FileList::iterator it2 = nzbInfo->GetFileList()->begin(); it2 != nzbInfo->GetFileList()->end(); it2++, fileNum++)
{
if (m_selectedQueueEntry == fileNum)
{
FileInfo* fileInfo = *it2;
ID = fileInfo->GetId();
if (action == DownloadQueue::eaFilePause)
{
action = !fileInfo->GetPaused() ? DownloadQueue::eaFilePause : DownloadQueue::eaFileResume;
}
}
}
}
UnlockQueue();
}
m_lastEditEntry = m_selectedQueueEntry;
NeedUpdateData();
if (ID != 0)
{
return ServerEditQueue(action, offset, ID);
}
else
{
return false;
}
}
void NCursesFrontend::SetCurrentQueueEntry(int entry)
{
int queueSize = CalcQueueSize();
if (entry < 0)
{
entry = 0;
}
else if (entry > queueSize - 1)
{
entry = queueSize - 1;
}
if (entry > m_queueScrollOffset + m_queueWinClientHeight ||
entry < m_queueScrollOffset - m_queueWinClientHeight)
{
m_queueScrollOffset = entry - m_queueWinClientHeight / 2;
}
else if (entry < m_queueScrollOffset)
{
m_queueScrollOffset -= m_queueWinClientHeight;
}
else if (entry >= m_queueScrollOffset + m_queueWinClientHeight)
{
m_queueScrollOffset += m_queueWinClientHeight;
}
if (m_queueScrollOffset > queueSize - m_queueWinClientHeight)
{
m_queueScrollOffset = queueSize - m_queueWinClientHeight;
}
if (m_queueScrollOffset < 0)
{
m_queueScrollOffset = 0;
}
m_selectedQueueEntry = entry;
}
/*
* Process keystrokes starting with the initialKey, which must not be
* READKEY_EMPTY but has alread been set via ReadConsoleKey.
*/
void NCursesFrontend::UpdateInput(int initialKey)
{
int key = initialKey;
while (key != READKEY_EMPTY)
{
int queueSize = CalcQueueSize();
// Normal or edit queue mode
if (m_inputMode == normal || m_inputMode == editQueue)
{
switch (key)
{
case 'q':
// Key 'q' for quit
ExitProc();
break;
case 'z':
// show/hide NZBFilename
m_showNzbname = !m_showNzbname;
break;
case 'w':
// swicth window sizes
if (m_queueWindowPercentage == 50)
{
m_queueWindowPercentage = 100;
}
else if (m_queueWindowPercentage == 100 && m_inputMode != editQueue)
{
m_queueWindowPercentage = 0;
}
else
{
m_queueWindowPercentage = 50;
}
CalcWindowSizes();
SetCurrentQueueEntry(m_selectedQueueEntry);
break;
case 'g':
// group/ungroup files
m_groupFiles = !m_groupFiles;
SetCurrentQueueEntry(m_selectedQueueEntry);
NeedUpdateData();
break;
}
}
// Normal mode
if (m_inputMode == normal)
{
switch (key)
{
case 'p':
// Key 'p' for pause
if (!IsRemoteMode())
{
info(m_pauseDownload ? "Unpausing download" : "Pausing download");
}
ServerPauseUnpause(!m_pauseDownload);
break;
case 'e':
case 10: // return
case 13: // enter
if (queueSize > 0)
{
m_inputMode = editQueue;
if (m_queueWindowPercentage == 0)
{
m_queueWindowPercentage = 50;
}
return;
}
break;
case 'r':
// Download rate
m_inputMode = downloadRate;
m_inputNumberIndex = 0;
m_inputValue = 0;
return;
case 't':
// show/hide Timestamps
m_showTimestamp = !m_showTimestamp;
break;
}
}
// Edit Queue mode
if (m_inputMode == editQueue)
{
switch (key)
{
case 'e':
case 10: // return
case 13: // enter
m_inputMode = normal;
return;
case KEY_DOWN:
if (m_selectedQueueEntry < queueSize - 1)
{
SetCurrentQueueEntry(m_selectedQueueEntry + 1);
}
break;
case KEY_UP:
if (m_selectedQueueEntry > 0)
{
SetCurrentQueueEntry(m_selectedQueueEntry - 1);
}
break;
case KEY_PPAGE:
if (m_selectedQueueEntry > 0)
{
if (m_selectedQueueEntry == m_queueScrollOffset)
{
m_queueScrollOffset -= m_queueWinClientHeight;
SetCurrentQueueEntry(m_selectedQueueEntry - m_queueWinClientHeight);
}
else
{
SetCurrentQueueEntry(m_queueScrollOffset);
}
}
break;
case KEY_NPAGE:
if (m_selectedQueueEntry < queueSize - 1)
{
if (m_selectedQueueEntry == m_queueScrollOffset + m_queueWinClientHeight - 1)
{
m_queueScrollOffset += m_queueWinClientHeight;
SetCurrentQueueEntry(m_selectedQueueEntry + m_queueWinClientHeight);
}
else
{
SetCurrentQueueEntry(m_queueScrollOffset + m_queueWinClientHeight - 1);
}
}
break;
case KEY_HOME:
SetCurrentQueueEntry(0);
break;
case KEY_END:
SetCurrentQueueEntry(queueSize > 0 ? queueSize - 1 : 0);
break;
case 'p':
// Key 'p' for pause
EditQueue(DownloadQueue::eaFilePause, 0);
break;
case 'd':
SetHint(" Use Uppercase \"D\" for delete");
break;
case 'D':
// Delete entry
if (EditQueue(DownloadQueue::eaFileDelete, 0))
{
SetCurrentQueueEntry(m_selectedQueueEntry);
}
break;
case 'u':
if (EditQueue(DownloadQueue::eaFileMoveOffset, -1))
{
SetCurrentQueueEntry(m_selectedQueueEntry - 1);
}
break;
case 'n':
if (EditQueue(DownloadQueue::eaFileMoveOffset, +1))
{
SetCurrentQueueEntry(m_selectedQueueEntry + 1);
}
break;
case 't':
if (EditQueue(DownloadQueue::eaFileMoveTop, 0))
{
SetCurrentQueueEntry(0);
}
break;
case 'b':
if (EditQueue(DownloadQueue::eaFileMoveBottom, 0))
{
SetCurrentQueueEntry(queueSize > 0 ? queueSize - 1 : 0);
}
break;
}
}
// Edit download rate input mode
if (m_inputMode == downloadRate)
{
// Numbers
if (m_inputNumberIndex < 5 && key >= '0' && key <= '9')
{
m_inputValue = (m_inputValue * 10) + (key - '0');
m_inputNumberIndex++;
}
// Enter
else if (key == 10 || key == 13)
{
ServerSetDownloadRate(m_inputValue * 1024);
m_inputMode = normal;
return;
}
// Escape
else if (key == 27)
{
m_inputMode = normal;
return;
}
// Backspace
else if (m_inputNumberIndex > 0 && key == KEY_BACKSPACE)
{
int remain = m_inputValue % 10;
m_inputValue = (m_inputValue - remain) / 10;
m_inputNumberIndex--;
}
}
key = ReadConsoleKey();
}
}
int NCursesFrontend::ReadConsoleKey()
{
#ifdef WIN32
HANDLE hConsole = GetStdHandle(STD_INPUT_HANDLE);
DWORD NumberOfEvents;
BOOL ok = GetNumberOfConsoleInputEvents(hConsole, &NumberOfEvents);
if (ok && NumberOfEvents > 0)
{
while (NumberOfEvents--)
{
INPUT_RECORD InputRecord;
DWORD NumberOfEventsRead;
if (ReadConsoleInput(hConsole, &InputRecord, 1, &NumberOfEventsRead) &&
NumberOfEventsRead > 0 &&
InputRecord.EventType == KEY_EVENT &&
InputRecord.Event.KeyEvent.bKeyDown)
{
char c = tolower(InputRecord.Event.KeyEvent.wVirtualKeyCode);
if (bool(InputRecord.Event.KeyEvent.dwControlKeyState & CAPSLOCK_ON) ^
bool(InputRecord.Event.KeyEvent.dwControlKeyState & SHIFT_PRESSED))
{
c = toupper(c);
}
return c;
}
}
}
return READKEY_EMPTY;
#else
return getch();
#endif
}
#endif