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/Cli.lvt

namespace Eccelerators.Cli

using Livt.IO

/**
 * Transport-independent, bounded command-line interface for FPGA applications.
 * Owns independent 64-byte line/output stores; each backend shares this context.
 * Defaults to block RAM. One caller serializes transactions; payload initialization
 * and reset values are not observable outside the valid line/queue prefix.
 */
component Cli<LINE_STORAGE: IRam<byte> & new() = Ram<byte, 64, MemoryStyle.Block>,
	OUTPUT_STORAGE: IRam<byte> & new() = Ram<byte, 64, MemoryStyle.Block>>
{
	const MINIMUM_INPUT_OUTPUT_SPACE: int = 32

	output: CliOutput<OUTPUT_STORAGE>

	editor: CliLineEditor

	parser: CliParser<LINE_STORAGE>

	echoEnabled: bool

	freeTextEnabled: bool

	commandReady: bool

	promptPending: bool

	/**
	 * Creates a CLI with optional local terminal echo.
	 */
	new(echoEnabled: bool)
	{
		this.output = new CliOutput<OUTPUT_STORAGE>()
		this.editor = new CliLineEditor()
		this.parser = new CliParser<LINE_STORAGE>()
		this.echoEnabled = echoEnabled
		this.freeTextEnabled = false
		this.commandReady = false
		this.promptPending = true
	}

	/**
	 * Queues a pending prompt atomically. Returns false when no prompt is pending
	 * or the output queue lacks capacity.
	 */
	public fn Service() bool
	{
		if (this.promptPending == true && this.output.CanWrite(2) == true) {
			var prompt = "> ".Encode()

			if (this.output.TryWrite(prompt) == true) {
				this.promptPending = false
				return true
			}
		}

		return false
	}

	/**
	 * Returns true when one transport byte can be accepted without losing output.
	 */
	public fn CanAcceptByte() bool
	{
		if (this.commandReady == true || this.editor.IsComplete() == true) { return false }

		return this.output.GetFreeSpace() >= MINIMUM_INPUT_OUTPUT_SPACE
	}

	/**
	 * Applies line editing and optional echo to one transport byte. Returns false
	 * without consuming the byte when input is backpressured.
	 */
	public fn AcceptByte(value: byte) bool
	{
		if (this.CanAcceptByte() == false) { return false }

		var oldLength: int = this.editor.GetLength()

		if (this.editor.AcceptByte(value) == false) { return false }

		var newLength: int = this.editor.GetLength()

		if (newLength > oldLength) {
			var appended: bool = this.parser.AppendByte(value)

			if (appended == false) { return false }
		} elif (newLength < oldLength) {
			var removed: bool = this.parser.RemoveLastByte()

			if (removed == false) { return false }
		}

		if (this.echoEnabled == true) {
			if (this.editor.IsComplete() == true) {
				var empty = "".Encode()
				var lineEndingWritten: bool = this.output.TryWriteLine(empty)

				if (lineEndingWritten == false) { return false }
			} elif (newLength < oldLength) {
				var eraseBack: bool = this.output.TryWriteByte(0x08)

				if (eraseBack == false) { return false }

				var eraseSpace: bool = this.output.TryWriteByte(0x20)

				if (eraseSpace == false) { return false }

				var eraseAgain: bool = this.output.TryWriteByte(0x08)

				if (eraseAgain == false) { return false }
			} elif (newLength > oldLength) {
				var echoed: bool = this.output.TryWriteByte(value)

				if (echoed == false) { return false }
			}
		}

		if (this.editor.IsComplete() == true) {
			if (this.editor.HasOverflow() == true) {
				var overflowMessage = "Error: line too long".Encode()
				var overflowWritten: bool = this.output.TryWriteLine(overflowMessage)

				if (overflowWritten == false) { return false }

				this.editor.ResetLine()
				this.parser.Clear()
				this.promptPending = true
				return true
			}

			this.parser.Parse()

			if (this.parser.HasTooManyArguments() == true && this.freeTextEnabled == false) {
				var argumentsMessage = "Error: too many arguments".Encode()
				var argumentsWritten: bool = this.output.TryWriteLine(argumentsMessage)

				if (argumentsWritten == false) { return false }

				this.editor.ResetLine()
				this.parser.Clear()
				this.promptPending = true
			} elif (this.parser.GetCommandLength() == 0) {
				this.editor.ResetLine()
				this.parser.Clear()
				this.promptPending = true
			} else {
				this.commandReady = true
			}
		}

		return true
	}

	/**
	 * Allows completed lines with more than eight arguments to reach the
	 * application. The complete line remains available through GetLineByte.
	 */
	public fn EnableFreeText()
	{
		this.freeTextEnabled = true
	}

	/**
	 * Restores strict command parsing and excessive-argument rejection.
	 */
	public fn DisableFreeText()
	{
		this.freeTextEnabled = false
	}

	/**
	 * Returns the complete retained line length.
	 */
	public fn GetLineLength() int
	{
		return this.parser.GetLineLength()
	}

	/**
	 * Returns one byte from the complete retained line, or zero outside it.
	 */
	public fn GetLineByte(index: int) byte
	{
		return this.parser.GetLineByte(index)
	}

	/**
	 * Returns true while a parsed command awaits application dispatch.
	 */
	public fn HasCommand() bool
	{
		return this.commandReady
	}

	/**
	 * Completes the pending command and schedules the next prompt. Returns false
	 * when no command is pending.
	 */
	public fn CompleteCommand() bool
	{
		if (this.commandReady == false) { return false }

		this.commandReady = false
		this.editor.ResetLine()
		this.parser.Clear()
		this.promptPending = true
		return true
	}

	/**
	 * Restores an empty CLI and schedules a fresh prompt.
	 */
	public fn Reset()
	{
		this.output.Clear()
		this.editor.Clear()
		this.parser.Clear()
		this.commandReady = false
		this.promptPending = true
	}

	/**
	 * Returns true when the current command exactly matches expected.
	 */
	public fn CommandEquals(expected: byte[]) bool
	{
		return this.parser.CommandEquals(expected)
	}

	/**
	 * Returns the current command length.
	 */
	public fn GetCommandLength() int
	{
		return this.parser.GetCommandLength()
	}

	/**
	 * Returns the current argument count.
	 */
	public fn GetArgumentCount() int
	{
		return this.parser.GetArgumentCount()
	}

	/**
	 * Returns one argument length, or zero when index is invalid.
	 */
	public fn GetArgumentLength(index: int) int
	{
		return this.parser.GetArgumentLength(index)
	}

	/**
	 * Returns one argument byte, or zero when index or offset is invalid.
	 */
	public fn GetArgumentByte(index: int, offset: int) byte
	{
		return this.parser.GetArgumentByte(index, offset)
	}

	/**
	 * Returns true when one argument exactly matches expected.
	 */
	public fn ArgumentEquals(index: int, expected: byte[]) bool
	{
		return this.parser.ArgumentEquals(index, expected)
	}

	/**
	 * Returns true when length is nonnegative and that many output bytes are free.
	 */
	public fn CanWrite(length: int) bool
	{
		return this.output.CanWrite(length)
	}

	/**
	 * Queues one response byte, or returns false without changing the queue.
	 */
	public fn TryWriteByte(value: byte) bool
	{
		return this.output.TryWriteByte(value)
	}

	/**
	 * Queues a complete response atomically, or returns false without changing
	 * the queue.
	 */
	public fn TryWrite(data: byte[]) bool
	{
		return this.output.TryWrite(data)
	}

	/**
	 * Queues a complete response and CRLF atomically, or returns false without
	 * changing the queue.
	 */
	public fn TryWriteLine(data: byte[]) bool
	{
		return this.output.TryWriteLine(data)
	}

	/**
	 * Queues all parsed arguments separated by spaces and followed by CRLF.
	 * The complete line is written atomically.
	 */
	public fn TryWriteArgumentsLine() bool
	{
		var argumentCount: int = this.parser.GetArgumentCount()
		var required: int = 2

		for (var i = 0; i < argumentCount; i++) {
			required = required + this.parser.GetArgumentLength(i)

			if (i > 0) { required = required + 1 }
		}

		if (this.output.CanWrite(required) == false) { return false }

		for (var i = 0; i < argumentCount; i++) {
			if (i > 0) {
				var spaceWritten: bool = this.output.TryWriteByte(0x20)

				if (spaceWritten == false) { return false }
			}

			var length: int = this.parser.GetArgumentLength(i)

			for (var offset = 0; offset < length; offset++) {
				var value: byte = this.parser.GetArgumentByte(i, offset)
				var valueWritten: bool = this.output.TryWriteByte(value)

				if (valueWritten == false) { return false }
			}
		}

		var carriageReturnWritten: bool = this.output.TryWriteByte(0x0D)

		if (carriageReturnWritten == false) { return false }

		return this.output.TryWriteByte(0x0A)
	}

	/**
	 * Returns true when output is ready for the transport.
	 */
	public fn HasOutput() bool
	{
		return this.output.HasData()
	}

	/**
	 * Returns the next transport output byte without removing it.
	 */
	public fn PeekOutput() byte
	{
		return this.output.Peek()
	}

	/**
	 * Removes one output byte after the transport accepts it.
	 */
	public fn ConsumeOutput() bool
	{
		return this.output.Consume()
	}

	/**
	 * Returns the number of queued output bytes.
	 */
	public fn GetOutputCount() int
	{
		return this.output.GetCount()
	}
}