Files
go3270/screen.go
T
Matthew R. Wilson 5f2d7355e4 Non-screen-clearing update support
New ShowScreenOpts() function, which takes a new ScreenOpts struct, to control
various aspects of the ShowScreen behavior. This should now be used in favor of
the deprecated ShowScreen() and ShowScreenNoResponse() functions, and allows
for the additional of future options without needing to continue making new
functions or breaking the public API.
2025-04-12 16:56:24 -07:00

366 lines
11 KiB
Go

// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
// See LICENSE in the project root for license information.
package go3270
import (
"bytes"
"net"
"strings"
)
// Field is a field on the 3270 screen.
type Field struct {
// Row is the row, 0-based, that the field attribute character should
// begin at. This library currently only supports 24 rows, so Row must
// be 0-23.
Row int
// Col is the column, 0-based, that the field attribute character should
// begin at. This library currently only supposed 80 columns, so Column
// must be 0-79.
Col int
// Text is the content of the field to display.
Content string
// Write allows the user to edit the value of the field.
Write bool
// Autoskip causes protected (Write = false) fields to automatically be
// skipped and the cursor should move to the next field upon encountering
// this field. Autoskip is ignored on fields with Write = true.
Autoskip bool
// Intense indicates this field should be displayed with high intensity.
Intense bool
// Hidden indicates the field content should not be displayed (e.g. a
// password input field).
Hidden bool
// NumericOnly indicates that only numbers may be entered into the field.
// Very fiew 3270 clients support this, so you must always still validate
// the input on the server side.
NumericOnly bool
// Color is the field color. The default value is the default color.
Color Color
// Highlighting is the highlight attribute for the field. The default value
// is the default (i.e. no) highlighting.
Highlighting Highlight
// Name is the name of this field, which is used to get the user-entered
// data. All writeable fields on a screen must have a unique name.
Name string
// KeepSpaces will prevent the strings.TrimSpace() function from being
// called on the field value. Generally you want leading and trailing
// spaces trimmed from fields in 3270 before processing, but if you are
// building a whitespace-sensitive application, you can ask for the
// original, un-trimmed value for a field by setting this to true.
KeepSpaces bool
}
// Color is a 3270 extended field attribute color value
type Color byte
// The valid 3270 colors
const (
DefaultColor Color = 0
Blue Color = 0xf1
Red Color = 0xf2
Pink Color = 0xf3
Green Color = 0xf4
Turquoise Color = 0xf5
Yellow Color = 0xf6
White Color = 0xf7
)
// Highlight is a 3270 extended field attribute highlighting method
type Highlight byte
// The valid 3270 highlights
const (
DefaultHighlight Highlight = 0
Blink Highlight = 0xf1
ReverseVideo Highlight = 0xf2
Underscore Highlight = 0xf4
)
// Screen is an array of Fields which compose a complete 3270 screen.
// No checking is performed for lack of overlapping fields, unique field
// names,
type Screen []Field
// ScreenOpts are the options that callers may set when sending a screen
// to the 3270 client.
type ScreenOpts struct {
// NoResponse will draw the screen and immediately return, without
// waiting for any input data from the remote client.
NoResponse bool
// NoClear will send the data stream to the remote client without
// clearing the screen first. Existing data will be overlayed with
// the current screen.
NoClear bool
// CursorRow sets the row (0-indexed) to position the cursor after
// sending the screen, when NoClear is false. Maximum value is 23.
CursorRow int
// CursorCol sets the column (0-indexed) to position the cursor after
// sending the screen, when NoClear is false. Maximum value is 79.
CursorCol int
}
// fieldmap is a map of field buffer addresses and the corresponding field
// name.
type fieldmap map[int]string
// ShowScreenOpts writes the 3270 datastream for the screen, with the provided
// ScreenOpts, to a connection.
//
// Fields that aren't valid (e.g. outside of the 24x80 screen) are silently
// ignored. If a named field has an entry in the values map, the content of
// the field from the values map is used INSTEAD OF the Field struct's Content
// field. The values map may be nil if no overrides are needed.
//
// If opts.NoClear is false, the client screen will be cleared before writing
// the new screen, and the cursor will be repositioned to the values in
// opts.CursorRow and opts.CursorCol. If opts.NoClear is true, the screen will
// NOT be cleared, the cursor will NOT be repositioned, and the new screen
// will be overlayed over the current state of the client screen.
//
// If opts.NoResponse is false, ShowScreenOpts will block before returning,
// waiting for data from the client and returning the Response. If
// opts.NoResponse is true, ShowScreenOpts will immediately return after
// sending the datastream and the Response will be empty.
//
// If using from multiple threads -- one to block and wait for a response, and
// another to send screens with NoResponse and/or NoClear, be aware that if
// you change the input fields on screen after the initial blocking call is
// made, the response fields will not line up correctly and end up being
// invalid. That is to say, while waiting for a response, don't perform other
// actions from another thread that could layout the user input fields
// differently.
func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
opts ScreenOpts) (Response, error) {
var resp Response
fm, err := showScreenInternal(screen, values, opts.CursorRow,
opts.CursorCol, conn, !opts.NoClear)
if err != nil {
return resp, err
}
if !opts.NoResponse {
resp, err = readResponse(conn, fm)
if err != nil {
return resp, err
}
// Strip trailing spaces from field values. Most applications will
// want to strip leading spaces, too, but it's possible they want them
// preserved, so we'll leave that to the caller.
for _, fld := range screen {
if !fld.KeepSpaces {
if _, ok := resp.Values[fld.Name]; ok {
resp.Values[fld.Name] =
strings.TrimRight(resp.Values[fld.Name], " ")
}
}
}
}
return resp, nil
}
// Deprecated: use ShowScreenOpts with default/empty ScreenOpts.
func ShowScreen(screen Screen, values map[string]string, crow, ccol int,
conn net.Conn) (Response, error) {
return ShowScreenOpts(screen, values, conn,
ScreenOpts{CursorRow: crow, CursorCol: ccol})
}
// Deprecated: use ShowScreenOpts with ScreenOpts.NoResponse = true.
func ShowScreenNoResponse(screen Screen, values map[string]string,
crow, ccol int, conn net.Conn) error {
_, err := ShowScreenOpts(screen, values, conn,
ScreenOpts{NoResponse: true, CursorRow: crow, CursorCol: ccol})
return err
}
func showScreenInternal(screen Screen, values map[string]string,
crow, ccol int, conn net.Conn, clear bool) (fieldmap, error) {
var b bytes.Buffer
var fm = make(fieldmap) // field buffer positions -> name
if clear {
b.WriteByte(0xf5) // Erase/Write to terminal
} else {
b.WriteByte(0xf1) // Write to terminal
}
b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT
// Build the commands for each field on the screen
for _, fld := range screen {
if fld.Row < 0 || fld.Row > 23 || fld.Col < 0 || fld.Col > 79 {
// Invalid field position
continue
}
b.Write(sba(fld.Row, fld.Col))
b.Write(buildField(fld))
// Use fld.Content, unless the field is named and appears in the
// value map.
content := fld.Content
if fld.Name != "" {
if val, ok := values[fld.Name]; ok {
content = val
}
}
if content != "" {
b.Write(a2e([]byte(content)))
}
// If a writable field, add it to the field map. We add 1 to bufaddr
// to make the value match the reported position (I'm guessing it's
// because we get the position of the field's first input position,
// not the position of the field attribute byte).
if fld.Write {
bufaddr := fld.Row*80 + fld.Col
fm[bufaddr+1] = fld.Name
}
}
// If we cleared the screen, set the cursor position to the
// caller-provided coordinates.
if clear {
// Set cursor position. Correct out-of-bounds values to 0.
if crow < 0 || crow > 23 {
crow = 0
}
if ccol < 0 || ccol > 79 {
ccol = 0
}
b.Write(ic(crow, ccol))
}
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
// Now write the datastream to the writer, returning any potential error.
debugf("sending datastream: %x\n", b.Bytes())
if _, err := conn.Write(b.Bytes()); err != nil {
return nil, err
}
return fm, nil
}
// sba is the "set buffer address" 3270 command.
func sba(row, col int) []byte {
result := make([]byte, 1, 3)
result[0] = 0x11 // SBA
result = append(result, getpos(row, col)...)
return result
}
// buildField will return either an sf or sfe command depending for the
// field.
func buildField(f Field) []byte {
var buf bytes.Buffer
if f.Color == DefaultColor && f.Highlighting == DefaultHighlight {
// this is a traditional field, issue a normal sf command
buf.WriteByte(0x1d) // sf - "start field"
buf.WriteByte(sfAttribute(f.Write, f.Intense, f.Hidden, f.Autoskip,
f.NumericOnly))
return buf.Bytes()
}
// Otherwise, this needs an extended attribute field
buf.WriteByte(0x29) // sfe - "start field extended"
var paramCount byte = 1 // we will always have the basic field attribute
if f.Color != DefaultColor {
paramCount++
}
if f.Highlighting != DefaultHighlight {
paramCount++
}
buf.WriteByte(paramCount)
// Write the basic field attribute
buf.WriteByte(0xc0)
buf.WriteByte(sfAttribute(f.Write, f.Intense, f.Hidden, f.Autoskip,
f.NumericOnly))
// Write the highlighting attribute
if f.Highlighting != DefaultHighlight {
buf.WriteByte(0x41)
buf.WriteByte(byte(f.Highlighting))
}
// Write the color attribute
if f.Color != DefaultColor {
buf.WriteByte(0x42)
buf.WriteByte(byte(f.Color))
}
return buf.Bytes()
}
// sfAttribute builds the attribute byte for the "start field" 3270 command
func sfAttribute(write, intense, hidden, skip, numeric bool) byte {
var attribute byte
if !write {
attribute |= 1 << 5 // set "bit 2"
if skip {
attribute |= 1 << 4 // set "bit 3"
}
} else {
// The MDT bit -- we always want writable field values returned,
// even if unchanged
attribute |= 1 // set "bit 7"
if numeric {
attribute |= 1 << 4 // set "bit 3"
}
}
if intense {
attribute |= 1 << 3 // set "bit 4"
}
if hidden {
attribute |= 1 << 3 // set "bit 4"
attribute |= 1 << 2 // set "bit 5"
}
// Fill in top 2 bits with appropriate values
attribute = codes[attribute]
return attribute
}
// ic is the "insert cursor" 3270 command. This function will include the
// appropriate SBA command.
func ic(row, col int) []byte {
result := make([]byte, 0, 3)
result = append(result, sba(row, col)...)
result = append(result, 0x13) // IC
return result
}
// getpos translates row and col to buffer address control characters.
func getpos(row, col int) []byte {
result := make([]byte, 2)
address := row*80 + col
hi := (address & 0xfc0) >> 6
lo := address & 0x3f
result[0] = codes[hi]
result[1] = codes[lo]
return result
}