@ 260915.5
current
/******************************************************************************\
* Copyright (c) 2019, Robert van Engelen, Genivia Inc. 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 author 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.                                   *
\******************************************************************************/

/**
@file      query.cpp
@brief     Query engine TUI
@author    Robert van Engelen - engelen@genivia.com
@copyright (c) 2019-2025, Robert van Engelen, Genivia Inc. All rights reserved.
@copyright (c) BSD-3 License - see LICENSE.txt
*/

#include "ugrep.hpp"
#include "stats.hpp"
#include "query.hpp"

#include <reflex/error.h>
#include <iostream>
#include <fstream>
#include <thread>
#include <chrono>
#include <fcntl.h>

#ifdef OS_WIN_OR_MINGW

#include <direct.h>

// create a non-blocking pipe (Windows named pipe)
inline HANDLE nonblocking_pipe(int fd[2])
{
  DWORD pid = GetCurrentProcessId();
  std::string pipe_name = "\\\\.\\pipe\\ugrep_";
  pipe_name.append(std::to_string(pid)).append("_").append(std::to_string(time(NULL)));
  DWORD buffer_size = QUERY_BUFFER_SIZE;
  HANDLE pipe_r = CreateNamedPipeA(pipe_name.c_str(), PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, buffer_size, buffer_size, 0, NULL);
  if (pipe_r != INVALID_HANDLE_VALUE)
  {
    HANDLE pipe_w = CreateFileA(pipe_name.c_str(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
    if (pipe_w != INVALID_HANDLE_VALUE)
    {
      fd[0] = _open_osfhandle(reinterpret_cast<intptr_t>(pipe_r), _O_RDONLY);
      fd[1] = _open_osfhandle(reinterpret_cast<intptr_t>(pipe_w), _O_WRONLY);
    }
    else
    {
      CloseHandle(pipe_r);
      pipe_r = INVALID_HANDLE_VALUE;
    }
  }
  return pipe_r;
}

#else

#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>

// create a pipe with non-blocking read end
inline int nonblocking_pipe(int fd[2])
{
  if (pipe(fd) == 0)
  {
    if (fcntl(fd[0], F_SETFL, fcntl(fd[0], F_GETFL) | O_NONBLOCK) >= 0)
      return 0;
    close(fd[0]);
    close(fd[1]);
  }
  return -1;
}

// make a pipe block
inline void set_blocking(int fd0)
{
  fcntl(fd0, F_SETFL, fcntl(fd0, F_GETFL) & ~O_NONBLOCK);
}

#endif

static const char *LARROW = "«"; // left arrow
static const char *RARROW = "»"; // right arrow

// return pointer to character in the query search line at screen col, taking UTF-8 double width characters into account
char *Query::line_ptr(int col)
{
  char *ptr = line_;
  while (*ptr != '\0')
  {
    col -= Screen::mbchar_width(ptr, NULL);
    if (col < 0)
      break;
    ++ptr;
  }
  return ptr;
}

// return pointer to the character in the query search line at pos distance after the screen col
char *Query::line_ptr(int col, int pos)
{
  return Screen::mbstring_pos(line_ptr(col), pos);
}

// return pointer to the end of the query search line
char *Query::line_end()
{
  char *ptr = line_;
  while (*ptr != '\0')
    ++ptr;
  return ptr;
}

// return number of character positions on the query search line up to the current screen Query::col_, taking UTF-8 wide characters into account
int Query::line_pos()
{
  char *ptr = line_;
  char *end = line_ptr(col_);
  int pos = 0;
  while (ptr < end && *ptr != '\0')
  {
    Screen::wchar(ptr, const_cast<const char**>(&ptr));
    ++pos;
  }
  return pos;
}

// return the length of the query search line as a number of screen columns displayed
int Query::line_len()
{
  return Screen::mbstring_width(line_);
}

// return the length of the query search line as the number of UTF-8 wide characters
int Query::line_wsize()
{
  int num = 0;
  char *ptr = line_;
  while (*ptr != '\0')
  {
    Screen::wchar(ptr, const_cast<const char**>(&ptr));
    ++num;
  }
  return num;
}

// draw the textual part of the query search line, this function is called by draw()
void Query::display(int col, int len)
{
  const char *ptr = line_ptr(col);
  const char *end = line_ptr(col + len);
  const char *err = error_ >= 0 && !Screen::mono ? line_ + error_ : NULL;
  const char *next;
  bool alert = false;
  bool list = false;
  bool braced = false;
  bool literal = false;
  int prev = -1;
  if (!Screen::mono)
  {
    if (globbing_)
    {
      for (const char *look = line_; look < ptr; ++look)
      {
        if (*look == (list ? ']' : '['))
          list = !list, look += (*look == '[') + (look[1] == '!' || look[1] == '^' || look[1] == '\\');
        else if (*look == '\\')
          ++look;
      }
    }
    else if (flag_mode != ::Mode::FIXED)
    {
      for (const char *look = line_; look < ptr; ++look)
      {
        if (!braced && !literal && *look == (list ? ']' : '['))
          list = !list, look += (*look == '[') + (look[1] == '^' || look[1] == '\\');
        else if (!list && !literal && *look == (braced ? '}' : '{'))
          braced = !braced;
        else if (!list && !braced && *look == '"' && flag_bool)
          literal = !literal;
        else if (*look == '\\' && look[1] == (literal ? 'E' : 'Q'))
          literal = !literal;
        else if (*look == '\\')
          ++look;
      }
    }
    if (literal || list)
      Screen::put(color_ql);
    else if (braced)
      Screen::put(color_qb);
    else
      Screen::put(color_qr);
  }
  for (next = ptr; next < end; ++next)
  {
    if (next == err)
    {
      Screen::put(ptr, next - ptr);
      Screen::normal();
      if (!Screen::mono)
        Screen::put(color_qe);
      ptr = next;
      alert = true;
    }
    else
    {
      int ch = static_cast<unsigned char>(*next);
      if (err != NULL && alert && next > err && (ch & 0xc0) != 0x80)
      {
        Screen::put(ptr, next - ptr);
        Screen::normal();
        if (list)
          Screen::put(color_ql);
        else if (braced)
          Screen::put(color_qb);
        else
          Screen::put(color_qr);
        ptr = next;
        alert = false;
      }
      if (ch <= 0x1f || ch == 0x7f)
      {
        Screen::put(ptr, next - ptr);
        if (err != NULL && alert && next > err)
        {
          Screen::normal();
          if (!Screen::mono)
            Screen::put(color_qr);
          alert = false;
        }
        if (!alert)
          Screen::invert();
        if (ch == 0x7f)
        {
          Screen::put("^?");
        }
        else
        {
          char buf[2] = { '^', static_cast<char>('@' + ch) };
          Screen::put(buf, 2);
        }
        Screen::normal();
        ptr = next + 1;
        alert = false;
      }
      else if (!Screen::mono)
      {
        if (globbing_)
        {
          if (ch == '[' && !list)
          {
            list = true;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_qm);
            Screen::put(ch);
            Screen::normal();
            Screen::put(color_ql);
            ptr = ++next;
            next += (*next == '!' || *next == '^');
            next += (*next == '\\');
          }
          else if (ch == ']' && list)
          {
            list = false;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_qm);
            Screen::put(ch);
            Screen::normal();
            Screen::put(color_qr);
            ptr = next + 1;
          }
          else if (strchr("!*,?^", ch) != NULL && !list)
          {
            if ((ch != '!' && ch != '^') || prev == ',' || prev == -1)
            {
              Screen::put(ptr, next - ptr);
              Screen::normal();
              Screen::put(color_qm);
              Screen::put(ch);
              Screen::normal();
              Screen::put(color_qr);
              ptr = next + 1;
            }
          }
          else if (ch == '\\' && !list)
          {
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_ql);
            Screen::put(ch);
            if (next + 1 < end)
              Screen::put(next[1]);
            Screen::normal();
            Screen::put(color_qr);
            ptr = ++next + 1;
          }
        }
        else if (flag_mode != ::Mode::FIXED)
        {
          if (ch == '[' && !list && !literal && !braced)
          {
            list = true;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_qm);
            Screen::put(ch);
            Screen::normal();
            Screen::put(color_ql);
            ptr = ++next;
            next += (*next == '^');
            next += (*next == '\\');
          }
          else if (ch == ']' && list && !literal && !braced)
          {
            list = false;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_qm);
            Screen::put(ch);
            Screen::normal();
            Screen::put(color_qr);
            ptr = next + 1;
          }
          else if (ch == '{' && !list && !literal && !braced)
          {
            braced = true;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_qb);
            ptr = next;
          }
          else if (ch == '}' && !list && !literal && braced)
          {
            braced = false;
            Screen::put(ptr, next - ptr + 1);
            Screen::normal();
            Screen::put(color_qr);
            ptr = next + 1;
          }
          else if (ch == '\\' && next + 1 != err && next[1] >= ' ' && next[1] <= '~')
          {
            if (next[1] == 'E' && !list)
              literal = false;
            if (!literal)
            {
              if (next[1] == 'Q' && !list)
                literal = true;
              Screen::put(ptr, next - ptr);
              Screen::normal();
              Screen::put(color_qm);
              Screen::put(ch);
              if (next + 1 < end)
                Screen::put(next[1]);
              Screen::normal();
              if (literal || list)
                Screen::put(color_ql);
              else if (braced)
                Screen::put(color_qb);
              else
                Screen::put(color_qr);
              ptr = ++next + 1;
            }
          }
          else if (strchr("$()*+.?^|", ch) != NULL && !list && !literal && !braced)
          {
            if (flag_mode != ::Mode::BRE || strchr("()+?|", ch) == NULL)
            {
              Screen::put(ptr, next - ptr);
              Screen::normal();
              Screen::put(color_qm);
              Screen::put(ch);
              Screen::normal();
              Screen::put(color_qr);
              ptr = next + 1;
            }
          }
        }
        if (flag_bool && !list && !braced)
        {
          if (ch == '"')
          {
            literal = !literal;
            Screen::put(ptr, next - ptr);
            Screen::normal();
            Screen::put(color_ql);
            Screen::put(ch);
            Screen::normal();
            if (literal)
              Screen::put(color_ql);
            else
              Screen::put(color_qr);
            ptr = next + 1;
          }
          else if (!literal)
          {
            if (prev == ' ' || prev == '|' || prev == '(' || prev == -1)
            {
              if (ch == '-' ||
                  (flag_mode == ::Mode::FIXED && (ch == '(' || ch == ')' || ch == '|')) ||
                  strncmp(next, "AND ", 4) == 0 ||
                  strncmp(next, "OR ", 3) == 0 ||
                  strncmp(next, "NOT ", 4) == 0)
              {
                Screen::put(ptr, next - ptr);
                Screen::normal();
                Screen::put(color_qm);
                ptr = next;
                if (isalpha(ch))
                  next += 1 + (next[2] != ' ');
                Screen::put(ptr, next - ptr + 1);
                Screen::normal();
                Screen::put(color_qr);
                ptr = next + 1;
                ch = ' ';
              }
            }
            else if (flag_mode == ::Mode::FIXED &&
                (ch == '|' ||
                 (ch == ')' && (next + 1 >= end || next[1] == ' ' || next[1] == '|' || next[1] == ')'))))
            {
              Screen::put(ptr, next - ptr);
              Screen::normal();
              Screen::put(color_qm);
              Screen::put(ch);
              Screen::normal();
              Screen::put(color_qr);
              ptr = next + 1;
            }
          }
        }
      }
      prev = ch;
    }
  }
  if (ptr < end)
    Screen::put(ptr, end - ptr);
  if (next == err && !Screen::mono)
    Screen::put(color_qe);
}

// draw the query search line
void Query::draw()
{
  if (mode_ == Mode::QUERY)
  {
    start_ = 0;

    Screen::home();

    if (row_ > 0)
    {
      char down[16];

      snprintf(down, sizeof(down), "%3d ", row_);

      Screen::normal();
      Screen::put(down);

      start_ = static_cast<int>(strlen(down));
    }

    if (select_ == -1)
    {
      if (!dirs_.empty())
      {
        int width = Screen::mbstring_width(dirs_.c_str());
        int offset = 0;
        int middle = Screen::cols/2 - start_;
        if (width + 2 > middle)
          offset = width + 2 - middle;

        Screen::normal();
        if (offset > 0)
          Screen::put(LARROW);

        const char *dir = Screen::mbstring_pos(dirs_.c_str(), offset);
        Screen::put(dir);
        Screen::put(' ');

        start_ += Screen::mbstring_width(dir) + 1 + (offset > 0);
      }

      if (!Screen::mono)
      {
        Screen::normal();
        Screen::put(error_ == -1 ? color_qp : color_qe);
      }

      Screen::put(prompt_.c_str());
      Screen::normal();

      start_ += static_cast<int>(prompt_.size());

      int pos;
      if (len_ - col_ < shift_)
        pos = Screen::cols - start_ - (len_ - col_) - 1;
      else
        pos = Screen::cols - start_ - shift_ - 1;

      offset_ = col_ > pos ? col_ - pos : 0;

      if (offset_ > 0)
      {
        if (!Screen::mono)
          Screen::put(error_ == -1 ? color_qp : color_qe);

        Screen::put(LARROW);
        Screen::normal();

        int adj = 1;
        if (line_ptr(offset_) == line_ptr(offset_ + 1)) // adjust columns when double width char at offset
        {
          Screen::put(' ');
          adj = 2; // make the displayed line start one character later
        }

        if (len_ >= offset_ + Screen::cols - start_)
        {
          display(offset_ + adj, Screen::cols - start_ - adj - 1);
          Screen::erase();
          if (!Screen::mono)
            Screen::put(error_ == -1 ? color_qp : color_qe);
          Screen::put(RARROW);
        }
        else
        {
          display(offset_ + adj, len_ - offset_ - adj);
          Screen::erase();
        }
      }
      else
      {
        if (len_ > Screen::cols - start_)
        {
          display(0, Screen::cols - start_ - 1);
          Screen::erase();
          if (!Screen::mono)
            Screen::put(error_ == -1 ? color_qp : color_qe);
          Screen::put(RARROW);
        }
        else
        {
          display(0, len_);
          if (len_ < Screen::cols - start_)
            Screen::erase();
        }
      }
    }
    else
    {
      Screen::normal();
      Screen::put(0, start_, "\033[7mEnter\033[m/\033[7mDel\033[m (de)select line  \033[7mA\033[mll  \033[7mC\033[mlear  \033[7mEsc\033[m go back  \033[7m^Q\033[m quit & output");
    }
  }
}

