mirror of
https://github.com/JetSetIlly/Gopher2600.git
synced 2025-04-02 11:02:17 -04:00
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:
parent
6ca3124e0a
commit
3af5cfb281
20 changed files with 404 additions and 213 deletions
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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)
|
||||
func (cmds *Commands) AddExtension(group string, extension Extension) {
|
||||
if cmds.extensions == nil {
|
||||
cmds.extensions = make(map[string]Extension)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
82
debugger/terminal/commandline/help.go
Normal file
82
debugger/terminal/commandline/help.go
Normal 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()
|
||||
}
|
|
@ -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,9 +197,8 @@ 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
|
||||
if n.placeholderLabel == "" {
|
||||
return n.tag
|
||||
}
|
||||
|
||||
switch n.tag {
|
||||
|
@ -209,15 +210,9 @@ func (n node) tagVerbose() string {
|
|||
return "floating-point argument"
|
||||
case "%F":
|
||||
return "filename argument"
|
||||
case "%X":
|
||||
return "extension argument"
|
||||
default:
|
||||
return "placeholder argument"
|
||||
}
|
||||
}
|
||||
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] == '%'
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ package commandline
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -51,20 +52,21 @@ import (
|
|||
// %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 ' ':
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
88
hardware/memory/cartridge/elf/commands.go
Normal file
88
hardware/memory/cartridge/elf/commands.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
Loading…
Add table
Reference in a new issue