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
This commit is contained in:
JetSetIlly 2024-09-09 12:22:38 +01:00
parent 6ca3124e0a
commit 3af5cfb281
20 changed files with 404 additions and 213 deletions

View file

@ -21,7 +21,6 @@ import (
"io"
"os"
"runtime"
"sort"
"strconv"
"strings"
@ -64,23 +63,20 @@ var scriptUnsafeCommands *commandline.Commands
func init() {
var err error
// parse command template
debuggerCommands, err = commandline.ParseCommandTemplate(commandTemplate)
if err != nil {
panic(err)
}
err = debuggerCommands.AddHelp(cmdHelp, helps)
err = commandline.AddHelp(debuggerCommands)
if err != nil {
panic(err)
}
sort.Stable(debuggerCommands)
scriptUnsafeCommands, err = commandline.ParseCommandTemplate(scriptUnsafeTemplate)
if err != nil {
panic(err)
}
sort.Stable(scriptUnsafeCommands)
}
// parseCommand tokenises the input and processes the tokens.
@ -163,12 +159,19 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
default:
return fmt.Errorf("%s is not yet implemented", command)
case cmdHelp:
keyword, ok := tokens.Get()
if ok {
dbg.printLine(terminal.StyleHelp, debuggerCommands.Help(keyword))
case commandline.HelpCommand:
if topic, ok := tokens.Get(); ok {
topic = strings.ToUpper(topic)
dbg.printLine(terminal.StyleHelp, helps[topic])
// also print usage command if the command has arguments
usage := debuggerCommands.Usage(topic)
if strings.Count(usage, " ") > 0 {
dbg.printLine(terminal.StyleHelp, "")
dbg.printLine(terminal.StyleHelp, fmt.Sprintf("Usage: %s", debuggerCommands.Usage(topic)))
}
} else {
dbg.printLine(terminal.StyleHelp, debuggerCommands.HelpOverview())
dbg.printLine(terminal.StyleHelp, commandline.HelpSummary(debuggerCommands))
}
// we don't want the HELP command to appear in the script
@ -575,7 +578,14 @@ func (dbg *Debugger) processTokens(tokens *commandline.Tokens) error {
spec, _ := tokens.Get()
err := dbg.vcs.Mem.Cart.SetBank(spec)
if err != nil {
dbg.printLine(terminal.StyleFeedback, err.Error())
dbg.printLine(terminal.StyleError, err.Error())
}
default:
tokens.Unget()
w := dbg.writerInStyle(terminal.StyleFeedback)
err := dbg.vcs.Mem.Cart.ParseCommand(w, tokens.Remainder())
if err != nil {
dbg.printLine(terminal.StyleError, err.Error())
}
}
} else {

View file

@ -15,8 +15,10 @@
package debugger
import "github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
var helps = map[string]string{
cmdHelp: "Lists commands and provides help for individual commands.",
commandline.HelpCommand: "Lists commands and provides help for individual commands.",
cmdReset: `Reset the emulated machine (including television) to its initial state. The
debugger itself (breakpoints, etc.) will not be reset.`,
@ -91,8 +93,7 @@ http:// will loaded via the http protocol. If no such protocol is present, the
cartridge will be loaded from disk.`,
cmdCartridge: `Display information about the current cartridge. Without arguments the command
will show where the game was loaded from, the cartridge type and bank number. The BANK
argument meanwhile can be used to switch banks (if possible).`,
will show where the game was loaded from, the cartridge type and bank number.`,
cmdPatch: "Apply a patch file to the loaded cartridge",

View file

@ -87,8 +87,6 @@ const (
cmdVersion = "VERSION"
)
const cmdHelp = "HELP"
var commandTemplate = []string{
cmdReset,
cmdQuit,
@ -103,7 +101,7 @@ var commandTemplate = []string{
cmdGoto + " [%<clock>N] (%<scanline>N) (%<frame>N)",
cmdInsert + " %<cartridge>F",
cmdCartridge + " (PATH|NAME|MAPPER|CONTAINER|MAPPEDBANKS|HASH|STATIC|REGISTERS|RAM|DUMP|SETBANK %<bank>S)",
cmdCartridge + " (PATH|NAME|MAPPER|CONTAINER|MAPPEDBANKS|HASH|STATIC|REGISTERS|RAM|DUMP|SETBANK %<bank>S|{%<mapper specific>X})",
cmdPatch + " %<patch file>S",
cmdDisasm + " (BYTECODE|REDUX)",
cmdGrep + " (OPERATOR|OPERAND|COPROC) %<search>S",
@ -140,9 +138,9 @@ var commandTemplate = []string{
cmdKeypad + " [LEFT|RIGHT] [NONE|0|1|2|3|4|5|6|7|8|9|*|#]",
// halt conditions
cmdBreak + " [%<pc value>S|%<target>S %<value>N] {& %<value>S|%<target>S %<value>S}",
cmdTrap + " [%<target>S] {%<targets>S}",
cmdWatch + " (READ|WRITE) (STRICT) (PHANTOM|GHOST) [%<address>S] (%<value>S)",
cmdBreak + " [%<symbol>S|%<target>S %<value>N] {& %<symbol>S|%<target>S %<value>S}",
cmdTrap + " [%<symbol>S] {%<symbol>S}",
cmdWatch + " (READ|WRITE) (STRICT) (PHANTOM|GHOST) [%<symbol>S] (%<value>S)",
cmdTrace + " (STRICT) (%<address>S)",
cmdList + " [BREAKS|TRAPS|WATCHES|TRACES|ALL]",
cmdDrop + " [BREAK|TRAP|WATCH|TRACE] %<number in list>N",

View file

@ -490,6 +490,9 @@ func NewDebugger(opts CommandLineOptions, create CreateUserInterface) (*Debugger
return nil, fmt.Errorf("debugger: %w", err)
}
// add extensions to debugger commands
debuggerCommands.AddExtension("mapper specific", dbg.vcs.Mem.Cart)
return dbg, nil
}

View file

@ -22,6 +22,7 @@ import (
"github.com/jetsetilly/gopher2600/debugger"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/gui"
"github.com/jetsetilly/gopher2600/prefs"
)
@ -55,7 +56,7 @@ func (trm *mockTerm) Initialise() error {
func (trm *mockTerm) CleanUp() {
}
func (trm *mockTerm) RegisterTabCompletion(_ terminal.TabCompletion) {
func (trm *mockTerm) RegisterTabCompletion(_ *commandline.TabCompletion) {
}
func (trm *mockTerm) Silence(silenced bool) {

View file

@ -23,8 +23,8 @@ package colorterm
import (
"os"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/colorterm/easyterm"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
)
// ColorTerminal implements debugger UI interface with a basic ANSI terminal.
@ -33,7 +33,7 @@ type ColorTerminal struct {
reader runeReader
commandHistory []command
tabCompletion terminal.TabCompletion
tabCompletion *commandline.TabCompletion
silenced bool
}
@ -62,9 +62,7 @@ func (ct *ColorTerminal) CleanUp() {
ct.EasyTerm.CleanUp()
}
// RegisterTabCompletion adds an implementation of TabCompletion to the
// ColorTerminal.
func (ct *ColorTerminal) RegisterTabCompletion(tc terminal.TabCompletion) {
func (ct *ColorTerminal) RegisterTabCompletion(tc *commandline.TabCompletion) {
ct.tabCompletion = tc
}

View file

@ -16,139 +16,64 @@
package commandline
import (
"fmt"
"strings"
)
// A commandline Extension provides an instance of Commands such that it can be
// used to extend the number of parameters available to a command
type Extension interface {
Commands() *Commands
}
// Commands is the root of the node tree.
type Commands struct {
Index map[string]*node
index map[string]*node
cmds []*node
// list of commands. should be sorted alphabetically
list []*node
helpCommand string
helpCols int
helpColFmt string
helps map[string]string
// extension handlers. indexed by a name given to the extension in the
// commands template
extensions map[string]Extension
}
// Len implements Sort package interface.
func (cmds Commands) Len() int {
return len(cmds.cmds)
return len(cmds.list)
}
// Less implements Sort package interface.
func (cmds Commands) Less(i int, j int) bool {
return cmds.cmds[i].tag < cmds.cmds[j].tag
return cmds.list[i].tag < cmds.list[j].tag
}
// Swap implements Sort package interface.
func (cmds Commands) Swap(i int, j int) {
cmds.cmds[i], cmds.cmds[j] = cmds.cmds[j], cmds.cmds[i]
cmds.list[i], cmds.list[j] = cmds.list[j], cmds.list[i]
}
// String returns the verbose representation of the command tree. Use this only
// for testing/validation purposes. HelpString() is more useful to the end
// user.
// for testing/validation purposes. Help() is more useful to the end user.
func (cmds Commands) String() string {
s := strings.Builder{}
for c := range cmds.cmds {
s.WriteString(cmds.cmds[c].String())
for c := range cmds.list {
s.WriteString(cmds.list[c].String())
s.WriteString("\n")
}
return strings.TrimRight(s.String(), "\n")
}
// AddHelp adds a "help" command to an already prepared Commands type. it uses
// the top-level nodes of the Commands instance as arguments for the specified
// helpCommand.
func (cmds *Commands) AddHelp(helpCommand string, helps map[string]string) error {
// if help command exists then there is nothing to do
if _, ok := cmds.Index[helpCommand]; ok {
return fmt.Errorf("%s: already defined", helpCommand)
// Usage returns the usage string for a command
func (cmds Commands) Usage(command string) string {
if c, ok := cmds.index[command]; ok {
return c.string(true, false)
}
// keep reference to helps
cmds.helps = helps
// helpCommand consist of the helpCommand string followed by all the other
// commands as optional arguments
defn := strings.Builder{}
defn.WriteString(helpCommand)
defn.WriteString(" (")
// build help command
longest := 0
if len(cmds.cmds) > 0 {
defn.WriteString(cmds.cmds[0].tag)
for i := 1; i < len(cmds.cmds); i++ {
defn.WriteString("|")
if cmds.cmds[i].isPlaceholder() && cmds.cmds[i].placeholderLabel != "" {
defn.WriteString(cmds.cmds[i].placeholderLabel)
} else {
defn.WriteString(cmds.cmds[i].tag)
}
if len(cmds.cmds[i].tag) > longest {
longest = len(cmds.cmds[i].tag)
}
}
}
// add HELP command itself to list of possible HELP arguments
defn.WriteString("|")
defn.WriteString(helpCommand)
// close argument list
defn.WriteString(")")
// parse the constructed definition
p, d, err := parseDefinition(defn.String(), "")
if err != nil {
return fmt.Errorf("%s: %s (char %d)", helpCommand, err, d)
}
// add parsed definition to list of commands
cmds.cmds = append(cmds.cmds, p)
// add to index
cmds.Index[p.tag] = p
// record sizing information for help subsystem
cmds.helpCommand = helpCommand
cmds.helpCols = 80 / (longest + 3)
cmds.helpColFmt = fmt.Sprintf("%%%ds", longest+3)
return nil
return ""
}
// HelpOverview returns a columnised list of all help entries.
func (cmds Commands) HelpOverview() string {
s := strings.Builder{}
for c := range cmds.cmds {
s.WriteString(fmt.Sprintf(cmds.helpColFmt, cmds.cmds[c].tag))
if c%cmds.helpCols == cmds.helpCols-1 {
s.WriteString("\n")
}
func (cmds *Commands) AddExtension(group string, extension Extension) {
if cmds.extensions == nil {
cmds.extensions = make(map[string]Extension)
}
return strings.TrimRight(s.String(), "\n")
}
// Help returns the help (and usage for the command).
func (cmds Commands) Help(keyword string) string {
keyword = strings.ToUpper(keyword)
s := strings.Builder{}
if helpTxt, ok := cmds.helps[keyword]; !ok {
s.WriteString(fmt.Sprintf("no help for %s", keyword))
} else {
s.WriteString(helpTxt)
if cmd, ok := cmds.Index[keyword]; ok {
s.WriteString("\n\n Usage: ")
s.WriteString(cmd.usageString())
}
}
return s.String()
cmds.extensions[strings.ToLower(group)] = extension
}

View file

@ -62,6 +62,8 @@
// sort(data, rising)
// }
//
// # Tab Completion
//
// The TabCompletion type is used to transform input such that it more closely
// resemebles a valid command according to the supplied template. The
// NewTabCompletion() function expects an instance of Commands.
@ -78,4 +80,29 @@
// be returned first followed by the second, third, etc. on subsequent calls to
// Complete(). A tab completion session can be terminated with a call to
// Reset().
//
// # Extensions
//
// Extensions are a way of giving a command additional arguments that are not in
// the original template. This is good for handling specialist arguments that
// are rarely used but none-the-less would benefit from tab-completion.
//
// To give a command an extension the %x directive is used. The placeholder must
// have a label for it to be effective. In the example below the label is
// "listing". Note that labels are case insensitive.
//
// template := []string {
// "LIST (%<listing>x)",
// "PRINT [%s]",
// "SORT (RISING|FALLING)",
// }
//
// Once the template has been parsed, an extension handler must be added with
// the AddExtension() function. In addition to the label argument, which must be
// the same as the label given in the template, the AddExtension() function also
// requires an implementation of the interface.
//
// The Commands() function of the Extension interface will return another
// instance of the Commands type. This instance will be used when the %x
// directive is encounted during validation or tab completion.
package commandline

View file

@ -0,0 +1,82 @@
// 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 commandline
import (
"fmt"
"sort"
"strings"
)
// The command that should be used to invoke the HELP system
const HelpCommand = "HELP"
// AddHelp() adds the HELP command to the list of commands if it hasn't been
// added already (or was part of the template given to the ParseCommandTemplate()
// function)
//
// It is up to the user of the commandline package to do something with the HELP
// command
func AddHelp(cmds *Commands) error {
// add help command only if it's not been added already
if _, ok := cmds.index[HelpCommand]; ok {
return nil
}
// create definition string using existing command list
var def string
def = fmt.Sprintf("%s (", HelpCommand)
for _, n := range cmds.list {
def = fmt.Sprintf("%s%s|", def, n.tag)
}
def = strings.TrimSuffix(def, "|")
def = fmt.Sprintf("%s)", def)
// parse definition and add it to the list of commands
h, _, err := parseDefinition(def, "")
if err != nil {
return fmt.Errorf("parser: error adding HELP command: %w", err)
}
cmds.index[HelpCommand] = h
cmds.list = append(cmds.list, h)
// resort changed list of commands
sort.Stable(cmds)
return nil
}
// HelpSummary returns a string showing the top-level HELP topics in five columns
func HelpSummary(cmds *Commands) string {
// which help topic is the longest (string length). we should maybe do this
// once after adding the commands but this is simpler and with no impact
var longest int
for _, n := range cmds.list {
longest = max(longest, len(n.tag))
}
longest += 2
var s strings.Builder
for i, n := range cmds.list {
s.WriteString(n.tag)
if (i+1)%5 == 0 {
s.WriteString("\n")
} else {
s.WriteString(strings.Repeat(" ", longest-len(n.tag)))
}
}
return s.String()
}

View file

@ -46,7 +46,7 @@ type node struct {
tag string
// friendly name for the placeholder tags. not used if tag is not a
// placeholder. you can use isPlaceholder() to check
// placeholder. if string is not nil then that indicates that
placeholderLabel string
typ nodeType
@ -59,13 +59,12 @@ type node struct {
}
// String returns the verbose representation of the node (and its children).
// Use this only for testing/validation purposes. HelpString() is more useful
// to the end user.
// Only useful this only for testing/validation purposes.
func (n node) String() string {
return n.string(false, false)
}
// HelpString returns the string representation of the node (and it's children)
// usageString returns the string representation of the node (and it's children)
// without extraneous placeholder directives (if placeholderLabel is available)
//
// So called because it's better to use when displaying help.
@ -93,10 +92,13 @@ func (n node) usageString() string {
//
// note: string should not be called directly except as a recursive call
// or as an initial call from String() and usageString().
//
// note: fromBranch should always be false on first call. it is only ever true
// on a recursive call
func (n node) string(useLabels bool, fromBranch bool) string {
s := strings.Builder{}
if n.isPlaceholder() && n.placeholderLabel != "" {
if n.placeholderLabel != "" {
// placeholder labels come without angle brackets
label := fmt.Sprintf("<%s>", n.placeholderLabel)
if useLabels {
@ -183,10 +185,10 @@ func (n node) string(useLabels bool, fromBranch bool) string {
func (n node) nodeVerbose() string {
s := strings.Builder{}
s.WriteString(n.tagVerbose())
for bi := range n.branch {
if n.branch[bi].tag != "" {
for _, br := range n.branch {
if br.tag != "" {
s.WriteString(" or ")
s.WriteString(n.branch[bi].tagVerbose())
s.WriteString(br.tagVerbose())
}
}
return s.String()
@ -195,29 +197,22 @@ func (n node) nodeVerbose() string {
// tagVerbose returns a readable versions of the tag field, using labels if
// possible.
func (n node) tagVerbose() string {
if n.isPlaceholder() {
if n.placeholderLabel != "" {
return n.placeholderLabel
}
switch n.tag {
case "%S":
return "string argument"
case "%N":
return "numeric argument"
case "%P":
return "floating-point argument"
case "%F":
return "filename argument"
default:
return "placeholder argument"
}
if n.placeholderLabel == "" {
return n.tag
}
return n.tag
}
// isPlaceholder checks tag to see if it is a placeholder. does not check to
// see if placeholder is valid.
func (n node) isPlaceholder() bool {
return len(n.tag) == 2 && n.tag[0] == '%'
switch n.tag {
case "%S":
return "string argument"
case "%N":
return "numeric argument"
case "%P":
return "floating-point argument"
case "%F":
return "filename argument"
case "%X":
return "extension argument"
default:
return "placeholder argument"
}
}

View file

@ -30,6 +30,7 @@ package commandline
import (
"fmt"
"sort"
"strings"
)
@ -47,24 +48,25 @@ import (
//
// Placeholders
//
// %N numeric value
// %P irrational number value
// %S string (numbers can be strings too)
// %F file name
// %N numeric value
// %P irrational number value
// %S string (numbers can be strings too)
// %F file name
// %X extension
//
// Placeholders can be labelled. For example:
//
// %<first name>S
// %<age>N
//
// Returned commands are sorted alphabetically
func ParseCommandTemplate(template []string) (*Commands, error) {
cmds := &Commands{
cmds: make([]*node, 0, 10),
Index: make(map[string]*node),
cmds := Commands{
list: make([]*node, 0, 10),
index: make(map[string]*node),
}
for t := range template {
defn := template[t]
for t, defn := range template {
// tidy up spaces in definition string - we don't want more than one
// consecutive space
defn = strings.Join(strings.Fields(defn), " ")
@ -85,15 +87,19 @@ func ParseCommandTemplate(template []string) (*Commands, error) {
}
// add to list of commands (order doesn't matter at this stage)
cmds.cmds = append(cmds.cmds, p)
cmds.list = append(cmds.list, p)
}
// build index
for ci := range cmds.cmds {
cmds.Index[cmds.cmds[ci].tag] = cmds.cmds[ci]
for _, l := range cmds.list {
if _, ok := cmds.index[l.tag]; ok {
return nil, fmt.Errorf("parser: duplicate command in template: %s", l.tag)
}
cmds.index[l.tag] = l
}
return cmds, nil
sort.Stable(cmds)
return &cmds, nil
}
func parseDefinition(defn string, trigger string) (*node, int, error) {
@ -361,9 +367,17 @@ func parseDefinition(defn string, trigger string) (*node, int, error) {
p = string(defn[i])
}
if p != "N" && p != "P" && p != "S" && p != "F" && p != "%" {
if p != "N" && p != "P" && p != "S" && p != "F" && p != "%" && p != "X" {
return nil, i, fmt.Errorf("unknown placeholder directive (%s)", wn.tag)
}
// extension placeholder requires a label for it to be effective
if p == "X" {
if len(wn.placeholderLabel) == 0 {
return nil, i, fmt.Errorf("%%x placeholder (%s) requires a label", wn.tag)
}
}
wn.tag = fmt.Sprintf("%%%s", p)
case ' ':

View file

@ -15,8 +15,6 @@
package commandline
// #tab #completion
import (
"fmt"
"strconv"
@ -34,17 +32,18 @@ type TabCompletion struct {
lastCompletion string
}
// NewTabCompletion initialises a new TabCompletion instance. Completion works
// best if Commands has been sorted.
// NewTabCompletion initialises a new TabCompletion instance
func NewTabCompletion(cmds *Commands) *TabCompletion {
tc := &TabCompletion{cmds: cmds}
tc := TabCompletion{
cmds: cmds,
}
tc.Reset()
return tc
return &tc
}
// Complete transforms the input such that the last word in the input is
// expanded to meet the closest match allowed by the template. Subsequent
// calls to Complete() without an intervening call to Reset() will cycle
// calls to Complete() without an intervening call to TabCompletionReset() will cycle
// through the original available options.
func (tc *TabCompletion) Complete(input string) string {
// split input tokens -- it's easier to work with tokens
@ -92,9 +91,7 @@ func (tc *TabCompletion) Complete(input string) string {
tok = strings.ToUpper(tok)
// look for match
for i := range tc.cmds.cmds {
n := tc.cmds.cmds[i]
for _, n := range tc.cmds.list {
// if there is an exact match then recurse into the node looking for
// where the last token coincides with the node tree
if tok == n.tag {
@ -106,7 +103,7 @@ func (tc *TabCompletion) Complete(input string) string {
// recurse
tokens.Unget()
tc.buildMatches(n, tokens)
tc.Match(n, tokens)
return endGuess()
}
@ -128,9 +125,9 @@ func (tc *TabCompletion) Reset() {
tc.match = -1
}
func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
func (tc *TabCompletion) Match(n *node, tokens *Tokens) {
// note the current state of tokens. we use this to make sure we don't
// continuously attempt to call buildMatches with the same token state
// continuously attempt to call Match() with the same token state
remainder := tokens.Remainder()
// if there is no more input then return true (validation has passed) if
@ -153,13 +150,13 @@ func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
}
tokens.Unget()
for ni := range n.next {
tc.buildMatches(n.next[ni], tokens)
for _, nx := range n.next {
tc.Match(nx, tokens)
}
for bi := range n.branch {
for _, br := range n.branch {
tokens.Unget()
tc.buildMatches(n.branch[bi], tokens)
tc.Match(br, tokens)
}
return
@ -187,6 +184,21 @@ func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
// see commentary for %S above
match = false
case "%X":
if x, ok := tc.cmds.extensions[n.placeholderLabel]; ok {
xcmds := x.Commands()
if xcmds != nil {
for _, xn := range xcmds.list {
// unget token on each call of match because the token would
// have been consumed on the previous call to match
tokens.Unget()
tc.Match(xn, tokens)
}
}
} else {
match = false
}
default:
// case sensitive matching
tok = strings.ToUpper(tok)
@ -210,17 +222,16 @@ func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
// take a note of current token position. if the token wanders past
// this point as a result of a branch then we can see that the branch
// was deeper then just one token. if this is the case then we can see
// that the branch was *partially* accepted and that we should not
// proceed onto next-nodes from here.
// that the branch was *partially* accepted
tokenAt := tokens.curr
for bi := range n.branch {
for _, br := range n.branch {
// we want to use the current token again so we unget() the last
// token so that it is available at the beginning of the recursed
// function
tokens.Unget()
tc.buildMatches(n.branch[bi], tokens)
tc.Match(br, tokens)
}
// the key to this condition is the tokenAt variable. see note above.
@ -245,8 +256,8 @@ func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
}
// token does match this node. check nodes that follow on.
for nx := range n.next {
tc.buildMatches(n.next[nx], tokens)
for _, nx := range n.next {
tc.Match(nx, tokens)
}
// no more nodes in the next array. move to the repeat node if there is one
@ -255,6 +266,6 @@ func (tc *TabCompletion) buildMatches(n *node, tokens *Tokens) {
if remainder == tokens.Remainder() {
return
}
tc.buildMatches(n.repeat, tokens)
tc.Match(n.repeat, tokens)
}
}

View file

@ -35,9 +35,9 @@ func (cmds Commands) ValidateTokens(tokens *Tokens) error {
}
cmd = strings.ToUpper(cmd)
for n := range cmds.cmds {
if cmd == cmds.cmds[n].tag {
err := cmds.cmds[n].validate(tokens, false)
for n := range cmds.list {
if cmd == cmds.list[n].tag {
err := cmds.list[n].validate(tokens, false)
if err != nil {
return err
}
@ -48,7 +48,7 @@ func (cmds Commands) ValidateTokens(tokens *Tokens) error {
arg, _ := tokens.Get()
// special handling for help command
if cmd == cmds.helpCommand {
if cmd == HelpCommand {
return fmt.Errorf("no help for %s", strings.ToUpper(arg))
}
@ -180,6 +180,11 @@ func (n *node) validate(tokens *Tokens, speculative bool) error {
tentativeMatch = true
match = n.branch == nil
case "%X":
// we could choose to pass this to the actual extension but I don't
// think there's any real need to
match = true
default:
// case insensitive matching. n.tag should have been normalised
// already.

View file

@ -23,6 +23,7 @@ import (
"os"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
)
// PlainTerminal is the default, most basic terminal interface. It keeps the
@ -46,7 +47,7 @@ func (pt *PlainTerminal) CleanUp() {
}
// RegisterTabCompletion adds an implementation of TabCompletion to the terminal.
func (pt *PlainTerminal) RegisterTabCompletion(terminal.TabCompletion) {
func (pt *PlainTerminal) RegisterTabCompletion(*commandline.TabCompletion) {
}
// Silence implements the terminal.Terminal interface.

View file

@ -20,6 +20,7 @@ import (
"fmt"
"os"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/userinput"
)
@ -103,20 +104,13 @@ type Terminal interface {
// Register a tab completion implementation to use with the terminal. Not
// all implementations need to respond meaningfully to this.
RegisterTabCompletion(TabCompletion)
RegisterTabCompletion(*commandline.TabCompletion)
// Silence all input and output except error messages. In other words,
// TermPrintLine() should display error messages even if silenced is true.
Silence(silenced bool)
}
// TabCompletion defines the operations required for tab completion. A good
// implementation can be found in the commandline sub-package.
type TabCompletion interface {
Complete(input string) string
Reset()
}
// Broker implementations can identify a terminal.
type Broker interface {
GetTerminal() Terminal

View file

@ -19,6 +19,7 @@ import (
"sync/atomic"
"github.com/jetsetilly/gopher2600/debugger/terminal"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/logger"
)
@ -43,7 +44,7 @@ type term struct {
silenced bool
// reference to tab completion. used by terminal window
tabCompletion terminal.TabCompletion
tabCompletion *commandline.TabCompletion
}
func newTerm() *term {
@ -78,7 +79,7 @@ func (trm *term) CleanUp() {
}
// RegisterTabCompletion implements the terminal.Terminal interface.
func (trm *term) RegisterTabCompletion(tc terminal.TabCompletion) {
func (trm *term) RegisterTabCompletion(tc *commandline.TabCompletion) {
trm.tabCompletion = tc
}

View file

@ -18,10 +18,12 @@ package cartridge
import (
"errors"
"fmt"
"io"
"strings"
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/ace"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/cdf"
@ -406,6 +408,22 @@ func (cart *Cartridge) SetBank(bank string) error {
return fmt.Errorf("cartridge: %s does not support setting of bank", cart.mapper.ID())
}
// Commands returns an instance of commandline.Commands
func (cart *Cartridge) Commands() *commandline.Commands {
if com, ok := cart.mapper.(mapper.TerminalCommand); ok {
return com.Commands()
}
return nil
}
// ParseCommand forwards a terminal command to the mapper
func (cart *Cartridge) ParseCommand(w io.Writer, command string) error {
if com, ok := cart.mapper.(mapper.TerminalCommand); ok {
return com.ParseCommand(w, command)
}
return fmt.Errorf("cartridge: %s does not support any terminal commands", cart.mapper.ID())
}
// AccessPassive is called so that the cartridge can respond to changes to the
// address and data bus even when the data bus is not addressed to the cartridge.
//

View file

@ -0,0 +1,88 @@
// 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 elf
import (
"fmt"
"io"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
)
func newCommands() (*commandline.Commands, error) {
var template = []string{
"STREAM (DRAIN|NEXT)",
}
commands, err := commandline.ParseCommandTemplate(template)
if err != nil {
return nil, err
}
return commands, nil
}
// Commands implements the mapper.TerminalCommand interface
func (elf *Elf) Commands() *commandline.Commands {
return elf.commands
}
// ParseCommand implements the mapper.TerminalCommand interface
func (elf *Elf) ParseCommand(w io.Writer, command string) error {
tokens := commandline.TokeniseInput(command)
err := elf.commands.ValidateTokens(tokens)
if err != nil {
return err
}
tokens.Reset()
arg, _ := tokens.Get()
switch arg {
case "STREAM":
if !elf.mem.stream.active {
return fmt.Errorf("ELF streaming is not active")
}
arg, ok := tokens.Get()
if ok {
switch arg {
case "DRAIN":
if elf.mem.stream.drain {
w.Write([]byte("ELF stream is already draining"))
} else {
w.Write([]byte("ELF stream draining started"))
elf.mem.stream.drain = true
}
case "NEXT":
if elf.mem.stream.drain {
w.Write([]byte(fmt.Sprintf("%s", elf.mem.stream.stream[elf.mem.stream.drainPtr])))
} else {
w.Write([]byte("ELF stream is not currently draining"))
}
}
} else {
if elf.mem.stream.drain {
w.Write([]byte(fmt.Sprintf("ELF stream draining: %d remaining",
elf.mem.stream.drainTop-elf.mem.stream.drainPtr)))
} else {
w.Write([]byte(fmt.Sprintf("ELF stream length: %d",
elf.mem.stream.ptr)))
}
}
}
return nil
}

View file

@ -24,6 +24,7 @@ import (
"github.com/jetsetilly/gopher2600/cartridgeloader"
"github.com/jetsetilly/gopher2600/coprocessor"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/arm"
"github.com/jetsetilly/gopher2600/hardware/memory/cartridge/mapper"
@ -47,6 +48,9 @@ type Elf struct {
// armState is a copy of the ARM's state at the moment of the most recent
// Snapshot. it's used only suring a Plumb() operation
armState *arm.ARMState
// commandline extensions
commands *commandline.Commands
}
// elfReaderAt is an implementation of io.ReaderAt and is used with elf.NewFile()
@ -136,6 +140,11 @@ func NewElf(env *environment.Environment, loader cartridgeloader.Loader, inACE b
yieldHook: coprocessor.StubCartYieldHook{},
}
cart.commands, err = newCommands()
if err != nil {
return nil, fmt.Errorf("ELF: %w", err)
}
cart.mem = newElfMemory(cart.env)
cart.arm = arm.NewARM(cart.env, cart.mem.model, cart.mem, cart)
cart.mem.Plumb(cart.arm)

View file

@ -16,6 +16,9 @@
package mapper
import (
"io"
"github.com/jetsetilly/gopher2600/debugger/terminal/commandline"
"github.com/jetsetilly/gopher2600/environment"
"github.com/jetsetilly/gopher2600/hardware/cpu"
"github.com/jetsetilly/gopher2600/hardware/memory/vcs"
@ -100,6 +103,13 @@ type SelectableBank interface {
SetBank(bank string) error
}
// TerminalCommand allows a mapper to react to terminal commands from the
// debugger
type TerminalCommand interface {
Commands() *commandline.Commands
ParseCommand(w io.Writer, command string) error
}
// PlumbFromDifferentEmulation is for mappers that are sensitive to being
// transferred from one emulation to another.
//