// view one row of text by drawing it on screen
void Query::disp(int row)
{
  Screen::normal();
  if (row < rows_ + append_)
  {
    if (selected_[row])
      Screen::select();
    int nulls = !flag_text ? 2 : 0; // how many nulls to ignore in marked pathnames?
    Screen::put(row - row_ + 1, 0, view_[row], skip_, -1, nulls);
    if (selected_[row])
      Screen::deselect();
  }
  else if (row - row_ + 1 < maxrows_)
  {
    Screen::setpos(row - row_ + 1, 0);
    Screen::erase();
  }
}

// redraw the screen
void Query::redraw()
{
  Screen::getsize();
  maxrows_ = Screen::rows;
  if (flag_split)
    maxrows_ /= 2;
  shift_ = (Screen::cols - start_) / 10;
  Screen::normal();

  if (mode_ == Mode::HELP)
  {
    message_ = false;

#ifdef __APPLE__
    Screen::put(0, 0, "\033[7m     HELP AND OPTIONS     \033[m  \033[7m^\033[m=ctrl  \033[7mS-\033[m=shift  \033[7mM-\033[m=option or \033[7m^O\033[m+key");
#else
    Screen::put(0, 0, "\033[7m     HELP AND OPTIONS     \033[m  \033[7m^\033[m=ctrl  \033[7mS-\033[m=shift  \033[7mM-\033[m=alt or \033[7m^O\033[m+key");
#endif

    Screen::put( 1, 0, "");
    Screen::put( 2, 0, "\033[7mEsc\033[m   go back / exit");
    Screen::put( 3, 0, "\033[7mTab\033[m   cd dir / select file");
    Screen::put( 4, 0, "\033[7mS-Tab\033[m cd .. / deselect file");
    Screen::put( 5, 0, "\033[7mEnter\033[m output selection mode");
    Screen::put( 6, 0, "");
    Screen::put( 7, 0, "\033[7mUp\033[m     \033[7mDown\033[m    scroll");
    Screen::put( 8, 0, "\033[7mPgUp\033[m   \033[7mPgDn\033[m    scroll page");
#ifdef __APPLE__
    Screen::put( 9, 0, "\033[7mS-Left\033[m \033[7mS-Right\033[m pan ½ page");
    Screen::put(10, 0, "\033[7mS-Up\033[m   \033[7mS-Down\033[m  scroll ½ pg");
#else
    Screen::put( 9, 0, "\033[7mM-Left\033[m \033[7mM-Right\033[m pan ½ page");
    Screen::put(10, 0, "\033[7mM-Up\033[m   \033[7mM-Down\033[m  scroll ½ pg");
#endif
    Screen::put(11, 0, "\033[7mHome\033[m   \033[7mEnd\033[m     of pattern");
    Screen::put(12, 0, "");
    Screen::put(13, 0, "\033[7m^K\033[m delete after cursor");
    Screen::put(14, 0, "\033[7m^L\033[m refresh screen");
    Screen::put(15, 0, "\033[7m^Q\033[m quit and output");
    Screen::put(16, 0, "\033[7m^R\033[m/\033[7mF4\033[m restore bookmark");
    Screen::put(17, 0, "\033[7m^S\033[m next dir/file/context");
    Screen::put(18, 0, "\033[7m^T\033[m/\033[7mF5\033[m toggle split screen");
    Screen::put(19, 0, "\033[7m^U\033[m delete before cursor");
    Screen::put(20, 0, "\033[7m^V\033[m verbatim character");
    Screen::put(21, 0, "\033[7m^W\033[m prev dir/file/context");
    Screen::put(22, 0, "\033[7m^X\033[m/\033[7mF3\033[m set bookmark");
    Screen::put(23, 0, "\033[7m^Y\033[m/\033[7mF2\033[m view or edit file");
    Screen::put(24, 0, "\033[7m^Z\033[m/\033[7mF1\033[m help");
    Screen::put(25, 0, "\033[7m^^\033[m chdir to starting dir");
    Screen::put(26, 0, "\033[7m^]\033[m toggle color/mono");
    Screen::put(27, 0, "\033[7m^\\\033[m force quit");
    Screen::put(28, 0, "");
    Screen::put(29, 0, "\033[7mM-/xxxx/\033[m U+xxxx code point");
    Screen::put(30, 0, "");

    std::string buf;
    int row = 31;
    int col = 0;

    for (Flags *fp = flags_; fp->text != NULL; ++fp)
    {
      buf.assign("\033[7mM- \033[m ");
      buf[6] = fp->key;
      if (strncmp(fp->text, "decrease", 8) == 0 || strncmp(fp->text, "increase", 8) == 0)
        buf.append("    ").append(fp->text);
      else if (fp->flag)
        buf.append("[\033[32;1m*\033[m] \033[1m").append(fp->text).append("\033[m");
      else
        buf.append("[ ] ").append(fp->text);
      if (row >= Screen::rows)
      {
        row = 2;
        col += 28;
      }
      Screen::put(row, col, buf);
      ++row;
    }

    if (col == 0)
      Screen::end();

    Screen::put(0, Screen::cols - 1, "?");
  }
  else
  {
    // reset to 4 to do not show dots
    tick_ = 4;

    if (error_ == -1)
    {
      if (select_ >= 0 && select_ >= row_ + maxrows_ - 1)
        row_ = select_ - maxrows_ + 3;
      else if (select_ >= 0 && select_ < row_)
        row_ = select_ - 1;

      if (row_ >= rows_)
        row_ = rows_ - 1;
      if (row_ < 0)
        row_ = 0;

      int end = row_ + append_ + maxrows_ - 2;

      for (int i = row_; i < end; ++i)
        disp(i);

      status(true);

      preview();
    }

    if (!message_)
      draw();
  }
}

#ifdef OS_WIN_OR_MINGW

// CTRL-C/BREAK handler
BOOL WINAPI Query::sigint(DWORD)
{
  VKey::cleanup();
  Screen::cleanup();

  // return FALSE to invoke the next handler
  return FALSE;
}

#else

void Query::sigwinch(int)
{
  redraw();
}

// SIGINT and SIGTERM handler
void Query::sigint(int sig)
{
  VKey::cleanup();
  Screen::cleanup();

  // force close, to deliver pending writes
  close(Screen::tty);

  // reset to the default handler
  signal(sig, SIG_DFL);

  // signal again
  kill(getpid(), sig);
}

#endif

// move the cursor to a column in the query search line
void Query::move(int col)
{
  int dir = 0;
  if (col > col_)
    dir = 1;
  else if (col < col_)
    dir = -1;
  if (col <= 0)
    col = 0;
  else if (col >= len_)
    col = len_;
  else if (dir != 0 && line_ptr(col - 1) == line_ptr(col)) // oops, we're at the second half of a full width char
    col += dir; // direction is -1 or 1 to jump at or after full width char
  col_ = col;
  if (len_ >= Screen::cols - start_ && col >= Screen::cols - start_ - shift_)
    draw();
  else if (offset_ > 0)
    draw();
  else
    Screen::setpos(0, start_ + col_ - offset_);
}

// insert text to line at cursor
void Query::insert(const char *text, size_t size)
{
  char *end = line_end();
  if (end + size >= line_ + sizeof(Line))
  {
    size = line_ + sizeof(Line) - end - 1;
    Screen::alert();
  }
  if (size > 0)
  {
    char *ptr = line_ptr(col_);
    memmove(ptr + size, ptr, end - ptr + 1);
    memcpy(ptr, text, size);
    int oldlen = len_;
    len_ = line_len();
    int forward = len_ - oldlen;
    if (forward > 0)
    {
      updated_ = true;
      error_ = -1;
      col_ += forward;
      draw();
    }
  }
}

// insert one character (or a byte of a multi-byte sequence) in the line at the cursor
void Query::insert(int ch)
{
  char buf = static_cast<char>(ch);
  insert(&buf, 1);
}

// erase num bytes from the line at and after the cursor
void Query::erase(int num)
{
  char *ptr = line_ptr(col_);
  char *next = line_ptr(col_, num);
  char *skip = next;
  while (*skip != '\0' && Screen::mbchar_width(next, const_cast<const char**>(&skip)) == 0)
    next = skip;
  if (next > ptr)
  {
    const char *end = line_end();
    memmove(ptr, next, end - next + 1);
    updated_ = true;
    error_ = -1;
    len_ = line_len();
    draw();
  }
}

// called by the main program
void Query::query()
{
  // do not exit on usage warnings
  flag_usage_warnings = true;

  get_stdin();

  if (!VKey::setup(VKey::TTYRAW))
    abort("no ANSI terminal keyboard detected");

  if (!Screen::setup("ugrep --query"))
  {
    VKey::cleanup();
    abort("no ANSI terminal screen detected");
  }

#ifdef OS_WIN_OR_MINGW

  // handle CTRL-C
  SetConsoleCtrlHandler(&sigint, TRUE);

#else

  signal(SIGINT, sigint);
  signal(SIGQUIT, sigint);
  signal(SIGTERM, sigint);
  signal(SIGPIPE, SIG_IGN);
  signal(SIGWINCH, sigwinch);

#endif

  VKey::map_alt_key('E', NULL);
  VKey::map_alt_key('Q', NULL);

  for (Flags *fp = flags_; fp->text != NULL; ++fp)
    VKey::map_alt_key(fp->key, NULL);

  get_flags();

  // if --view is empty (default), then try the PAGER, VISUAL or EDITOR environment variable as viewer or use the default viewer
  if (flag_view != NULL && *flag_view == '\0')
  {
    flag_view = getenv("PAGER");

    if (flag_view == NULL)
      flag_view = getenv("VISUAL");

    if (flag_view == NULL)
      flag_view = getenv("EDITOR");

    if (flag_view == NULL)
      flag_view = DEFAULT_VIEW_COMMAND;
  }

  query_ui();

  VKey::cleanup();
  Screen::cleanup();

  // print the selected output
  print();

  // close the search pipe to terminate the search threads, if still open
  if (!eof_)
  {
    close(search_pipe_[0]);
    eof_ = true;

    // graciously shut down ugrep() if still running
    Static::cancel_ugrep();
  }

  // close the stdin pipe
  if (flag_stdin && Static::source != stdin && Static::source != NULL)
    fclose(Static::source); // close source but don't reset to NULL, source is used by the search thread

  // join the search thread
  if (search_thread_.joinable())
    search_thread_.join();

  // join the stdin sender thread
  stdin_stop = true;
  if (stdin_thread_.joinable())
    stdin_thread_.join();
}

