TerminalStream.java

/*
** Module   : TerminalStream.java
** Abstract :
**
** Copyright (c) 2008-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------Description----------------------------
** 001 SIY 20080314   @37452 Created initial version
**                           Ref: #37453
** 002 GES 20080327   @37699 Removed direct use of the native interfaces
**                           in toolkit. This makes the result more
**                           abstract and safer for I18N purposes. But
**                           it should be noted that this case should
**                           not have any direct use of the CHARVA code
**                           at all because that is a dependency that is
**                           inappropriate for the util package and the
**                           streams implementation.
** 003 NVS 20090216   @41321 Terminal stream shouldn't use the interactive
**                           output driver. Now using the direct output
**                           driver instead.
** 004 SIY 20100619          Switching to own implementation of character UI.
** 005 SIY 20100804          Changes in imports.
** 006 SVL 20110403          Added write(byte[]) function.
** 007 SIY 20110112          Changes related to new color handling scheme.
** 008 CA  20140730          Protected stream close/cleanup code from raised conditions, to allow
**                           close/cleanup fully complete even if conditions were raised.
** 009 CA  20140805          ChUI-specific classes are explicitly imported, to isolate their usage.
**                           For now, this class works only with a ChUI driver, but this needs to
**                           be improved, to allow GUI driver support.
** 010 HC  20140911          GUI coordinate support - data types refactoring.
** 011 HC  20151013          Changes to extend GUI multi-window focus management and ACTIVE-
**                           WINDOW system handle processing to modal windows.
** 012 OM  20170622          Implemented peekCh() method.
** 013 ECF 20171026          Added write(byte[], int, int) method.
** 014 EVL 20220325          Base refactoring for Stream based classes getting single byte and array of bytes.
** 015 GBB 20240912          Replacing OutputManager dependency with the new interface Terminal.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.io.*;
import java.util.*;

import com.goldencode.p2j.ui.*;
import com.goldencode.p2j.ui.chui.*;
import com.goldencode.p2j.ui.client.*;
import com.goldencode.p2j.ui.client.chui.driver.ChuiOutputManager;


/**
 * This class provides support for stream redirected to the interactive
 * terminal. This stream behaves as something in the middle between stream and
 * interactive terminal. It displays output as regular stream (for example
 * like {@link FileStream}), but processes pause as interactive terminal.
 * <p>
 * Terminal stream handles paging differently than regular stream. In
 * particular, it has different default page size dictated by terminal height
 * and each page triggers a pause which requires user action (pressing a key).
 * 
 * @author  SIY
 */
