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 < data.length}.
* @param len
* Length of data to be written. Must be non-negative and {@code <= (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();
}
}
}