// run the query UI
void Query::query_ui()
{
  mode_ = Mode::QUERY;
  updated_ = false;
  message_ = false;
  *line_ = '\0';
  col_ = 0;
  len_ = 0;
  offset_ = 0;
  shift_ = 8;
  error_ = -1;
  tick_ = 4;
  row_ = 0;
  rows_ = 0;
  maxrows_ = Screen::rows;
  skip_ = 0;
  select_ = -1;
  select_all_ = false;
  globbing_ = false;
  eof_ = true;
  buflen_ = 0;

  Screen::clear();

  // if -e PATTERN specified, collect patterns on the line to edit
  if (!flag_regexp.empty())
  {
    size_t num = flag_regexp.size();
    if (num >= sizeof(Line))
      num = sizeof(Line) - 1;

    flag_regexp.copy(line_, num);
    line_[num] = '\0';

    len_ = line_len();

    move(len_);

    flag_regexp.clear();
  }

  set_prompt();
  search();
  redraw();

  bool ctrl_o = false;
  bool ctrl_v = false;
  bool err = false;

  while (true)
  {
    size_t delay = message_ ? QUERY_MESSAGE_DELAY : flag_delay;

    int key = 0;

    while (true)
    {
      if (mode_ == Mode::QUERY)
      {
        update();

        if (!message_)
        {
          if (select_ == -1)
            Screen::setpos(0, start_ + col_ - offset_);
          else
            Screen::setpos(select_ - row_ + 1, 0);
        }
      }
      else
      {
        Screen::setpos(select_ - row_ + 1, col_ - offset_);
      }

      if (error_ >= 0 && !err)
      {
        // show error position in the query line, but only once
        draw();

        err = true;
      }

      if (delay-- == 0)
      {
        if (mode_ == Mode::QUERY && updated_)
        {
          search();

          err = false;
        }
#ifdef OS_WIN_OR_MINGW
        else
        {
          // detect screen size changes here for Windows (no SIGWINCH)
          int rows = Screen::rows;
          int cols = Screen::cols;

          Screen::getsize();

          if (rows != Screen::rows || cols != Screen::cols)
            redraw();
        }
#endif

        if (message_)
        {
          message_ = false;
          draw();
        }

        delay = flag_delay;
      }

      key = VKey::in(100);

      if (key > 0)
        break;
    }

    if (message_)
    {
      message_ = false;
      draw();
    }

    if (ctrl_o)
    {
      // CTRL-O + key = Alt+key
      meta(key);

      ctrl_o = false;
    }
    else if (ctrl_v)
    {
      // CTRL-V: insert verbatim character
      if (key < 0x80)
        insert(key);

      ctrl_v = false;
    }
    else
    {
      switch (key)
      {
        case VKey::ESC:
          if (mode_ == Mode::QUERY)
          {
            if (globbing_)
            {
              globbing_ = false;

              memcpy(line_, temp_, sizeof(Line));
              len_ = line_len();
              move(len_);

              set_prompt();
              draw();
            }
            else if (select_ == -1)
            {
              if (confirm("Exit"))
                return;
            }
            else
            {
              select_ = -1;
              redraw();
            }
          }
          break;

        case VKey::LF:
        case VKey::CR:
          if (mode_ == Mode::QUERY)
          {
            if (select_ == -1)
            {
              if (rows_ > 0)
              {
                select_ = row_;
                select_all_ = false;
                if (flag_split)
                {
                  flag_split = false;
                  redraw();
                }
                else
                {
                  draw();
                }
              }
              else
              {
                Screen::alert();
              }
            }
            else
            {
              selected_[select_] = !selected_[select_];
              disp(select_);
              down();
            }
          }
          break;

        case VKey::META:
          key = VKey::get();
          switch (key)
          {
            case VKey::TAB: // Shift-TAB: chdir .. or deselect file (or pan screen in selection mode)
              if (mode_ == Mode::QUERY && error_ == -1)
              {
                if (select_ == -1)
                {
                  deselect();
                }
                else
                {
                  skip_ -= 8;
                  if (skip_ < 0)
                    skip_ = 0;
                  redraw();
                }
              }
              else if (error_ != -1 && !dirs_.empty())
              {
                deselect();
              }
              else
              {
                Screen::alert();
              }
              break;

            case VKey::UP: // SHIFT-UP/CTRL-UP: scroll half a page up
              pgup(true);
              break;

            case VKey::DOWN: // SHIFT-DOWN/CTRL-DOWN: scroll half a page down
              pgdn(true);
              break;

            case VKey::LEFT: // SHIFT-LEFT/CTRL-LEFT: pan half a page left
              if (mode_ == Mode::QUERY)
              {
                skip_ -= Screen::cols / 2;
                if (skip_ < 0)
                  skip_ = 0;
                redraw();
              }
              else
              {
                Screen::alert();
              }
              break;

            case VKey::RIGHT: // SHIFT-RIGHT/CTRL-RIGHT: pan half a page right
              if (mode_ == Mode::QUERY)
              {
                skip_ += Screen::cols / 2;
                redraw();
              }
              else
              {
                Screen::alert();
              }
              break;

            default:
              if (select_ == -1)
                meta(key);
              else
                Screen::alert();
          }
          break;

        case VKey::TAB: // TAB: chdir or select file (or pan screen in selection mode)
          if (mode_ == Mode::QUERY && error_ == -1)
          {
            if (select_ == -1)
            {
              select();
            }
            else
            {
              skip_ += 8;
              redraw();
            }
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::BS: // Backspace: delete character before the cursor
          if (mode_ == Mode::QUERY)
          {
            if (select_ == -1)
            {
              if (col_ <= 0)
                break;
              move(col_ - 1);
              erase(1);
            }
            else
            {
              up();
              selected_[select_] = !selected_[select_];
              disp(select_);
            }
          }
          break;

        case VKey::DEL: // DEL: delete character under the cursor
          if (select_ == -1)
          {
            erase(1);
          }
          else
          {
            up();
            selected_[select_] = !selected_[select_];
            disp(select_);
          }
          break;

        case VKey::RIGHT: // RIGHT/CTRL-F: move right
          if (select_ == -1)
          {
            move(col_ + 1);
          }
          else if (mode_ == Mode::QUERY)
          {
            skip_ += 8;
            redraw();
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::LEFT: // LEFT/CTRL-B: move left
          if (select_ == -1)
          {
            move(col_ - 1);
          }
          else if (mode_ == Mode::QUERY)
          {
            skip_ -= 8;
            if (skip_ < 0)
              skip_ = 0;
            redraw();
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::UP: // UP/CTRL-P: move up
          up();
          break;

        case VKey::DOWN: // DOWN/CTRL-N: move down
          down();
          break;

        case VKey::PGUP: // PgUp/CTRL-G: page up
          pgup();
          break;

        case VKey::PGDN: // PgDn/CTRL-D: page down
          pgdn();
          break;

        case VKey::HOME: // HOME/CTRL-A: begin of line
          if (select_ == -1)
            move(0);
          else
            Screen::alert();
          break;

        case VKey::END: // END/CTRL-E: end of line
          if (select_ == -1)
            move(len_);
          else
            Screen::alert();
          break;

        case VKey::CTRL_C: // CTRL-C: quit and output lines
          if (confirm("Exit"))
            return;
          break;

        case VKey::CTRL_K: // CTRL-K: delete after cursor
          if (select_ == -1)
            erase(len_ - col_);
          else
            Screen::alert();
          break;

        case VKey::CTRL_L: // CTRL-L: refresh screen
          redraw();
          break;

        case VKey::CTRL_O: // CTRL-O: press KEY to get Alt-KEY
          if (select_ == -1)
            ctrl_o = true;
          else
            Screen::alert();
          break;

        case VKey::CTRL_R: // CTRL-R or F4: restore bookmarked state
        case VKey::FN(4):
          if (mark_.row >= 0)
          {
            int old_row;
            if (mark_.restore(old_row))
            {
              globbing_ = false;
              set_prompt();
              len_ = line_len();
              search();
            }
            jump(old_row);
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::CTRL_Q: // CTRL-Q: immediately quit and output lines
          return;

        case VKey::CTRL_S: // CTRL-S: scroll to next file or directory (with -l or -c) or context with -ABC
          next();
          break;

        case VKey::CTRL_T: // CTRL-T or F5: toggle split screen
        case VKey::FN(5):
          if (select_ == -1)
            flag_split = !flag_split; // (un)split screen
          redraw();
          break;

        case VKey::CTRL_U: // CTRL-U: delete before cursor
          if (select_ == -1)
          {
            int pos = line_pos();
            col_ = 0;
            erase(pos);
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::CTRL_V: // CTRL-V: verbatim insert character
          if (select_ == -1)
            ctrl_v = true;
          else
            Screen::alert();
          break;

        case VKey::CTRL_W: // CTRL-W: scroll back one file or directory (with -l or -c) or context with -ABC
          back();
          break;

        case VKey::CTRL_X: // CTRL-X or F3: set bookmark and save state
        case VKey::FN(3):
          if (select_ == -1)
          {
            mark_.save();
            status(true);
          }
          else
          {
            Screen::alert();
          }
          break;

        case VKey::CTRL_Y: // CTRL-Y or F2: view (or edit) file
        case VKey::FN(2):
          if (select_ == -1)
            view();
          else
            Screen::alert();
          break;

        case VKey::CTRL_Z: // CTRL-Z or F1: help
        case VKey::FN(1):
          if (help())
            return;
          break;

        case VKey::CTRL_BS: // CTRL-\: terminate
#ifdef OS_WIN_OR_MINGW
          GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
#else
          raise(SIGTERM);
#endif
          break;

        case VKey::CTRL_RB: // CTRL-]: toggle colors/mono
          Screen::mono = !Screen::mono;
          redraw();
          break;

        case VKey::CTRL_CA: // CTRL-^: chdir back to working directory
          if (mode_ == Mode::QUERY && (error_ == -1 || !dirs_.empty()) && select_ == -1)
            unselect();
          break;

        default:
          if (key >= 32 && key < 256)
          {
            if (select_ == -1)
            {
              insert(key);
            }
            else if (key == 'A' || key == 'a')
            {
              for (int i = 0; i < rows_; ++i)
                selected_[i] = true;
              select_all_ = true;

              redraw();
            }
            else if (key == 'C' || key == 'c')
            {
              for (int i = 0; i < rows_; ++i)
                selected_[i] = false;
              select_all_ = false;

              redraw();
            }
            else
            {
              Screen::alert();
            }
          }
          else
          {
            if (help())
              return;
          }
      }
    }
  }
}

// start a new search, cancel the previous search when still running
void Query::search()
{
  stop_stdin();

  bool cancel = !eof_;

  if (cancel)
  {
    close(search_pipe_[0]);
    eof_ = true;
    buflen_ = 0;

    // graciously shut down ugrep() if still running
    Static::cancel_ugrep();
  }

  if (search_thread_.joinable())
  {
    if (cancel && error_ == -1)
    {
      const int banlen = 256;
      char banner[banlen];
      snprintf(banner, banlen, "restarting: please be patient while I cancel searching large files...%*s", banlen - 70, "");
      Screen::normal();
      Screen::invert();
      Screen::put(maxrows_ - 1, 0, banner);
      Screen::normal();
    }

    search_thread_.join();
  }

#ifdef OS_WIN_OR_MINGW

  hPipe_ = nonblocking_pipe(search_pipe_);

  if (hPipe_ == INVALID_HANDLE_VALUE)
  {
    what_.assign("cannot create pipe");
    // error at the end of the line, not within
    error_ = line_wsize();
    return;
  }

  memset(&overlapped_, 0, sizeof(overlapped_));
  blocking_ = false;
  pending_ = false;

#else

  if (nonblocking_pipe(search_pipe_) < 0)
  {
    what_.assign("cannot create pipe");
    // error at the end of the line, not within
    error_ = line_wsize();
    return;
  }

#endif

  eof_ = false;
  append_ = false;
  row_ = 0;
  rows_ = 0;
  skip_ = 0;
  searched_ = 0;
  found_ = 0;
  error_ = -1;

  // reset ugrep stats
  Stats::reset();

  Static::arg_pattern = globbing_ ? temp_ : line_;

  if (*Static::arg_pattern == '\0' && !flag_file.empty())
    Static::arg_pattern = NULL;

  if (deselect_file_)
  {
    // S-Tab back out of a selected file
    selected_file_.clear();
    deselect_file_ = false;
  }
  else if (!selected_file_.empty() && Static::arg_files.empty())
  {
    // Tab into the selected file to search
    Static::arg_files.emplace_back(selected_file_.c_str());
  }

  set_flags();

  start_stdin();

  if (error_ == -1)
  {
    // reset warnings
    Static::warnings = 0;

    // clear the CNF first to populate the CNF in ugrep() with the contents of arg_pattern
    Static::bcnf.clear();

    // delete old matcher, if any, to prevent preview from using it (rows_ == 0 prevents that too)
    Static::matcher.reset();

    search_thread_ = std::thread(Query::execute, search_pipe_[1]);
  }

  select_ = -1;
  select_all_ = false;
  updated_ = false;
  tick_ = 4;

  draw();
}

// display the status line
void Query::status(bool show)
{
  const int banlen = 256;
  char banner[banlen];
  size_t sf = Stats::searched_files();
  size_t ff = Stats::found_files();
  const char *bookmark = mark_.is_set() ? "^R restore" : "^X bookmark";

  // force redisplay by setting tick
  if (show)
    tick_ = 4;

  if (eof_)
  {
    if (tick_ < 8)
    {
      Screen::normal();

      if (ff == 0)
        Screen::put(1, 0, "[no matches found]");

      size_t sd = Stats::searched_dirs();
      size_t ws = Static::warnings;
      const char *pager = flag_view ? flag_view : "-";

      snprintf(banner, banlen, "%zu/%zu files | %zu dirs | %zu warnings | ^Q quit  ^T split  ^Y %s  ^Z help  %s%*s", ff, sf, sd, ws, pager, bookmark, banlen - 100, "");
      Screen::invert();
      Screen::put(maxrows_ - 1, 0, banner);
      Screen::normal();

      tick_ = 8;
    }
  }
  else
  {
    if (sf > searched_)
    {
      searched_ = sf;
      tick_ = 4;
    }

    if (ff > found_)
    {
      found_ = ff;
      tick_ = 4;
    }

    if (tick_ == 4)
    {
      Screen::normal();

      int spinner = "-\\|/"[spin_];
      const char *pager = flag_view ? flag_view : "-";

      snprintf(banner, banlen, "[%c] %zu/%zu files queued | ^Q quit  ^T split  ^Y %s  ^Z help  %s%*s", spinner, ff, sf, pager, bookmark, banlen - 100, "");
      Screen::invert();
      Screen::put(maxrows_ - 1, 0, banner);
      Screen::normal();

      spin_ = (spin_ + 1) & 3;
    }

    tick_ = (tick_ + 1) & 7;
  }
}

// update screen periodically and display more data when data becomes available
bool Query::update()
{
  int begin = rows_;

  // fetch viewable portion plus one up to two screenfuls more
  fetch(row_ - (row_ % Screen::rows) + 2 * Screen::rows);

  int end = rows_ + append_;

  // display the viewable portion when updated
  if (end > begin && begin < row_ + maxrows_ - 2)
  {
    Screen::normal();

    if (begin + maxrows_ - 2 > end)
      begin = end - maxrows_ + 2;
    if (begin < 0)
      begin = 0;
    if (begin < row_)
      begin = row_;

    for (int i = begin; i < std::min(end, row_ + maxrows_ - 2); ++i)
      disp(i);
  }

  if (error_ == -1)
  {
    if (tick_ < 8 && end < row_ + maxrows_ - 2)
    {
      int row = end - row_ + 1;

      // no dots and clear below when we hit eof
      if (eof_)
        tick_ = 4;

      Screen::setpos(row, 0);
      if (tick_ < 4)
      {
        Screen::normal();
        if (tick_ == 0)
          Screen::erase();
        else
          Screen::put(&"..."[3 - tick_]);
      }
      else if (tick_ == 4)
      {
        // clear elipsis and below
        if (flag_split)
        {
          if (eof_)
          {
            redraw();
          }
          else
          {
            // clear top half below last row
            for (int i = rows_; i < maxrows_ - 1; ++i)
              disp(i);

            // clear bottom half if nothing was found yet
            if (rows_ == 0)
            {
              Screen::setpos(maxrows_, 0);
              Screen::end();
            }
          }
        }
        else
        {
          // clear all below last row
          Screen::normal();
          Screen::end();
        }
      }
    }

    // display updated status line
    status(false);
  }
  else if (tick_ < 8)
  {
    Screen::setpos(1, 0);
    Screen::normal();
    Screen::erase();

    if (!Screen::mono)
    {
      Screen::setpos(2, 0);
      Screen::put(color_qe);
      Screen::erase();
    }

    Screen::put(2, 0, what_);
    Screen::normal();
    Screen::end();

    tick_ = 8;
  }

  // preview the first file when it arrived
  if (flag_split && begin == 0 && rows_ > 0)
    preview();

  // return true if screen was updated when data was available
  return begin < rows_;
}

// fetch rows up to and including the specified row, when available, i.e. do not block when pipe is non-blocking
bool Query::fetch(int row)
{
  int prev_rows = rows_;

  while (rows_ <= row)
  {
    bool incomplete = false;

    // look for the first newline character in the buffer
    char *nlptr = static_cast<char*>(memchr(buffer_, '\n', buflen_));

    if (nlptr == NULL)
    {
      // no newline and buffer is not full and not EOF reached yet, get more data
      if (buflen_ < QUERY_BUFFER_SIZE && !eof_)
      {
#ifdef OS_WIN_OR_MINGW

        // try to fetch more data from the non-blocking pipe when immediately available
        DWORD nread = 0;
        bool avail = !pending_;

        if (pending_)
        {
          pending_ = false;

          if (!GetOverlappedResult(hPipe_, &overlapped_, &nread, FALSE))
          {
            switch (GetLastError())
            {
              case ERROR_IO_INCOMPLETE:
                pending_ = true;
                break;

              case ERROR_MORE_DATA:
                break;

              case ERROR_HANDLE_EOF:
              default:
                close(search_pipe_[0]);
                eof_ = true;
                Static::cancel_ugrep();
            }
          }
        }

        if (avail)
        {
          pending_ = false;

          if (!ReadFile(hPipe_, buffer_ + buflen_, static_cast<DWORD>(QUERY_BUFFER_SIZE - buflen_), &nread, blocking_ ? NULL : &overlapped_))
          {
            switch (GetLastError())
            {
              case ERROR_IO_PENDING:
                pending_ = true;
                break;

              case ERROR_MORE_DATA:
                break;

              case ERROR_HANDLE_EOF:
              case ERROR_BROKEN_PIPE:
              default:
                close(search_pipe_[0]);
                eof_ = true;
                Static::cancel_ugrep();
            }
          }
        }

        buflen_ += nread;

#else

        // try to fetch more data from the non-blocking pipe when immediately available
        ssize_t nread = read(search_pipe_[0], buffer_ + buflen_, QUERY_BUFFER_SIZE - buflen_);

        if (nread > 0)
        {
          // success, more data read into the buffer
          buflen_ += nread;
        }
        else if (nread < 0)
        {
          // if pipe is empty but not EINTR/EAGAIN/EWOULDBLOCK then error (EOF)
          if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK)
          {
            close(search_pipe_[0]);
            eof_ = true;
            Static::cancel_ugrep();
          }
        }
        else
        {
          // no data, pipe is closed (EOF)
          close(search_pipe_[0]);
          eof_ = true;
          Static::cancel_ugrep();
        }

#endif
      }

      if (buflen_ == 0)
        break;

      // we may have more data now, so look again for the first newline character in the buffer
      nlptr = static_cast<char*>(memchr(buffer_, '\n', buflen_));

      if (nlptr == NULL)
      {
        // data has no newline but buffer_[] is full, so we will add it and mark it as an incomplete row
        nlptr = buffer_ + buflen_;

        if (!eof_)
          incomplete = true;
      }
    }

    if (nlptr != NULL)
    {
      // allocate more rows on demand
      if (rows_ >= static_cast<int>(view_.size()))
      {
        view_.emplace_back();
        selected_.push_back(select_all_);
      }

      // assign or append the row from the buffer
      if (append_)
        view_[rows_].append(buffer_, nlptr - buffer_);
      else
        view_[rows_].assign(buffer_, nlptr - buffer_);

      // this row is selected if select all is set
      selected_[rows_] = select_all_;

      // if row is complete, move to the next
      if (!incomplete)
      {
        // added another row
        if (++rows_ < static_cast<int>(view_.size()))
          view_[rows_].clear();

        // skip \n
        if (nlptr < buffer_ + buflen_)
          ++nlptr;
      }

      // append the next chunk of text from the buffer
      append_ = incomplete;

      // shift the buffer to make room
      buflen_ -= nlptr - buffer_;
      memmove(buffer_, nlptr, buflen_);
    }
  }

  // true if more rows were fetched
  return prev_rows < rows_;
}

// execute the search in a new thread and send results to a pipe
void Query::execute(int pipe_fd)
{
  Static::output = fdopen(pipe_fd, "wb");

  if (Static::output != NULL)
  {
    try
    {
      ugrep();
    }

    catch (reflex::regex_error& error)
    {
      what_.assign(error.what());

      // error position in the pattern
      error_ = static_cast<int>(error.pos());

      // subtract 4 for (?m) or (?misx)
      if (error_ >= 4 + flag_ignore_case + flag_dotall + flag_free_space)
        error_ -= 4 + flag_ignore_case + flag_dotall + flag_free_space;

      // subtract 2 for -F
      if (flag_mode == ::Mode::FIXED && error_ >= 2)
        error_ -= 2;

      // subtract 2 or 3 for -x or -w
      if (flags_[27].flag && error_ >= 2)
        error_ -= 2;
      else if (flags_[25].flag && error_ >= 3)
        error_ -= 3;
    }

    catch (std::exception& error)
    {
      what_.assign(error.what());

      // error at the end of the line, not within
      error_ = line_wsize();
    }

    fclose(Static::output);
    Static::output = NULL;
  }
  else
  {
    what_.assign("cannot fdopen pipe");

    // error at the end of the line, not within
    error_ = line_wsize();
  }
}

// cursor up
void Query::up()
{
  if (select_ > 0)
  {
    --select_;
    if (select_ > row_)
      return;
  }
  if (row_ > 0)
  {
    if (!flag_split)
    {
      disp(row_ - 1);
      --row_;
      Screen::pan_down();
      status(true);
      draw();
    }
    else
    {
      // split screen
      --row_;
      redraw();
    }
  }
}

// cursor down
void Query::down()
{
  if (select_ >= 0)
  {
    ++select_;
    if (select_ >= rows_)
      select_ = rows_ - 1;
    if (select_ < row_ + maxrows_ - 2)
      return;
  }
  if (row_ + 1 < rows_)
  {
    if (!flag_split)
    {
      Screen::normal();
      disp(row_ + maxrows_ - 2);
      ++row_;
      Screen::pan_up();
      status(true);
      draw();
    }
    else
    {
      // split screen
      ++row_;
      redraw();
    }
  }
}

// page up the display
void Query::pgup(bool half_page)
{
  if (select_ >= 0)
  {
    if (half_page)
      select_ -= maxrows_ / 2;
    else
      select_ -= maxrows_ - 2;
    if (select_ < 0)
      select_ = 0;
    if (select_ > row_)
      return;
  }
  if (row_ > 0)
  {
    int oldrow = row_;
    if (half_page)
      row_ -= maxrows_ / 2;
    else
      row_ -= maxrows_ - 2;
    if (row_ < 0)
      row_ = 0;
    if (!flag_split)
    {
      Screen::pan_down(oldrow - row_);
      for (int i = row_; i < oldrow; ++i)
        disp(i);
      status(true);
      draw();
    }
    else
    {
      // split screen
      redraw();
    }
  }
}

// page down the display
void Query::pgdn(bool half_page)
{
  if (select_ >= 0)
  {
    if (half_page)
      select_ += maxrows_ / 2;
    else
      select_ += maxrows_ - 2;
    if (select_ >= rows_)
      select_ = rows_ - 1;
    if (select_ < row_ + maxrows_ - 2)
      return;
  }
  if (row_ + maxrows_ - 1 <= rows_)
  {
    int oldrow = row_;
    if (half_page)
      row_ += maxrows_ / 2;
    else
      row_ += maxrows_ - 2;
    if (row_ + maxrows_ > rows_)
    {
      row_ = rows_ - maxrows_ + 2;
      if (row_ < oldrow)
        row_ = oldrow;
    }
    if (!flag_split)
    {
      int diff = row_ - oldrow;
      if (diff > 0)
      {
        Screen::normal();
        Screen::pan_up(diff);
        for (int i = row_ + maxrows_ - diff - 2; i < row_ + maxrows_ - 2; ++i)
          disp(i);
        status(true);
        draw();
      }
    }
    else
    {
      // split screen
      redraw();
    }
  }
}

// move back up one file
void Query::back()
{
  if (rows_ <= 0)
    return;

  // if output is not suitable to scroll over its content by filename, then PGUP
  if (flag_text || flag_format != NULL)
  {
    pgup();
    return;
  }

  // compare the directory part for options -l and -c to move between directories
  bool compare_dir = flag_files_with_matches || flag_count;

  // scroll current row_ or select_ selection
  int& ref = select_ == -1 ? row_ : select_;

  if (ref == 0)
    return;

  --ref;

  if (compare_dir && flag_tree)
  {
    if (ref > 0)
    {
      do
        --ref;
      while (ref > 0 && view_[ref].size() > 1);
    }
  }
  else
  {
    std::string filename;

    // get the current filename to compare against, when present
    find_filename(ref, filename);

    bool found = false;

    while (ref > 0 && !(found = find_filename(ref, filename, compare_dir)))
      --ref;

    if (found && (compare_dir || !flag_heading))
    {
      ++ref;

      // --tree: skip over directory tree spacing
      if (compare_dir && flag_tree && (view_[ref].empty() || view_[ref].front() != '\0'))
        ++ref;
    }
  }

  redraw();
}

// move down to the next file
void Query::next()
{
  // if output is not suitable to scroll by filename, then PGDN
  if (flag_text || flag_format != NULL)
  {
    pgdn();
    return;
  }

  // compare the directory part for options -l and -c to move between directories
  bool compare_dir = flag_files_with_matches || flag_count;

  // scroll current row_ or select_ selection
  int& ref = select_ == -1 ? row_ : select_;

  if (compare_dir && flag_tree)
  {
    ++ref;

    while (true)
    {
      bool found = false;

      while (ref + 1 < rows_ && !(found = view_[ref].size() <= 1))
        ++ref;

      redraw();

      if (found || (eof_ && buflen_ == 0))
        return;

      // fetch more search results when available
      if (update())
      {
        // poll keys without timeout and stop if a key was pressed
        if (VKey::poll(0))
          return;
      }
      else
      {
        // poll keys with 100 ms timeout and stop if a key was pressed
        if (VKey::poll(100))
          return;
      }
    }
  }
  else
  {
    std::string filename;

    // get the current filename to compare when present
    if (ref < rows_)
      find_filename(ref, filename);

    ++ref;

    while (true)
    {
      bool found = false;

      // seach forward for different filename or different directory
      while (ref + 1 < rows_ && !(found = find_filename(ref, filename, compare_dir)))
        ++ref;

      redraw();

      if (found || (eof_ && buflen_ == 0))
        return;

      // fetch more search results when available
      if (update())
      {
        // poll keys without timeout and stop if a key was pressed
        if (VKey::poll(0))
          return;
      }
      else
      {
        // poll keys with 100 ms timeout and stop if a key was pressed
        if (VKey::poll(100))
          return;
      }
    }
  }
}

// jump to the specified row
void Query::jump(int row)
{
  if (row < 0)
    row = 0;

  // scroll current row_ or select_ selection
  int& ref = select_ == -1 ? row_ : select_;

  if (row <= ref)
  {
    ref = row;

    if (ref >= rows_)
      ref = rows_ - 1;
  }
  else if (row < rows_)
  {
    ref = row;
  }
  else
  {
    while (true)
    {
      while (ref < row && ref + 1 < rows_)
        ++ref;

      // exit if at the desired row or if the desired row is beyond the end of the search results
      if (ref == row || (eof_ && buflen_ == 0))
        break;

      redraw();

      // fetch more search results when available
      if (update())
      {
        // poll keys without timeout and stop if a key was pressed
        if (VKey::poll(0))
          return;
      }
      else
      {
        // poll keys with 100 ms timeout and stop if a key was pressed
        if (VKey::poll(100))
          return;
      }
    }
  }

  redraw();
}

// skip over ANSI escape sequence when present on a line
static size_t skip_escapes(const std::string& line, size_t pos)
{
  size_t end = line.size();

  while (pos < end && line.at(pos) == '\033')
  {
    if (pos + 1 < end && line.at(pos + 1) == '[')
    {
      // CSI \e[... sequence
      pos += 2;
      while (pos < end && (line.at(pos) < 0x40 || line.at(pos) > 0x7e))
        ++pos;
      ++pos;
    }
    else if (pos + 1 < end && line.at(pos + 1) == ']')
    {
      // OSC \e]...BEL|ST sequence
      pos += 2;
      while (pos < end && line.at(pos) != '\a' && (line.at(pos) != '\033' || (pos + 1 < end && line.at(pos + 1) != '\\')))
        ++pos;
      if (pos < end && line.at(pos) == '\033')
        ++pos;
      ++pos;
    }
    else
    {
      // ESC x sequence
      pos += 2;
    }
  }

  return pos;
}

// view or edit the file located under the cursor or just above in the screen (when not in selection mode)
void Query::view()
{
  if (row_ >= rows_ || flag_text || flag_format != NULL)
  {
    Screen::alert();
    return;
  }

  // if no viewer, then give up
  if (flag_view == NULL || *flag_view == '\0' || (*flag_view == '+' && flag_view[1] == '\0'))
  {
    Screen::alert();
    return;
  }

  bool keypress = (*flag_view == '+');
  std::string command(flag_view + keypress);
  int ref = select_ >= 0 ? select_ : row_;
  size_t line_number = 0;

  // -n: extract line number from ugrep output if the specified pager supports +linenum
  if (flag_line_number &&
      !flag_files_with_matches &&
      !flag_count &&
      !flag_hex &&
      !flag_with_hex &&
      (command == "less"       ||
       command == "moar-pager" ||
       command == "moar"       ||
       command == "moor"       ||
       command == "more"       ||
       command == "most"       ||
       command == "w3m"        ||
       command == "emacs"      ||
       command == "joe"        ||
       command == "vi"         ||
       command == "vim"        ||
       command == "vis"        ||
       command == "kak"        ||
       command == "nano"       ||
       command == "pico"       ||
       command == "vile"       ||
       command == "zile"))
  {
    line_number = get_line_number();
  }

  std::string filename;
  std::string partname;
  bool found = false;

  if (rows_ > 0)
  {
    if (flag_tree && (flag_files_with_matches || flag_count))
    {
      // --tree with -c or -l: move down over non-filename lines to reach a filename
      while (ref + 1 < rows_ && (view_[ref].empty() || view_[ref].front() != '\0'))
        ++ref;
    }
    else
    {
      // move down to skip empty lines
      while (ref + 1 < rows_ && view_[ref].empty())
        ++ref;
    }

    // move up until a filename header is found, unless we're at an empty line
    if (!view_[ref].empty())
      while (ref >= 0 && !(found = find_filename(ref, filename, false, true, &partname)))
        --ref;

    // only a partname was present, no filename
    if (filename.empty())
      found = false;

    if (!found)
    {
      if (Static::arg_files.size() == 1)
      {
        // if not found and only one FILE argument was provided then view FILE
        filename = Static::arg_files.front();
        if (filename == "-")
          filename = flag_label;
        found = true;
      }
      else if (flag_stdin && Static::arg_files.empty())
      {
        // view standard input
        filename = flag_label;
        found = true;
      }
    }
  }

  if (found)
  {
#ifdef OS_WIN_OR_MINGW
    _WIN32_FILE_ATTRIBUTE_DATA attr_before;
#else
    struct stat buf;
    uint64_t mtime = 0;
#endif

    if (!flag_stdin || filename != flag_label)
    {
      // check if the file exists and is a regular file, also get the file's last modification time
#ifdef OS_WIN_OR_MINGW
      found = GetFileAttributesExW(utf8_decode(filename).c_str(), GetFileExInfoStandard, &attr_before);
#else
      found = stat(filename.c_str(), &buf) == 0 && S_ISREG(buf.st_mode);
#if defined(HAVE_STAT_ST_ATIM) && defined(HAVE_STAT_ST_MTIM) && defined(HAVE_STAT_ST_CTIM)
      mtime = static_cast<uint64_t>(buf.st_mtim.tv_sec) * 1000000 + buf.st_mtim.tv_nsec / 1000;
#elif defined(HAVE_STAT_ST_ATIMESPEC) && defined(HAVE_STAT_ST_MTIMESPEC) && defined(HAVE_STAT_ST_CTIMESPEC)
      mtime = static_cast<uint64_t>(buf.st_mtimespec.tv_sec) * 1000000 + buf.st_mtimespec.tv_nsec / 1000;
#else
      mtime = static_cast<uint64_t>(buf.st_mtime);
#endif
#endif
    }

    if (found)
    {
      // -n: add +line_number
      if (line_number > 0)
        command.append(" +").append(std::to_string(line_number));

      FILE *pager = NULL;

#ifdef OS_WIN_OR_MINGW
      std::wstring wcommand;
#endif

      Screen::clear();
      Screen::invert();
      Screen::put(command.c_str());
      Screen::put(' ');
      Screen::put(filename.c_str());
      if (!partname.empty())
      {
        Screen::put('{');
        Screen::put(partname.c_str());
        Screen::put('}');
      }
      Screen::normal();
      Screen::setpos(1, 0);

#ifndef OS_WIN_OR_MINGW
      // normal tty mode
      VKey::cleanup();
#endif

      // executing the command is OK, unless checks fail, pipe fails, or command fails
      bool ok = true;

      // file was changed when viewed, e.g. by an editor?
      bool changed = false;

      // track elapsed time: if the command terminates very quickly within 100ms, then wait 1s
      reflex::timer_type et;
      reflex::timer_start(et);

      if (flag_stdin && filename == flag_label)
      {
        // standard input is viewed via a pipe to the pager
#ifdef OS_WIN
        pager = popen(command.c_str(), "wb");
#else
        pager = popen(command.c_str(), "w");
#endif
        if (pager != NULL)
          fwrite(stdin_buffer_.c_str(), 1, stdin_buffer_.size(), pager);
      }
      else if (!partname.empty())
      {
        // archive part is viewed via a pipe to the pager
#ifdef OS_WIN
        pager = popen(command.c_str(), "wb");
#else
        pager = popen(command.c_str(), "w");
#endif
        if (pager != NULL)
        {
          try
          {
            ugrep_extract(filename.c_str(), partname.c_str(), pager);
          }

          catch (...)
          {
            // this should never happen, but just in case we ignore errors
            ok = false;
          }
        }
      }
      else
      {
#ifdef OS_WIN_OR_MINGW
        if (filename.empty() || filename.at(0) == '/' || filename.find('"') != std::string::npos)
        {
          // illegal filename in Windows, should never happen, but just in case
          ok = false;
        }
        else
        {
          // view file in the pager using Windows _wsystem() call
          command.append(" \"").append(filename).append("\"");
          // Windows system() does not support non-ASCII, instead we use a wide string with _wsystem()
          wcommand = utf8_decode(command);
          // flush before calling _wsystem(), according to the Window's system API documentation
          // _flushall(); removed because this may cause the TUI to freeze
        }
#else
        if (filename.find('\'') != std::string::npos)
        {
          // incorrect filename
          ok = false;
        }
        else
        {
          // view file in the pager using system() call, double -- ends options
          command.append(" -- '").append(filename).append("'");
        }
#endif
      }

      if (ok)
      {
        if ((flag_stdin && filename == flag_label) || !partname.empty())
        {
          // popen() pipe to pager is OK?
          ok = (pager != NULL);

          if (ok)
          {
            // close the pipe to the pager
            pclose(pager);
            pager = NULL;
          }
        }
        else
        {
          // execute command, check if OK
#ifdef OS_WIN_OR_MINGW
          ok = (_wsystem(wcommand.c_str()) == 0);
#else
          ok = (system(command.c_str()) == 0);
#endif

          // check if file was changed by the pager (when it is an editor)
#ifdef OS_WIN_OR_MINGW
          _WIN32_FILE_ATTRIBUTE_DATA attr_after;
          changed = GetFileAttributesExW(utf8_decode(filename).c_str(), GetFileExInfoStandard, &attr_after) == 0 ||
            attr_before.ftLastWriteTime.dwLowDateTime != attr_after.ftLastWriteTime.dwLowDateTime ||
            attr_before.ftLastWriteTime.dwHighDateTime != attr_after.ftLastWriteTime.dwHighDateTime;
#else
          stat(filename.c_str(), &buf);
#if defined(HAVE_STAT_ST_ATIM) && defined(HAVE_STAT_ST_MTIM) && defined(HAVE_STAT_ST_CTIM)
          changed = (mtime != static_cast<uint64_t>(buf.st_mtim.tv_sec) * 1000000 + buf.st_mtim.tv_nsec / 1000);
#elif defined(HAVE_STAT_ST_ATIMESPEC) && defined(HAVE_STAT_ST_MTIMESPEC) && defined(HAVE_STAT_ST_CTIMESPEC)
          changed = (mtime != static_cast<uint64_t>(buf.st_mtimespec.tv_sec) * 1000000 + buf.st_mtimespec.tv_nsec / 1000);
#else
          changed = (mtime != static_cast<uint64_t>(buf.st_mtime));
#endif
#endif
        }
      }

#ifndef OS_WIN_OR_MINGW
      // resume RAW tty mode and flush the key buffer
      VKey::setup(VKey::TTYRAW);
#else
      // flush the key buffer
      VKey::flush();
#endif

#ifdef OS_WIN
      if (ok && (keypress || strcmp(flag_view, "more") == 0))
#else
      if (ok && keypress)
#endif
      {
        Screen::setpos(Screen::rows - 1, 0);
        Screen::invert();
        Screen::put("(END)");
        Screen::normal();
        Screen::put(" press a key ");
        VKey::get();
      }
      else if (reflex::timer_elapsed(et) < 100)
      {
        // command terminated very quickly within 100ms
        std::this_thread::sleep_for(std::chrono::seconds(1));
      }

      if (changed)
      {
        // file is changed, update the search results
        search();
        jump(ref);
      }
      else
      {
        redraw();
      }

      if (!ok)
      {
        Screen::alert();
        message(std::string("failed: ").append(command));
      }
    }
  }

  if (!found && (!filename.empty() || !partname.empty()))
  {
    std::string problem("cannot view or edit ");
    if (partname.empty())
      problem.append(filename);
    else
      problem.append(partname).append(" in ").append(filename);
    message(problem);
  }
}

// preview the file located under the cursor when the screen is split into multiple windows
void Query::preview()
{
  if (!flag_split || error_ != -1)
    return;

  bool found = false;
  std::string filename;
  std::string partname;

  if (rows_ > 0)
  {
    int ref = row_;

    // --tree with -c or -l: move down over non-filename lines to reach a filename
    if (flag_tree && (flag_files_with_matches || flag_count))
      while (ref + 1 < rows_ && (view_[ref].empty() || view_[ref].front() != '\0'))
        ++ref;

    // find a filename upwards, unless we're at an empty line
    if (!view_[ref].empty())
      while (ref >= 0 && !(found = find_filename(ref, filename, false, true, &partname)))
        --ref;

    // only a partname was present, no filename
    if (filename.empty())
      found = false;

    if (found && filename.back() == PATHSEPCHR)
    {
      found = false;
    }
    else if (!found && Static::arg_files.size() == 1)
    {
      // if not found an only one FILE argument was provided then preview FILE
      filename = Static::arg_files.front();
      found = true;
    }

    // cannot preview standard input
    if (found && flag_stdin && filename.compare(flag_label) == 0)
      found = false;
  }

  // no problems to report
  const char *problem = NULL;

  if (found)
  {
    // get line number if any, zero otherwise
    size_t from_lineno =
      flag_line_number &&
      !flag_files_with_matches &&
      !flag_count && !flag_hex &&
      !flag_with_hex ? get_line_number() : 0;

    // has the filename or partname or line number changed?
    if (from_lineno == 0 ||
        from_lineno != prevfrom_ ||
        filename != prevfile_ ||
        partname != prevpart_)
    {
      size_t max = Screen::rows - maxrows_ - 1;

      try
      {
        ugrep_find_text_preview(filename.c_str(), partname.c_str(), from_lineno, max, prevline_, prevnum_, preview_);
      }

      catch (...)
      {
        // this should never happen, but just in case we ignore errors
        prevnum_ = 0;
      }

      prevfile_.swap(filename);
      prevpart_.swap(partname);
      prevfrom_ = from_lineno;
    }

    if (prevnum_ == 0)
      problem = "[no matches to display]";

    if (problem == NULL)
    {
      char header[256];
      if (prevpart_.empty())
        snprintf(header, sizeof(header), "%s%s%s%s%s:%s%s%zu%s", color_off, color_fn, prevfile_.c_str(), color_off, color_se, color_off, color_ln, prevline_, color_off);
      else
        snprintf(header, sizeof(header), "%s%s%s{%s}%s%s:%s%s%zu%s", color_off, color_fn, prevfile_.c_str(), prevpart_.c_str(), color_off, color_se, color_off, color_ln, prevline_, color_off);
      Screen::put(maxrows_, 0, header);
      Screen::normal();

      // (re)display the (cached) preview text
      int row = maxrows_ + 1;
      for (size_t i = 0; i < prevnum_ && row < Screen::rows; ++i)
        row = Screen::put(row, 0, preview_[i], 0, 0);

      if (row < Screen::rows)
        Screen::end();
    }
  }
  else
  {
    problem = "[no file selected]";
  }

  if (problem != NULL)
  {
    Screen::normal();
    Screen::put(maxrows_, 0, problem);
    Screen::end();

    prevfile_.clear();
    prevpart_.clear();
    prevnum_ = 0;
    prevline_ = 0;
  }
}

// chdir one level down into the directory of the file located under the cursor or just above the screen
void Query::select()
{
  if (!selected_file_.empty())
  {
    Screen::alert();
    return;
  }

  int ref = select_ >= 0 ? select_ : row_;

  // --tree: move down over non-filename lines to reach a filename
  if (flag_tree && (flag_files_with_matches || flag_count))
  {
    while (ref + 1 < rows_ && (view_[ref].empty() || view_[ref].front() != '\0'))
      ++ref;
  }
  else
  {
    // move down to skip empty lines
    while (ref + 1 < rows_ && view_[ref].empty())
      ++ref;
  }

  std::string pathname;
  bool found = false;

  // move up until a filename header is found
  while (ref >= 0 && !(found = find_filename(ref, pathname, false, true)))
    --ref;

  if (pathname.empty())
    found = false;

  if (found)
  {
    // cannot view standard input
    if (flag_stdin && pathname == flag_label)
    {
      message("cannot chdir to standard input");
      return;
    }

    if (globbing_)
    {
      globbing_ = false;

      memcpy(line_, temp_, sizeof(Line));
      len_ = line_len();
      move(len_);

      set_prompt();
    }

    if (history_.empty())
      files_.swap(Static::arg_files);

    history_.emplace();
    history_.top().save();

    if (flag_directories_action != Action::RECURSE)
    {
      flags_[18].flag = flag_dereference;
      flags_[19].flag = !flag_dereference;
    }

    mark_.reset();

    size_t n = pathname.find(PATHSEPCHR, 1); // ignore PATHSEPCHR at front when present
    size_t b = pathname.find('{'); // do not cd into archives
    if (n != std::string::npos && (b == std::string::npos || n < b))
    {
      std::string dir(pathname.substr(0, n + 1));

      if (chdir(dir.c_str()) < 0)
      {
        message("cannot chdir: operation denied");
        history_.pop();
        if (history_.empty())
          files_.swap(Static::arg_files);
        return;
      }

      // absolute dir or relative dir
      if (dir.front() == PATHSEPCHR)
        dirs_.assign(dir);
      else
        dirs_.append(dir);

      pathname.erase(0, n + 1);

      // a directory change affects --hyperlink
      set_terminal_hyperlink();

      search();
    }
    else
    {
      if (b != std::string::npos)
        pathname.resize(b);

      selected_file_ = pathname;

      dirs_.append(pathname);

      search();
    }
  }
  else
  {
    Screen::alert();
  }
}

// chdir back up one level
void Query::deselect()
{
  if (selected_file_.empty())
  {
    if (flag_stdin)
    {
      message("cannot chdir .. because standard input is searched");
      return;
    }

    if (!Static::arg_files.empty())
    {
      message("cannot chdir .. because file or directory arguments are present");
      return;
    }

#ifdef OS_WIN
    if (dirs_.size() == 3 && dirs_.at(1) == ':' && dirs_.at(2) == PATHSEPCHR)
      return;
#else
    if (dirs_ == PATHSEPSTR)
      return;
#endif

    if (dirs_.empty())
    {
      char *cwd = getcwd0();
      if (cwd != NULL)
      {
        size_t n = strlen(cwd);
        dirs_.assign(cwd);
        wdir_.assign(cwd);
        if (n == 0 || cwd[n-1] != PATHSEPCHR)
          dirs_.append(PATHSEPSTR);
        free(cwd);
      }
    }

    if (dirs_.empty())
    {
      if (chdir("..") < 0)
        return;

      dirs_.assign(".." PATHSEPSTR);
    }
    else
    {
      dirs_.pop_back();

      size_t n = dirs_.find_last_of(PATHSEPCHR);

      if (n != std::string::npos && dirs_.front() == PATHSEPCHR)
      {
        if (chdir(dirs_.substr(0, n + 1).c_str()) < 0)
          return;

        dirs_.resize(n + 1);
      }
      else
      {
        if (chdir("..") < 0)
          return;

        if (n == std::string::npos)
        {
          if (dirs_ != "..")
            dirs_.clear();
          else
            dirs_.append(PATHSEPSTR ".." PATHSEPSTR);
        }
        else if (dirs_.compare(n + 1, std::string::npos, "..") == 0)
        {
          dirs_.append(PATHSEPSTR ".." PATHSEPSTR);
        }
        else
        {
          dirs_.resize(n + 1);
        }
      }
    }

    // a directory change affects --hyperlink
    set_terminal_hyperlink();
  }
  else
  {
    size_t n = dirs_.find_last_of(PATHSEPCHR);

    if (n != std::string::npos)
      dirs_.resize(n + 1);
    else
      dirs_.clear();

    deselect_file_ = true;
    Static::arg_files.clear();
  }

  mark_.reset();

  if (!history_.empty())
  {
    int old_row;
    history_.top().restore(old_row);
    history_.pop();
    if (history_.empty())
      files_.swap(Static::arg_files);
    globbing_ = false;
    set_prompt();
    len_ = line_len();
    search();
    jump(old_row);
  }
  else
  {
    search();
  }
}

// chdir back to the starting working directory
void Query::unselect()
{
  if (!wdir_.empty())
  {
    if (chdir(wdir_.c_str()) < 0)
      return;

    // a directory change affects --hyperlink
    set_terminal_hyperlink();
  }
  else if (!dirs_.empty())
  {
    if (!selected_file_.empty())
    {
      size_t n = dirs_.find_last_of(PATHSEPCHR);

      if (n != std::string::npos)
        dirs_.resize(n + 1);
      else
        dirs_.clear();
    }

    if (!dirs_.empty())
    {
      while (true)
      {
#ifdef OS_WIN
        if (dirs_.size() == 3 && dirs_.at(1) == ':' && dirs_.at(2) == PATHSEPCHR)
          break;
#else
        if (dirs_ == PATHSEPSTR)
          break;
#endif

        if (chdir("..") < 0)
          break;

        dirs_.pop_back();

        size_t n = dirs_.find_last_of(PATHSEPCHR);

        if (n == std::string::npos)
          break;

        dirs_.resize(n + 1);
      }

      // a directory change affects --hyperlink
      set_terminal_hyperlink();
    }
  }

  dirs_.clear();
  wdir_.clear();
  deselect_file_ = true;
  Static::arg_files.clear();

  mark_.reset();

  if (!history_.empty())
  {
    while (history_.size() > 1)
      history_.pop();
    int old_row;
    history_.top().restore(old_row);
    history_.pop();
    files_.swap(Static::arg_files);
    globbing_ = false;
    set_prompt();
    len_ = line_len();
    search();
    jump(old_row);
  }
  else
  {
    search();
  }
}

// display a message
void Query::message(const std::string& message)
{
  Screen::normal();
  if (!Screen::mono)
    Screen::put(color_qp);
  Screen::put(0, 0, "-> ");
  Screen::normal();
  Screen::put(0, 3, message.c_str());
  message_ = true;
}

// return true if prompt is confirmed
bool Query::confirm(const char *prompt)
{
  if (!flag_confirm)
    return true;

  message(std::string(prompt).append("? (y/n) [n] "));

  VKey::flush();

  int key = VKey::get();

  if (key == 'y' || key == 'Y')
    return true;

  message_ = false;
  draw();

  return false;
}

// display help page
bool Query::help()
{
  Mode oldMode = mode_;

  mode_ = Mode::HELP;

  Screen::clear();
  redraw();

  bool ctrl_q = false; // CTRL-Q is pressed
  bool ctrl_o = false; // CTRL-O is pressed
  bool restart = false; // META key pressed, restart search when exiting the help screen

  while (mode_ == Mode::HELP)
  {
    int key;

    Screen::put(0, Screen::cols - 1, "?");

#ifdef OS_WIN_OR_MINGW

    while (true)
    {
      key = VKey::in(500);

      if (key > 0)
        break;

      // detect screen size changes
      int rows = Screen::rows;
      int cols = Screen::cols;

      Screen::getsize();

      if (rows != Screen::rows || cols != Screen::cols)
        redraw();
    }

#else

    key = VKey::get();

#endif

    if (ctrl_o)
    {
      meta(key);
      ctrl_o = false;
      restart = true;
    }
    else if (key == VKey::CTRL_Q)
    {
      ctrl_q = true;
      break;
    }
    else if (key == VKey::ESC || key == VKey::CTRL_Z || key == VKey::FN(1))
    {
      break;
    }
    else
    {
      switch (key)
      {
        case VKey::CTRL_L:
          redraw();
          break;

        case VKey::CTRL_C:
          if (confirm("Exit"))
            return true;
          redraw();
          break;

        case VKey::CTRL_O:
          ctrl_o = true;
          break;

        case VKey::CTRL_BS:
#ifdef OS_WIN_OR_MINGW
          GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
#else
          raise(SIGTERM);
#endif
          break;

        case VKey::CTRL_RB: // CTRL-]: toggle colors/mono
          Screen::mono = !Screen::mono;
          redraw();
          break;

        case VKey::META:
          meta(VKey::get());
          restart = true;
          break;

        default:
          if (key < 0x80)
          {
            meta(key);
            restart = true;
          }
          else
          {
#ifdef __APPLE__
            Screen::put(color_qe);
            Screen::put(0, 0, "MacOS Terminal->Preferences->Profiles->Keyboard: enable \"Use Option as Meta key\"");
#endif
            Screen::alert();
          }
      }
    }
  }

  mode_ = oldMode;

  message_ = false;

  Screen::clear();
  redraw();

  if (restart)
    search();

  return ctrl_q;
}

// Alt/Meta/Option key
void Query::meta(int key)
{
  if (key == 'E' || key == 'Q')
  {
    if (flags_[5].flag || flags_[6].flag || flags_[17].flag || flags_[30].flag)
    {
      // option -E: reset -F, -G, -P, -Z to switch back to normal mode (the Q> prompt)
      flags_[5].flag = false;
      flags_[6].flag = false;
      flags_[17].flag = false;
      flags_[30].flag = false;

      // search when in QUERY mode, otherwise refresh HELP display
      if (mode_ == Mode::QUERY)
        search();
      else
        redraw();

      std::string msg;

      msg.assign("\033[7mM-E\033[m extended regex \033[32;1mon\033[m");

      message(msg);

      set_prompt();
    }

    return;
  }

  for (Flags *fp = flags_; fp->text != NULL; ++fp)
  {
    if (fp->key == key)
    {
      if (!fp->flag)
      {
        switch (key)
        {
          case 'A':
            flags_[1].flag = false;
            flags_[3].flag = false;
            flags_[4].flag = false;
            flags_[14].flag = false;
            flags_[29].flag = false;
            break;

          case 'B':
            flags_[0].flag = false;
            flags_[3].flag = false;
            flags_[4].flag = false;
            flags_[14].flag = false;
            flags_[29].flag = false;
            break;

          case 'b':
          case 'k':
          case 'n':
            flags_[4].flag = false;
            flags_[14].flag = false;
            break;

          case 'C':
            flags_[0].flag = false;
            flags_[1].flag = false;
            flags_[4].flag = false;
            flags_[14].flag = false;
            flags_[29].flag = false;
            break;

          case 'c':
            flags_[0].flag = false;
            flags_[1].flag = false;
            flags_[2].flag = false;
            flags_[3].flag = false;
            flags_[13].flag = false;
            flags_[14].flag = false;
            flags_[16].flag = false;
            flags_[29].flag = false;
            break;

          case 'F':
            flags_[6].flag = false;
            flags_[17].flag = false;
            break;

          case 'G':
            flags_[5].flag = false;
            flags_[17].flag = false;
            flags_[30].flag = false;
            break;

          case 'H':
            flags_[9].flag = false;
            break;

          case 'h':
            flags_[8].flag = false;
            break;

          case 'I':
            flags_[24].flag = false;
            flags_[26].flag = false;
            break;

          case 'i':
            flags_[12].flag = false;
            break;

          case 'j':
            flags_[11].flag = false;
            break;

          case 'l':
            flags_[0].flag = false;
            flags_[1].flag = false;
            flags_[2].flag = false;
            flags_[3].flag = false;
            flags_[4].flag = false;
            flags_[13].flag = false;
            flags_[16].flag = false;
            flags_[29].flag = false;
            break;

          case 'o':
            flags_[4].flag = false;
            flags_[14].flag = false;
            flags_[29].flag = false;
            break;

          case 'P':
            flags_[5].flag = false;
            flags_[6].flag = false;
            flags_[30].flag = false;
            break;

          case 'R':
            flags_[19].flag = false;
            for (int i = 33; i <= 41; ++i)
              flags_[i].flag = false;
            break;

          case 'r':
            flags_[18].flag = false;
            for (int i = 33; i <= 41; ++i)
              flags_[i].flag = false;
            break;

          case 'W':
            flags_[10].flag = false;
            flags_[26].flag = false;
            break;

          case 'w':
            flags_[26].flag = false;
            break;

          case 'X':
            flags_[10].flag = false;
            flags_[24].flag = false;
            break;

          case 'x':
            flags_[25].flag = false;
            break;

          case 'y':
            flags_[0].flag = false;
            flags_[1].flag = false;
            flags_[3].flag = false;
            flags_[4].flag = false;
            flags_[14].flag = false;
            flags_[16].flag = false;
            break;

          case 'Z':
            flags_[6].flag = false;
            flags_[17].flag = false;
            break;

          case '1':
          case '2':
          case '3':
          case '4':
          case '5':
          case '6':
          case '7':
          case '8':
          case '9':
            for (int i = 33; i <= 41; ++i)
              flags_[i].flag = false;
            if (!flags_[18].flag && !flags_[19].flag)
              flags_[18].flag = true;
            break;

          case '@':
            flags_[10].flag = false;
            break;

          case '~':
            flags_[47].flag = false;
            flags_[48].flag = false;
            flags_[49].flag = false;
            break;

          case '#':
            flags_[46].flag = false;
            flags_[48].flag = false;
            flags_[49].flag = false;
            break;

          case '$':
            flags_[46].flag = false;
            flags_[47].flag = false;
            flags_[49].flag = false;
            break;

          case '!':
            flags_[46].flag = false;
            flags_[47].flag = false;
            flags_[48].flag = false;
            break;
        }
      }
      else
      {
        switch (key)
        {
          case 'n':
            if (flags_[4].flag || flags_[14].flag)
            {
              flags_[4].flag = false;
              flags_[14].flag = false;
              flags_[15].flag = false;
            }
            break;

          case 'R':
          case 'r':
            for (int i = 33; i <= 41; ++i)
              flags_[i].flag = false;
            break;
        }
      }

      if (key == 'g')
      {
        if (mode_ != Mode::QUERY && !globbing_)
        {
          mode_ = Mode::QUERY;
          Screen::clear();
          redraw();
        }
        
        if (mode_ == Mode::QUERY)
        {
          if (!globbing_)
          {
            globbing_ = true;

            memcpy(temp_, line_, sizeof(Line));
            size_t num = globs_.size();
            if (num >= sizeof(Line))
              num = sizeof(Line) - 1;
            memcpy(line_, globs_.c_str(), num);
            line_[num] = '\0';

            len_ = line_len();
            move(len_);

            set_prompt();
          }
          else
          {
            globbing_ = false;

            memcpy(line_, temp_, sizeof(Line));
            len_ = line_len();
            move(len_);

            set_prompt();
          }

          draw();
        }
        else
        {
          mode_ = Mode::QUERY;
        }
      }
#if !defined(HAVE_PCRE2) && !defined(HAVE_BOOST_REGEX)
      else if (key == 'P')
      {
        message("option -P is not available in this build configuration of ugrep");
      }
#endif
#ifndef HAVE_LIBZ
      else if (key == 'z')
      {
        message("Option -z is not available in this build configuration of ugrep");
      }
#endif
      else
      {
        std::string msg;
        msg.assign("\033[7mM- \033[m ").append(fp->text);
        msg[6] = fp->key;

        switch (key)
        {
          case '[':
            if (!flags_[26].flag || flag_hexdump == NULL)
              if (!flags_[0].flag && !flags_[1].flag)
                flags_[3].flag = true;

            if (flags_[16].flag)
            {
              if (only_context_ > 1)
                --only_context_;
              msg.append(" to ").append(std::to_string(only_context_));
            }
            else
            {
              if (context_ > 1)
                --context_;
              msg.append(" to ").append(std::to_string(context_));
            }
            flags_[4].flag = false;
            flags_[14].flag = false;
            break;

          case ']':
            if (flags_[26].flag && flag_hexdump != NULL)
            {
              ++context_;
              msg.append(" to ").append(std::to_string(context_));
            }
            else if (flags_[16].flag)
            {
              if (flags_[0].flag || flags_[1].flag || flags_[3].flag)
                ++only_context_;
              else if (!flags_[0].flag && !flags_[1].flag)
                flags_[3].flag = true;
              msg.append(" to ").append(std::to_string(only_context_));
            }
            else
            {
              if (flags_[0].flag || flags_[1].flag || flags_[3].flag)
                ++context_;
              else if (!flags_[0].flag && !flags_[1].flag)
                flags_[3].flag = true;
              msg.append(" to ").append(std::to_string(context_));
            }
            flags_[4].flag = false;
            flags_[14].flag = false;
            break;

          case '{':
            flags_[30].flag = true;

            if ((fuzzy_ & 0xff) > 1)
              fuzzy_ = ((fuzzy_ & 0xff) - 1) | (fuzzy_ & 0xff00);

            msg.append(" to ").append(std::to_string(fuzzy_ & 0xff));
            break;

          case '}':
            if (flags_[30].flag && (fuzzy_ & 0xff) < 0xff)
              fuzzy_ = ((fuzzy_ & 0xff) + 1) | (fuzzy_ & 0xff00);
            else
              flags_[30].flag = true;

            msg.append(" to ").append(std::to_string(fuzzy_ & 0xff));
            break;

          case '%':
            if (fp->flag)
            {
              if (!flag_files)
              {
                flag_files = true;
                msg.append(" \033[32;1mfiles\033[m");
              }
              else
              {
                fp->flag = false;
                flag_files = false;
                msg.append(" \033[31;1moff\033[m");
              }
            }
            else
            {
              fp->flag = true;
              msg.append(" \033[32;1mlines\033[m");
            }
            break;

          default:
            fp->flag = !fp->flag;
            msg.append(fp->flag ? " \033[32;1mon\033[m" : " \033[31;1moff\033[m");
        }

        // search when in QUERY mode, otherwise refresh HELP display
        if (mode_ == Mode::QUERY)
          search();
        else
          redraw();

        message(msg);

        set_prompt();
      }

      return;
    }
  }

  Screen::alert();
}

// true if at least one line is selected
bool Query::selections()
{
  if (select_all_ && rows_ > 0)
    return true;

  for (int i = 0; i < rows_; ++i)
    if (selected_[i])
      return true;

  return false;
}

// print selected query results when done
void Query::print()
{
  int i = 0;

  // output selected query results
  while (i < rows_)
  {
    if (selected_[i])
      if (!print(view_[i]))
        return;
    view_[i].clear();
    ++i;
  }

  // if all lines are selected, output the remaining lines that aren't in view
  if (select_all_ && (!eof_ || buflen_ > 0))
  {
    // reading the search pipe should block until data is received
#ifdef OS_WIN_OR_MINGW
    blocking_ = true;
    pending_ = false;
#else
    set_blocking(search_pipe_[0]);
#endif

    while (!eof_ || buflen_ > 0)
    {
      // if not appending to a row, start over at the begin of the view to conserve memory
      if (!append_)
        rows_ = 0;

      i = rows_;

      // fetch a bunch more lines, if no more lines to fetch then break
      fetch(rows_ + 1000);

      while (i < rows_)
      {
        if (!print(view_[i]))
          return;
        view_[i].clear();
        ++i;
      }
    }
  }
}

// print one row of query results, return false on failure
bool Query::print(const std::string& line)
{
  if (line.empty())
    return fwrite("\n", 1, 1, stdout) == 1;

  const char *text = line.c_str();
  const char *end = text + line.size();

  // how many NULs to ignore that are part of the pathname marking in headers?
  int nulls = *text == '\0' && !flag_text ? 2 : 0;

  if (nulls > 0)
    ++text;

  const char *ptr = text;

  // if colors are turned off, then output the selected line without its CSI sequences
  if (flag_color_query == NULL || Screen::mono)
  {
    while (ptr < end)
    {
      if (*ptr == '\0' && nulls > 0)
      {
        size_t nwritten = fwrite(text, 1, ptr - text, stdout);

        if (text + nwritten < ptr)
          return false;

        --nulls;

        text = ++ptr;
      }
      else if (*ptr == '\033')
      {
        size_t nwritten = fwrite(text, 1, ptr - text, stdout);

        if (text + nwritten < ptr)
          return false;

        ++ptr;

        if (*ptr == '[')
        {
          ++ptr;
          while (ptr < end && !isalpha(static_cast<unsigned char>(*ptr)))
            ++ptr;
        }

        if (ptr < end)
          ++ptr;

        text = ptr;
      }
      else
      {
        ++ptr;
      }
    }

    size_t nwritten = fwrite(text, 1, ptr - text, stdout);

    if (text + nwritten < ptr)
      return false;
  }
  else if (nulls > 0)
  {
    while (ptr < end && nulls > 0)
    {
      if (*ptr == '\0')
      {
        size_t nwritten = fwrite(text, 1, ptr - text, stdout);

        if (text + nwritten < ptr)
          return false;

        --nulls;

        text = ++ptr;
      }
      else
      {
        ++ptr;
      }
    }

    size_t nwritten = fwrite(text, 1, end - text, stdout);

    if (text + nwritten < end)
      return false;
  }
  else
  {
    size_t nwritten = fwrite(line.c_str(), 1, line.size(), stdout);

    if (nwritten < line.size())
      return false;
  }

  if (fwrite("\n", 1, 1, stdout) < 1)
    return false;

  return true;
}

// initialze local flags with the global flags
void Query::get_flags()
{
  // remember file and directory restrictions, when specified, to enable/disable with ALT-@
  exclude_ = flag_exclude;
  exclude_dir_ = flag_exclude_dir;
  include_ = flag_include;
  include_dir_ = flag_include_dir;
  file_magic_ = flag_file_magic;
  ignore_files_ = flag_ignore_files;

  // remember the context size, when specified
  if (flag_hexdump != NULL)
  {
    if (flag_hex_after > 0)
      context_ = flag_hex_after;
    else if (flag_hex_before > 0)
      context_ = flag_hex_before;
    else
      context_ = 0;
  }
  else if (flag_only_matching)
  {
    if (flag_after_context > 0)
      only_context_ = flag_after_context;
    else if (flag_before_context > 0)
      only_context_ = flag_before_context;
  }
  else
  {
    if (flag_after_context > 0)
      context_ = flag_after_context;
    else if (flag_before_context > 0)
      context_ = flag_before_context;
  }

  // remember the --fuzzy max, when specified
  if (flag_fuzzy > 0)
    fuzzy_ = flag_fuzzy;

  // remember --dotall
  dotall_ = flag_dotall;

  // get the -g, -O, and -t globs
  for (auto& glob : flag_glob)
  {
    if (!glob.empty())
    {
      if (!globs_.empty())
        globs_.push_back(',');
      globs_.append(glob);
    }
  }

  // get the --iglob
  for (auto& glob : flag_iglob)
  {
    if (!glob.empty())
    {
      if (!globs_.empty())
        globs_.push_back(',');
      globs_.append(glob);
    }
  }

  // if any --iglob is specified, make all globs case-insensitive
  if (!flag_iglob.empty())
    flag_glob_ignore_case = true;

  // get the interactive flags from the ugrep flags
  flags_[0].flag = flag_after_context > 0 && flag_before_context == 0;
  flags_[1].flag = flag_after_context == 0 && flag_before_context > 0;
  flags_[2].flag = flag_byte_offset;
  flags_[3].flag = flag_after_context > 0 && flag_before_context > 0;
  flags_[4].flag = flag_count;
  flags_[5].flag = flag_mode == ::Mode::FIXED;
  flags_[6].flag = flag_mode == ::Mode::BRE;
  flags_[7].flag = !globs_.empty();
  flags_[8].flag = flag_with_filename;
  flags_[9].flag = flag_no_filename;
  flags_[10].flag = flag_binary_without_match;
  flags_[11].flag = flag_ignore_case;
  flags_[12].flag = flag_smart_case;
  flags_[13].flag = flag_column_number;
  flags_[14].flag = flag_files_with_matches;
  flags_[15].flag = flag_line_number;
  flags_[16].flag = flag_only_matching;
  flags_[17].flag = flag_mode == ::Mode::PERL;
  flags_[18].flag = flag_directories_action == Action::RECURSE && flag_dereference;
  flags_[19].flag = flag_directories_action == Action::RECURSE && !flag_dereference;
  flags_[20].flag = flag_initial_tab;
  flags_[21].flag = flag_binary;
  flags_[22].flag = flag_ungroup;
  flags_[23].flag = flag_invert_match;
  flags_[24].flag = flag_with_hex;
  flags_[25].flag = flag_word_regexp;
  flags_[26].flag = flag_hex;
  flags_[27].flag = flag_line_regexp;
  flags_[28].flag = flag_empty;
  flags_[29].flag = flag_any_line;
  flags_[30].flag = flag_fuzzy > 0;
  flags_[31].flag = flag_decompress;
  flags_[32].flag = flag_null;
  flags_[33].flag = flag_max_depth == 1;
  flags_[34].flag = flag_max_depth == 2;
  flags_[35].flag = flag_max_depth == 3;
  flags_[36].flag = flag_max_depth == 4;
  flags_[37].flag = flag_max_depth == 5;
  flags_[38].flag = flag_max_depth == 6;
  flags_[39].flag = flag_max_depth == 7;
  flags_[40].flag = flag_max_depth == 8;
  flags_[41].flag = flag_max_depth == 9;
  flags_[42].flag = flag_bool;
  flags_[43].flag = false;
  flags_[44].flag = flag_hidden;
  flags_[45].flag = flag_heading;
  flags_[46].flag = flag_sort && (strcmp(flag_sort, "best") == 0 || strcmp(flag_sort, "rbest") == 0);
  flags_[47].flag = flag_sort && (strcmp(flag_sort, "size") == 0 || strcmp(flag_sort, "rsize") == 0);
  flags_[48].flag = flag_sort && (strcmp(flag_sort, "changed") == 0 || strcmp(flag_sort, "rchanged") == 0);
  flags_[49].flag = flag_sort && (strcmp(flag_sort, "created") == 0 || strcmp(flag_sort, "rcreated") == 0);
  flags_[50].flag = flag_sort && *flag_sort == 'r';
}

// set the global flags to the local flags
void Query::set_flags()
{
  // reset flags that are set by ugrep() depending on other flags
  flag_no_header = false;
  flag_dereference = false;
  flag_no_dereference = false;
  flag_files_without_match = false;
  flag_match = false;
  flag_break = false;

  // set/reset file and directory restrictions
  if (flags_[43].flag)
  {
    flag_exclude.clear();
    flag_exclude_dir.clear();
    flag_include.clear();
    flag_include_dir.clear();
    flag_file_magic.clear();
    flag_ignore_files.clear();
  }
  else
  {
    flag_exclude = exclude_;
    flag_exclude_dir = exclude_dir_;
    flag_include = include_;
    flag_include_dir = include_dir_;
    flag_file_magic = file_magic_;
    flag_ignore_files = ignore_files_;
  }

  // restore flags that may have changed by the last search
  flag_dotall = dotall_;

  // suppress warning messages
  flag_no_messages = true;

  // set ugrep flags to the interactive flags
  if (flags_[26].flag && flag_hexdump != NULL)
  {
    flag_hex_after = flag_hex_before = context_;
    flag_after_context = flag_before_context = 0;
  }
  else
  {
    if (flags_[16].flag)
    {
      flag_after_context = only_context_ * (flags_[0].flag || flags_[3].flag);
      flag_before_context = only_context_ * (flags_[1].flag || flags_[3].flag);
    }
    else
    {
      flag_after_context = context_ * (flags_[0].flag || flags_[3].flag);
      flag_before_context = context_ * (flags_[1].flag || flags_[3].flag);
    }
    if (flag_hexdump != NULL)
    {
      flag_hex_after = (flag_after_context == 0);
      flag_hex_before = (flag_before_context == 0);
    }
  }
  flag_byte_offset = flags_[2].flag;
  flag_count = flags_[4].flag;
  flag_mode = flags_[6].flag ? ::Mode::BRE : flags_[5].flag ? ::Mode::FIXED : flags_[17].flag ? ::Mode::PERL : ::Mode::ERE;
  flag_glob.clear();
  flag_iglob.clear();
  if (globbing_)
    globs_.assign(line_);
  flags_[7].flag = !globs_.empty();
  if (flags_[7].flag && !flags_[43].flag)
    flag_glob.emplace_back(globs_);
  flag_with_filename = flags_[8].flag;
  flag_no_filename = flags_[9].flag;
  flag_binary_without_match = flags_[10].flag;
  flag_ignore_case = flags_[11].flag;
  flag_smart_case = flags_[12].flag;
  flag_column_number = flags_[13].flag;
  flag_files_with_matches = flags_[14].flag;
  flag_line_number = flags_[15].flag;
  flag_only_matching = flags_[16].flag;
  if (flags_[18].flag)
    flag_directories_action = Action::RECURSE, flag_dereference = true;
  else if (flags_[19].flag)
    flag_directories_action = Action::RECURSE, flag_dereference = false;
  else
    flag_directories_action = Action::UNSP;
  flag_initial_tab = flags_[20].flag;
  flag_binary = flags_[21].flag;
  flag_ungroup = flags_[22].flag;
  flag_invert_match = flags_[23].flag;
  flag_with_hex = flags_[24].flag;
  flag_word_regexp = flags_[25].flag;
  flag_hex = flags_[26].flag;
  flag_line_regexp = flags_[27].flag;
  flag_empty = flags_[28].flag;
  flag_any_line = flags_[29].flag;
  flag_fuzzy = flags_[30].flag ? fuzzy_ : 0;
  flag_decompress = flags_[31].flag;
  flag_null = flags_[32].flag;
  flag_max_depth = 0;
  for (size_t i = 33; i <= 41; ++i)
    if (flags_[i].flag)
      flag_max_depth = i - 32;
  flag_bool = flags_[42].flag;
  flag_hidden = flags_[44].flag;
  flag_heading = flags_[45].flag;
  if (flags_[46].flag)
    flag_sort = flags_[50].flag ? "rbest" : "best";
  else if (flags_[47].flag)
    flag_sort = flags_[50].flag ? "rsize" : "size";
  else if (flags_[48].flag)
    flag_sort = flags_[50].flag ? "rchanged" : "changed";
  else if (flags_[49].flag)
    flag_sort = flags_[50].flag ? "rcreated" : "created";
  else
    flag_sort = flags_[50].flag ? "rname" : "name";
}

void Query::set_prompt()
{
  if (globbing_)
  {
    if (flag_glob_ignore_case)
      prompt_.assign("--iglob=");
    else
      prompt_.assign("--glob=");
  }
  else
  {
    prompt_.clear();

    if (!flag_file.empty())
      prompt_.append("file");

    if (flags_[42].flag)
      prompt_.append(flag_files ? "%%" : "bool");

    const char *mode;

    if (flags_[5].flag)
    {
      if (flags_[30].flag)
        mode = "FZ>";
      else
        mode = "F>";
    }
    else if (flags_[6].flag)
    {
      mode = "G>";
    }
    else if (flags_[17].flag)
    {
      mode = "P>";
    }
    else if (flags_[30].flag)
    {
      mode = "Z>";
    }
    else
    {
      mode = "Q>";
    }

    prompt_.append(mode);
  }
}

void Query::get_stdin()
{
  // if standard input is searched, then dynamically buffer the input to search
  if (flag_stdin)
  {
#ifndef OS_WIN_OR_MINGW
    // make stdin non-blocking to read and buffer dynamically
    fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);
#endif

    stdin_input_ = stdin;
    stdin_input_.file_encoding(flag_encoding_type);
    stdin_buffer_.clear();
  }
}

// start a new stdin sender thread and pipe from sender to the search engine
void Query::start_stdin()
{
  // if standard input is searched, start thread to reproduce its text on demand
  if (flag_stdin)
  {
    // create a new pipe
    if (pipe(stdin_pipe_) < 0)
    {
      what_.assign("cannot create pipe");
      // error at the end of the line, not within
      error_ = line_wsize();
      return;
    }

    // and assign it to the source
    Static::source = fdopen(stdin_pipe_[0], "rb");

    // run stdin_sender in the background to push buffered standard input down the pipe
    stdin_stop = false;
    stdin_thread_ = std::thread(Query::stdin_sender, stdin_pipe_[1]);
  }
}

// stop stdin sender thread
void Query::stop_stdin()
{
  if (flag_stdin)
  {
    // close the stdin pipe when open
    if (Static::source != stdin && Static::source != NULL)
      fclose(Static::source); // close source but don't reset to NULL, source is used by the search thread

    // join the stdin_sender
    stdin_stop = true;
    if (stdin_thread_.joinable())
      stdin_thread_.join();
  }
}

// push standard input down the specified pipe fd
void Query::stdin_sender(int fd)
{
  // write the stdin data all at once to the search pipe
  char buf[QUERY_BUFFER_SIZE];
  ssize_t len = stdin_buffer_.size(), nwritten = write(fd, stdin_buffer_.c_str(), len);

  // when more stdin input arrives, buffer it and write it to the search pipe
  while (!stdin_stop && !feof(stdin) && nwritten == len)
  {
    len = stdin_input_.file_get(buf, QUERY_BUFFER_SIZE);

    if (len == 0)
    {
      if (feof(stdin))
        break;

#ifndef OS_WIN_OR_MINGW
      struct timeval tv;
      fd_set rfds, efds;
      FD_ZERO(&rfds);
      FD_ZERO(&efds);
      FD_SET(0, &rfds);
      FD_SET(0, &efds);
      tv.tv_sec = 0;
      tv.tv_usec = 10000; // 10ms timeout
      clearerr(stdin);
      if (::select(0 + 1, &rfds, NULL, &efds, &tv) >= 0 && FD_ISSET(0, &efds) != 0)
        break;
#endif

      nwritten = 0;
    }
    else
    {
      stdin_buffer_.append(buf, len);
      nwritten = write(fd, buf, len);
    }
  }

  close(fd);
}

// true if view_[ref] has a valid filename/filepath identified by three \0 markers and differs from the given filename, then assigns filename
bool Query::find_filename(int ref, std::string& filename, bool compare_dir, bool find_path, std::string *partname)
{
  // extract the new filename to compare with and assign to the old filename
  std::string new_filename;
  size_t start;
  size_t pos;
  if (!get_filename(ref, new_filename, start, pos))
    return false;

  // if we need a pathname but we are at a marked group separator, then return
  if (find_path && (flag_after_context > 0 || flag_before_context > 0) &&
      flag_group_separator != NULL &&
      new_filename.compare(flag_group_separator) == 0)
    return false;

  // if the new filename is an archive part name, then assign it to the partname when applicable
  if (find_path && flag_decompress && new_filename.front() == '{' && new_filename.back() == '}')
  {
    if (partname == NULL)
      return false;

    // swap partname for filename sans braces
    new_filename.erase(0, 1);
    new_filename.pop_back();
    partname->swap(new_filename);

    // try to find the filename in a header above
    while (--ref >= 0)
      if (get_filename(ref, filename, start, pos) && filename.front() != '{')
        break;

    if (ref < 0)
      filename.clear();

    return true;
  }

  // extract the archive partname, if any
  if (partname != NULL)
  {
    // -z: get the archive partname when present
    if (flag_decompress)
    {
      // skip ANSI escape codes when present
      start = ++pos;
      const std::string& line = view_[ref];
      size_t end = line.size();
      while (pos < end && (pos = skip_escapes(line, pos)) > start)
        start = pos;

      // extract the part from {part} or {path:part}
      if (pos < end && line.at(pos) == '{')
      {
        start = ++pos;

        while (pos < end && line.at(pos) != '}')
          ++pos;

        if (start < pos && pos < end)
          partname->assign(line.substr(start, pos - start));
      }
    }
  }

  // --tree: the new filename is the basename, we should reconstruct the new pathname from the tree in view_[]
  if (flag_tree && (flag_files_with_matches || flag_count))
  {
    size_t last_start = start;
    std::string dir;

    // while the top directory was not found (works since no colors are used for directory names)
    while (start > 2 && --ref >= 0)
    {
      if (view_[ref].size() <= 1)
        break;

      if (!get_filename(ref, dir, start, pos))
        continue;

      // if line scanned represents a parent directory, then prepend the directory to the new filename
      if (start < last_start && dir.back() == PATHSEPCHR)
      {
        new_filename.insert(0, dir);
        last_start = start;
      }
    }
  }

  if (compare_dir)
  {
    // the new filename must not be in the same directory as the old filename
    size_t skip = 0;
#ifdef OS_WIN
    if (filename.size() >= 3 && filename.at(1) == ':' && filename.at(2) == PATHSEPCHR)
      skip = 3;
#endif
    size_t pos1 = new_filename.find(PATHSEPCHR, skip);
    size_t pos2 = filename.find(PATHSEPCHR, skip);

    if (pos1 == pos2 && (pos1 == std::string::npos || new_filename.compare(0, pos1, filename, 0, pos2) == 0))
      return false; // the extracted filename is the same or is in the same directory as the old filename
  }
  else if ((flag_after_context > 0 || flag_before_context > 0) &&
      flag_group_separator != NULL &&
      new_filename.compare(flag_group_separator) == 0)
  {
    // -ABC: jump to the group-separator (default is '--') unless we find_path only then ignore group separators
    return true;
  }
  else if (new_filename.compare(filename) == 0)
  {
    // the new filename is the same as the old filename
    return false;
  }

  filename.swap(new_filename);

  return true;
}

// get filename from the output, return position at last \0 marker or -1 when not found
bool Query::get_filename(int ref, std::string& filename, size_t& start, size_t& pos)
{
  const std::string& line = view_[ref];
  size_t end = line.size();

  if (end < 4 || line.front() != '\0')
    return false;

  pos = 1;

  while (pos < end && line.at(pos) != '\0')
    ++pos;

  if (++pos >= end)
    return false;

  start = pos;

  while (pos < end && line.at(pos) != '\0')
    ++pos;

  if (pos == start || pos >= end)
    return false;

  filename = line.substr(start, pos - start);

  return true;
}

// get line number from the output, assumes option -n is active
size_t Query::get_line_number()
{
  int row = select_ >= 0 ? select_ : row_;

  // look at the current line and the one below it
  for (int i = row; i < rows_ && i <= row + 1; ++i)
  {
    const std::string& line = view_[i];
    size_t pos = 0;
    size_t end = line.size();

    // skip over pathname or group separator when present
    if (line.front() == '\0')
    {
      while (++pos < end && line.at(pos) != '\0')
        continue;

      while (++pos < end && line.at(pos) != '\0')
        continue;

      ++pos;
    }

    while (true)
    {
      // skip over ANSI escape sequence when present
      pos = skip_escapes(line, pos);

      // skip archive part
      if (pos < end && line.at(pos) == '{')
        while (pos < end && line.at(pos) != '}')
          ++pos;

      // at end?
      if (pos >= end)
        break;

      // found the line number?
      if (isdigit(static_cast<unsigned char>(line.at(pos))))
        return static_cast<size_t>(strtoull(line.c_str() + pos, NULL, 10));

      ++pos;
    }
  }

  return 0;
}

Query::Mode                Query::mode_                = Query::Mode::QUERY;
bool                       Query::updated_             = false;
bool                       Query::message_             = false;
Query::Line                Query::line_                = { '\0' };
Query::Line                Query::temp_                = { '\0' };
std::string                Query::prompt_;
int                        Query::start_               = 0;
int                        Query::col_                 = 0;
int                        Query::len_                 = 0;
int                        Query::offset_              = 0;
int                        Query::shift_               = 8;
std::atomic_int            Query::error_;
std::string                Query::what_;
int                        Query::row_                 = 0;
int                        Query::rows_                = 0;
int                        Query::maxrows_;
Query::State               Query::mark_;
int                        Query::select_              = -1;
bool                       Query::select_all_          = false;
bool                       Query::globbing_            = false;
std::string                Query::globs_;
std::string                Query::dirs_;
std::string                Query::wdir_;
std::string                Query::prevfile_;
std::string                Query::prevpart_;
size_t                     Query::prevfrom_;
size_t                     Query::prevline_;
std::vector<std::string>   Query::preview_;
size_t                     Query::prevnum_;
bool                       Query::deselect_file_;
std::string                Query::selected_file_;
std::stack<Query::History> Query::history_;
std::vector<const char*>   Query::files_;
int                        Query::skip_                = 0;
std::vector<std::string>   Query::view_;
std::vector<bool>          Query::selected_;
bool                       Query::eof_                 = true;
bool                       Query::append_              = false;
size_t                     Query::buflen_              = 0;
char                       Query::buffer_[QUERY_BUFFER_SIZE];
int                        Query::search_pipe_[2];
std::thread                Query::search_thread_;
reflex::Input              Query::stdin_input_;
std::string                Query::stdin_buffer_;
int                        Query::stdin_pipe_[2];
std::thread                Query::stdin_thread_;
std::atomic_bool           Query::stdin_stop;
size_t                     Query::searched_            = 0;
size_t                     Query::found_               = 0;
int                        Query::tick_                = 0;
int                        Query::spin_                = 0;
std::vector<std::string>   Query::exclude_;
std::vector<std::string>   Query::exclude_dir_;
std::vector<std::string>   Query::include_;
std::vector<std::string>   Query::include_dir_;
std::vector<std::string>   Query::file_magic_;
std::set<std::string>      Query::ignore_files_;
size_t                     Query::context_             = 2;
size_t                     Query::only_context_        = 20;
size_t                     Query::fuzzy_               = 1;
bool                       Query::dotall_              = false;

#ifdef OS_WIN_OR_MINGW

HANDLE                     Query::hPipe_;
OVERLAPPED                 Query::overlapped_;
bool                       Query::blocking_;
bool                       Query::pending_;

#endif

Query::Flags Query::flags_[] = {
  { false, 'A', "after context" },
  { false, 'B', "before context" },
  { false, 'b', "byte offset" },
  { false, 'C', "context" },
  { false, 'c', "count lines" },
  { false, 'F', "fixed strings" },
  { false, 'G', "basic regex" },
  { false, 'g', "apply globs" },
  { false, 'H', "with filename" },
  { false, 'h', "hide filename" },
  { false, 'I', "ignore binary" },
  { false, 'i', "ignore case" },
  { false, 'j', "smart case" },
  { false, 'k', "column number" },
  { false, 'l', "list files" },
  { false, 'n', "line number" },
  { false, 'o', "only matching" },
  { false, 'P', "perl regex" },
  { false, 'R', "recurse symlinks" },
  { false, 'r', "recurse" },
  { false, 'T', "initial tab" },
  { false, 'U', "binary pattern" },
  { false, 'u', "ungroup matches" },
  { false, 'v', "invert matches" },
  { false, 'W', "with hex binary" },
  { false, 'w', "word match" },
  { false, 'X', "hex binary" },
  { false, 'x', "line match" },
  { false, 'Y', "empty matches" },
  { false, 'y', "any line" },
  { false, 'Z', "fuzzy matching" },
  { false, 'z', "decompress" },
  { false, '0', "file name + \\0" },
  { false, '1', "recurse 1 level" },
  { false, '2', "recurse 2 levels" },
  { false, '3', "recurse 3 levels" },
  { false, '4', "recurse 4 levels" },
  { false, '5', "recurse 5 levels" },
  { false, '6', "recurse 6 levels" },
  { false, '7', "recurse 7 levels" },
  { false, '8', "recurse 8 levels" },
  { false, '9', "recurse 9 levels" },
  { false, '%', "Boolean queries" },
  { false, '@', "all not hidden" },
  { false, '.', "include hidden" },
  { false, '+', "show heading" },
  { false, '~', "sort by best" },
  { false, '#', "sort by size" },
  { false, '$', "sort by changed" },
  { false, '!', "sort by created" },
  { false, '^', "reverse sort" },
  { false, '[', "decrease context" },
  { false, ']', "increase context" },
  { false, '{', "decrease fuzziness" },
  { false, '}', "increase fuzziness" },
  { false, 0, NULL, }
};