public class TerminalStream
extends Stream
{
   /** Track start of the output is started. */
   private boolean outputStarted = false;
   
   /** Reference to toolkit. */
   private ChuiOutputManager tk;
   
   /** Reference to interactive output worker. */
   private BasePrimitives worker;
   
   /** Screen buffer. */
   private char[] buffer;
   
   /** Number of screen rows. */
   private int rows;

   /** Number of screen columns. */
   private int cols;
   
   /** Start of the buffer. */
   private int start = 0;
   
   /** Cursor position. */
   private int cursor = 0;
   
   /**
    * Constructor.
    * 
    * @param    terminal
    *           Provides access to terminal features and helpers.
    */
   public TerminalStream(Terminal terminal)
   {
      tk = (ChuiOutputManager) terminal.getOutputManager();
      worker = tk.getDirectOutput();
      
      cols = worker.screenWidth();
      rows = worker.screenHeight();
      buffer = new char[cols * rows];
      double height = WindowManager.resolveWindow().getScreenDimension().height;
      int nheight = tk.coordinates().heightToNative(height);
      cursor = nheight * cols;
      Arrays.fill(buffer, ' ');
   }
   
   /**
    * Sets the current page size in lines.
    *
    * @param    sz
    *           The number of lines per page.
    */
   @Override
   void rawSetPageSize(int sz)
   {
      // use terminal height as a default value
      if (sz == SET_PAGED_DEFAULT)
      {
         double h = WindowManager.resolveWindow().getScreenDimension().height; 
         sz = tk.coordinates().heightToNative(h) - 2;
      }

      super.rawSetPageSize(sz);
   }
   
   /**
    * Assigns the internal stream reference to the given reference, in 
    * actuality this has no effect.
    *
    * @param    stream
    *           The new internal stream reference to use for all operations,
    *           ignored.
    */
   @Override
   public void assign(Stream stream)
   throws UnsupportedOperationException
   {
      // nothing to do
   }

   /**
    * The number of bytes available to be immediately read without blocking.
    *
    * @return   always 0.
    */
   @Override
   public long available()
   throws IOException
   {
      return 0;
   }

   /**
    * Closes the stream and releases OS resources associated with it, in
    * actuality this has no effect.
    */
   @Override
   public void close()
   {
      // nothing to do
   }

   /**
    * Closes the input stream and releases OS resources associated with it,
    * in actuality this has no effect.
    */
   @Override
   public void closeIn()
   {
      // nothing to do
   }

   /**
    * Closes the output stream and clears the interactive terminal screen.
    */
   @Override
   public void closeOut()
   {
      if (outputStarted)
      {
         ThinClient cli = ThinClient.getInstance(); 
         int saveId = cli.switchOutput(-1, false);
         cli.refreshScreen();
         cli.switchOutput(saveId, false);
      }
   }

   /**
    * The length of the stream in bytes.
    *
    * @return   always 0.
    */
   @Override
   public long getLen()
   throws UnsupportedOperationException,
          IOException
   {
      return 0;
   }

   /**
    * The 0-based offset into the stream at which the next read or write will 
    * occur.  
    *
    * @return   always 0.
    */
   @Override
   public long getPos()
   throws UnsupportedOperationException,
          IOException
   {
      return 0;
   }

   /**
    * State of the input side of the stream.
    *
    * @return   always <code>false</code>
    */
   @Override
   public boolean isIn()
   {
      return false;
   }

   /**
    * State of the output side of the stream.
    *
    * @return   always <code>false</code>
    */
   @Override
   public boolean isOut()
   {
      return true;
   }
   
   /**
    * Peeks at the character from the current read position in the stream.
    *
    * @return   always -2 (simulates an {@code EOF}).
    */
   @Override
   public int peekCh()
   {
      return -2;
   }
   
   /**
    * Read a character from the current read position in the stream.
    *
    * @return   always -2 (simulates an <code>EOF</code>).
    */
   @Override
   public int readCh()
   {
      return -2;
   }

   /**
    * Read a single byte from the underlying stream.
    * 
    * @return   A single byte from the stream, -1 on any failure and -2 upon an {@code EOF}.
    */
   @Override
   public int readByte()
   {
      return -2;
   }
   
   /**
    * Read all characters from the current read position in the stream to the
    * next line separator (as determined by the <code>File.separator</code>
    * or to the <code>EOF</code>. Any line separator character(s) and the
    * <code>EOF</code> character are not returned.  In actuality this has no
    * effect.
    *
    * @return   always throws an <code>EOFException</code>.
    */
   @Override
   public String readLn()
   throws EOFException,
          IOException,
          InterruptedException
   {
      throw new EOFException("There is no stream.");
   }

   /**
    * Truncates or extends the stream to the specified length if this stream
    * supports such an operation, in actuality this has no effect.
    *
    * @param    len
    *           Ignored.
    */
   @Override
   public void setLen(long len)
   throws UnsupportedOperationException,
          IOException
   {
      // nothing to do
   }

   /**
    * Moves the current read/write position to the specified absolute 0-based
    * offset, in actuality this has no effect. 
    *
    * @param    pos
    *           The new read/write position in the stream.
    */
   @Override
   public void setPos(long pos)
   throws UnsupportedOperationException,
          IOException
   {
      // nothing to do
   }

   /**
    * Write the given string to the output stream.
    *
    * @param    data
    *           The data to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   @Override
   public void write(String data)
   throws IOException
   {
      if (data == null)
      {
         return;
      }
      
      syncTeletypeString(data);
   }
   
   /**
    * Write the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data)
   throws IOException
   {
      if (data == null)
      {
         return;
      }
      
      for (byte b : data)
      {
         syncTeletype((char) b);
      }
   }
   
   /**
    * Write the specified range of bytes from the given byte array to the output stream.
    *
    * @param    data
    *           The data to be written.
    * @param    off
    *           Starting offset in data from which to read bytes to be written. Must be
    *           non-negative and {@code &lt; data.length}.
    * @param    len
    *           Length of data to be written. Must be non-negative and {@code &lt;= (data.length
    *           - offset)}.
    *
    * @throws   IOException
    *           If an I/O error occurs.
    */
   public void write(byte[] data, int off, int len)
   throws IOException
   {
      if (data == null)
      {
         return;
      }
      
      for (int i = off; i < len; i++)
      {
         syncTeletype((char) data[i]);
      }
   }
   
   /**
    * Write the given byte to the output stream.
    *
    * @param    b
    *           The byte to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   @Override
   public void writeByte(byte b)
   throws IOException
   {
      syncTeletype((char) b);
   }

   /**
    * Write the given character to the output stream.
    *
    * @param    ch
    *           The character to be written.
    * 
    * @throws   IOException
    *           If an I/O error occurs. 
    */
   @Override
   public void writeCh(char ch)
   throws IOException
   {
      syncTeletype(ch);
   }

   /**
    * Simulate TTY output.
    * 
    * @param   ch
    *          Character to write to the stream.
    */
   private void teletype(char ch)
   {
      // form feed
      if (ch == '\f')
      {
         insertFormFeed();
         ThinClient.getInstance().pauseAfterPage();
         return;
      }
      else if (ch == '\n')
      {
         int delta = cols - (cursor % cols);

         Arrays.fill(buffer, cursor, cursor + delta, ' ');
         
         cursor += delta;
      }
      else
         buffer[cursor++] = ch;
      
      if (cursor >= buffer.length || cursor == start)
      {
         cursor = start;
         start += cols;
         
         if (start >= buffer.length)
            start = 0;
         
         Arrays.fill(buffer, cursor, cursor + cols, ' ');
      }
   }

   /**
    * Insert 4 empty lines.
    */
   private void insertFormFeed()
   {
      syncTeletypeString("\n\n\n\n");
   }

   /**
    * Simulate TTY output of single character and synchronize terminal when
    * processing is finished.
    * 
    * @param   ch
    *          Character to write to the stream.
    */
   private void syncTeletype(char ch)
   {
      Syncronizer sync = new Syncronizer();
      
      teletype(ch);

      sync.refresh();
   }

   /**
    * Simulate TTY output of string and synchronize terminal when processing
    * is finished.
    * 
    * @param   data
    *          The characters to write to the stream.
    */
   private void syncTeletypeString(String data)
   {
      Syncronizer sync = new Syncronizer();
      
      try
      {
         for (char c : data.toCharArray())
            teletype(c);
      }
      finally
      {
         sync.refresh();
      }
   }
   
   /**
    * Refresh content of entire screen.
    */
   private void refreshScreen()
   {
      for(int i = 0; i < rows; i++)
         refreshRow(i);
   }

   /**
    * Refresh content of specified row.
    * 
    * @param   row
    *          Row number to refresh.
    */
   private void refreshRow(int row)
   {
      if (row < 0 || row >= rows)
         return;
      
      int rowStart = (start + row * cols) % buffer.length;
      String str = new String(buffer, rowStart, cols);
      worker.cursorAt(0, row);
      worker.append(str, Color.DEFAULT);
   }

   /**
    * This class handles saving buffer state and refreshing terminal as
    * necessary.
    */
   class Syncronizer
   {
      /** Saved buffer start pointer. */
      private int saveStart;
      /** Saved cursor position. */
      private int saveCursor;

      /**
       * Construct an instance and save buffer state variables.
       */
      Syncronizer()
      {
         saveStart  = start;
         saveCursor = cursor;
         
         if (!outputStarted)
         {
            outputStarted = true;
            
            tk.clearScreen();
         }
      }
      
      /**
       * Refresh terminal as necessary.
       */
      void refresh()
      {
         if (saveStart != start)
            refreshScreen();
         else
            refreshRow((saveCursor - start)/cols);
         
         worker.sync();
      }
   }
}