Livt packages

Eccelerators.Cli

Version 1.4.1

Transport-independent, bounded command-line input, parsing, editing, and output for Livt FPGA applications.

Published files (23)

src/CompactCli.lvt

namespace Eccelerators.Cli

using Livt.IO

/**
 * Resource-oriented free-text CLI for constrained FPGA applications.
 *
 * The component owns one 64-byte distributed line buffer and a one-byte
 * backpressured output slot. It recognizes terminal line endings and editing,
 * retains complete free-text lines, and supports exact command matching. It
 * intentionally omits the full CLI's argument table and output FIFO.
 */
component CompactCli
{
	// Storage geometry and command matching limit.
	public const MAX_LINE_LENGTH: int = 64
	public const MAX_COMMAND_LENGTH: int = 16

	// Terminal control characters and echo continuation codes.
	const CARRIAGE_RETURN: byte = 0x0d
	const LINE_FEED: byte = 0x0a
	const BACKSPACE: byte = 0x08
	const DELETE: byte = 0x7f
	const ECHO_NONE: byte = 0x00
	const ECHO_LINE_FEED: byte = 0x01
	const ECHO_ERASE_SPACE: byte = 0x02
	const ECHO_ERASE_BACK: byte = 0x03

	// Live storage boundary; scheduled methods own the command registers.
	line: AsynchronousDistributedRam8x64
	lineAddress: logic[6]
	lineWriteEnable: bool
	lineWriteData: logic[8]
	lineReadData: logic[8]

	// Valid-prefix metadata and the independent backpressured output slot.
	lineLength: byte
	lineReady: bool
	overflow: bool
	ignoreNextLineFeed: bool
	outputValid: bool
	outputByte: byte
	echoEnabled: bool
	echoContinuation: byte

	/**
	 * Creates an empty line buffer without initializing its unread payload cells.
	 */
	new()
	{
		this.line = new AsynchronousDistributedRam8x64()
		this.echoEnabled = false
	}

	/**
	 * Wires the asynchronous storage without implicit registered observations.
	 */
	process MemoryPort[]()
	{
		this.line.address = lineAddress
		this.line.writeEnable = lineWriteEnable
		this.line.writeData = lineWriteData
		lineReadData = this.line.readData
	}

	/**
	 * Returns true when another transport byte can be consumed.
	 */
	public fn CanAcceptByte() bool
	{
		if (this.lineReady) { return false }
		return !this.echoEnabled || !this.outputValid
	}

	/**
	 * Applies line endings, deletion, printable filtering, and length bounds.
	 */
	public fn AcceptByte(value: byte) bool
	{
		if (this.ignoreNextLineFeed) {
			this.ignoreNextLineFeed = false
			if (value == LINE_FEED) { return true }
		}

		// Keep this check local. Calling the public CanAcceptByte dispatcher from
		// another method on the same component is not lowered as a direct call.
		if (this.lineReady) { return false }
		if (this.echoEnabled && this.outputValid) { return false }

		if (value == CARRIAGE_RETURN) {
			state {
				this.ignoreNextLineFeed = true
				if (this.lineLength != 0 || this.overflow) { this.lineReady = true }
				if (this.echoEnabled) {
					this.outputByte = CARRIAGE_RETURN
					this.outputValid = true
					this.echoContinuation = ECHO_LINE_FEED
				}
			}
			return true
		}

		if (value == LINE_FEED) {
			state {
				if (this.lineLength != 0 || this.overflow) { this.lineReady = true }
				if (this.echoEnabled) {
					this.outputByte = CARRIAGE_RETURN
					this.outputValid = true
					this.echoContinuation = ECHO_LINE_FEED
				}
			}
			return true
		}

		if (value == BACKSPACE || value == DELETE) {
			if (this.lineLength != 0) {
				state {
					this.lineLength = ((this.lineLength as int) - 1) as byte
					if (this.echoEnabled) {
						this.outputByte = BACKSPACE
						this.outputValid = true
						this.echoContinuation = ECHO_ERASE_SPACE
					}
				}
				state {}
			}
			return true
		}

		if (value < 0x20 || value > 0x7e) { return true }

		if ((this.lineLength as int) >= MAX_LINE_LENGTH) {
			state { this.overflow = true }
			return true
		}

		var address = this.lineLength as int
		state {
			lineAddress = address as logic[6]
			lineWriteData = value as logic[8]
			lineWriteEnable = true
			if (this.echoEnabled) {
				this.outputByte = value
				this.outputValid = true
				this.echoContinuation = ECHO_NONE
			}
		}
		state { lineWriteEnable = false }
		state { this.lineLength = (address + 1) as byte }
		state {}
		return true
	}

	/**
	 * Enables terminal echo for printable input, line endings, and editing.
	 */
	public fn EnableEcho() { this.echoEnabled = true }

	/**
	 * Disables echo without discarding bytes already accepted for output.
	 */
	public fn DisableEcho() { this.echoEnabled = false }

	/**
	 * Returns true while a complete line awaits application processing.
	 */
	public fn HasLine() bool { return this.lineReady }

	/**
	 * Returns whether printable input exceeded the retained line capacity.
	 */
	public fn HasOverflow() bool { return this.overflow }

	/**
	 * Returns the number of retained printable bytes.
	 */
	public fn GetLineLength() int { return this.lineLength as int }

	/**
	 * Returns one retained byte, or zero for an invalid index.
	 */
	public fn GetLineByte(index: int) byte
	{
		if (index < 0 || index >= (this.lineLength as int)) { return 0x00 }
		state { lineAddress = index as logic[6] }
		state {}
		state {}
		return lineReadData as byte
	}

	/**
	 * Returns true when the complete line exactly matches a fixed command.
	 *
	 * `expected` is a left-aligned fixed-width byte pattern and
	 * `expectedLength` is its logical length. The fixed shape avoids generated
	 * call records resizing differently sized dynamic command literals.
	 */
	public fn CommandEquals(expected: byte[16], expectedLength: int) bool
	{
		if (expectedLength < 0 || expectedLength > MAX_COMMAND_LENGTH) { return false }
		if ((this.lineLength as int) != expectedLength) { return false }
		for (var index = 0; index < expectedLength; index++) {
			state { lineAddress = index as logic[6] }
			state {}
			state {}
			state {}
			state {}
			if ((lineReadData as byte) != expected[index]) { return false }
		}
		return true
	}

	/**
	 * Releases the current line and preserves pending CRLF suppression.
	 */
	public fn CompleteLine() bool
	{
		if (!this.lineReady) { return false }
		state {
			this.lineLength = 0
			this.lineReady = false
			this.overflow = false
		}
		state {}
		state {}
		return true
	}

	/**
	 * Clears input, line-ending, and output state.
	 */
	public fn Reset()
	{
		state {
			this.lineLength = 0
			this.lineReady = false
			this.overflow = false
			this.ignoreNextLineFeed = false
			this.outputValid = false
			this.outputByte = 0x00
			this.echoContinuation = ECHO_NONE
		}
		state {}
		state {}
	}

	/**
	 * Queues one output byte, or applies backpressure when the slot is full.
	 */
	public fn TryWriteByte(value: byte) bool
	{
		if (this.outputValid) { return false }
		state {
			this.outputByte = value
			this.outputValid = true
		}
		state {}
		state {}
		return true
	}

	/**
	 * Returns true when one output byte awaits the transport.
	 */
	public fn HasOutput() bool { return this.outputValid }

	/**
	 * Returns the pending output byte without consuming it.
	 */
	public fn PeekOutput() byte { return this.outputByte }

	/**
	 * Acknowledges the pending output byte.
	 */
	public fn ConsumeOutput() bool
	{
		if (!this.outputValid) { return false }
		state {
			if (this.echoContinuation == ECHO_LINE_FEED) {
				this.outputByte = LINE_FEED
				this.echoContinuation = ECHO_NONE
			} elif (this.echoContinuation == ECHO_ERASE_SPACE) {
				this.outputByte = 0x20
				this.echoContinuation = ECHO_ERASE_BACK
			} elif (this.echoContinuation == ECHO_ERASE_BACK) {
				this.outputByte = BACKSPACE
				this.echoContinuation = ECHO_NONE
			} else {
				this.outputValid = false
			}
		}
		state {}
		state {}
		return true
	}
}