Gopher2600/gui/sdlimgui/terminal.go
JetSetIlly 3af5cfb281 maintenance work on commandline package
added extension directive. allows validation and tab-completion of a
command to be extended with additional parameters

added mapper.TerminalCommand interface. this interface works in
conjunction with the commandline package Extension interface

ELF mapper implements mapper.TerminalCommand and commandline.Extension
interfaces to give the CARTRIDGE command some ELF specific options
2024-09-14 12:30:20 +01:00

185 lines
5.5 KiB
Go

// This file is part of Gopher2600.
//
// Gopher2600 is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Gopher2600 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Gopher2600. If not, see <https://www.gnu.org/licenses/>.
package sdlimgui
import (
"sync/atomic"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/logger"
)
type term struct {
// input from the terminal window
inputChan chan string
// input from other gui elements (eg. the run button in the control window)
// only one command can be serviced at a time, which may be inconvenient
sideChan chan string
// was last TermRead() from side channel
sideChanLast atomic.Value // bool
// output to the terminal window to present as a prompt
promptChan chan terminal.Prompt
// output to the terminal window to present in the main output window
outputChan chan terminalOutput
// the state of the last call to Silence()
silenced bool
// reference to tab completion. used by terminal window
tabCompletion *commandline.TabCompletion
}
func newTerm() *term {
trm := &term{
// inputChan must not block
inputChan: make(chan string, 1),
// side-channel terminal input from other areas of the GUI. for
// example, we can have a menu item that writes "QUIT" to the side
// channel rather than calling a Quit() function directly.
//
// assigning a generous buffer. see pushCommand() for commentary.
sideChan: make(chan string, 10),
// promptChan must not block
promptChan: make(chan terminal.Prompt, 1),
// generous buffer for output channel
outputChan: make(chan terminalOutput, 4096),
}
trm.sideChanLast.Store(false)
return trm
}
// Initialise implements the terminal.Terminal interface.
func (trm *term) Initialise() error {
return nil
}
// CleanUp implements the terminal.Terminal interface.
func (trm *term) CleanUp() {
}
// RegisterTabCompletion implements the terminal.Terminal interface.
func (trm *term) RegisterTabCompletion(tc *commandline.TabCompletion) {
trm.tabCompletion = tc
}
// Silence implements the terminal.Terminal interface.
func (trm *term) Silence(silenced bool) {
trm.silenced = silenced
}
// TermPrintLine implements the terminal.Output interface.
func (trm *term) TermPrintLine(style terminal.Style, s string) {
// do not print anything if last terminal event was from sidechannel
if trm.sideChanLast.Load().(bool) {
trm.sideChanLast.Store(false)
if style == terminal.StyleError || style == terminal.StyleLog {
logger.Log(logger.Allow, "term", s)
}
return
}
if trm.silenced && style != terminal.StyleError {
return
}
trm.outputChan <- terminalOutput{style: style, text: s}
}
// TermRead implements the terminal.Input interface.
func (trm *term) TermRead(buffer []byte, prompt terminal.Prompt, events *terminal.ReadEvents) (int, error) {
trm.promptChan <- prompt
// the debugger is waiting for input from the terminal but we still need to
// service gui events in the meantime.
for {
select {
case inp := <-trm.inputChan:
copy(buffer, inp+"\n")
return len(inp) + 1, nil
case inp := <-trm.sideChan:
copy(buffer, inp+"\n")
return len(inp) + 1, nil
case sig := <-events.Signal:
return 0, events.SignalHandler(sig)
case ev := <-events.PushedFunction:
ev()
case ev := <-events.PushedFunctionImmediate:
ev()
return 0, nil
case ev := <-events.UserInput:
err := events.UserInputHandler(ev)
if err != nil {
return 0, nil
}
}
}
}
// TermRead implements the terminal.Input interface.
func (trm *term) TermReadCheck() bool {
// report on the number of pending items in inputChan and sideChan. if
// either of these have events waiting then that counts as true
return len(trm.inputChan) > 0 || len(trm.sideChan) > 0
}
// IsInteractive implements the terminal.Input interface.
func (trm *term) IsInteractive() bool {
return true
}
// IsRealTerminal implements the terminal.Input interface.
func (trm *term) IsRealTerminal() bool {
return false
}
// where possible the debugger issues commands via the terminal. this has the
// advntage of (a) simplicity and (b) consistency. A QUIT command, for example,
// will work in exactly the same way from the main manu or from the terminal.
//
// to achieve this functionality, the terminal has a side-channel to which a
// complete string is pushed (without a newline character please). the
// pushCommand() is a conveniently placed function to do this.
//
// ** do not to push commands if GUI is not in debug mode. there won't
// be anything to receive the input and so the channel will eventually
// fill up.
func (trm *term) pushCommand(input string) {
select {
case trm.sideChan <- input:
trm.sideChanLast.Store(true)
default:
// hopefully the side channel buffer is deep enough so that we don't
// ever have to drop input before the buffer can emptied in TermRead().
//
// in most instances a depth of one is sufficient but occasionally it
// is not (eg. the HALT/RUN commands sent by the rewind slider in
// win_control)
logger.Logf(logger.Allow, "term", "dropping from side channel (%s)", input)
}
}