Files

502 lines
17 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. When using the standard screen of 24 rows, Row must be 0-23.
// When writing to the alternate screen, Row may be up to 1 less than the
// number of rows on the screen.
Row int
// Col is the column, 0-based, that the field attribute character should
// begin at. When using the standard screen of 80 columns, Col must be
// 0-79. When writing to the alternate screen, Col may be up to 1 less
// than the number of columns on the screen.
Col int
// Text is the content of the field to display.
Content string
// PositionOnly will use a Set Buffer Address (SBA) command to move the
// cursor to the position, but not insert a new attribute byte. The only
// properties of the Field that will apply if PositionOnly is true are:
// Row, Col, Content, Name. Others will be silently ignored.
PositionOnly bool
// 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
// AttributeOnly will cause this "field" (which won't really be a new
// field) to use the SA (Set Attribute) 3270 command to change the color
// and highlighting of the text WITHOUT starting a new field. You can
// create a field at Row 5, Column 10, with AttributeOnly true, and the
// text will start in Roe 5, Column 10 and not skip a space to column 11
// like a new field would when AttributeOnly is false.
//
// If AttributeOnly is true, the Write, Autoskip, Intense, Hidden, and
// NumericOnly properties will have no effect. Only Highlighting and Color
// will affect the output, and will both default to Default (e.g. default
// highlighting and default color) if not specificed in this field.
//
// After using AttributeOnly to change color and highlighting, you may
// need to explicitly use another AttributeOnly "field" to reset to
// defaults, a regular (AttributeOnly = false) field may not end the
// attributes. (See example1.)
AttributeOnly bool
// 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.
// Protected fields may also have a name to populate them dynamically when
// the screen is sent.
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,
// fields out of bounds of the screen size (these will simply be omitted from
// the datastream), etc.
type Screen []Field
// ScreenOpts are the options that callers may set when sending a screen
// to the 3270 client.
type ScreenOpts struct {
// If AltScreen is non-nil, the screen will be written to the "alternate"
// screen size, which is the non-default (24x80) screen dimensions that
// the terminal supports (although for many terminals, the alternate
// screen is still just 24x80). If AltScreen is nil, the default (24x80)
// mode will be used. Never switch between AltScreen and normal screen
// (e.g. AltScreen = nil) unless NoClear is false. (That is, switching
// between default and alternate screen size or back requires a screen
// clear at the same time.) When AltScreen is nil, field positions in the
// screen must be within the 24x80 screen (so rows 0-23 and cols 0-79),
// when AltScreen is present, the field positions must be within the
// dimensions of the DevInfo.AltDimensions() values.
AltScreen DevInfo
// Codepage is the Codepage implementation to use when sending text to the
// client and translating incoming field text from the client. Typically
// you should pass in the return value from DevInfo.Codepage() each time
// to get the correct codepage that was detected when the client
// connected. If nil, the global default code page (default 1047, but
// changed with the SetCodepage() function) will be used. NOTE: providing
// a DevInfo to ScreenOpts.AltScreen does _not_ automatically set this
// value, you must set it explicitly on every call that accepts
// ScreenOpts.
Codepage Codepage
// 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. When AltScreen is nil, maximum value
// is 23; otherwise, maximum is 1 less than the number of rows in
// AltScreen.
CursorRow int
// CursorCol sets the column (0-indexed) to position the cursor after
// sending the screen, when NoClear is false. When AltScreen is nil,
// maximum value is 79; otherwise, maximum is 1 less than the number of
// columns in AltScreen.
CursorCol int
// PostSendCallback is a function that, if non-nil, will be called after
// go3270 sends the datastream to the client, but before it blocks to
// read the response (if NoResponse is true, the callback will still be
// called before returning). If the function returns an error, then
// the ShowScreenOpts() function will return the error instead of a
// response. The value of CallbackData will be passed as the argument to
// the function.
PostSendCallback func(any) error
// CallbackData is passed as the argument to the PostSendCallback
// function.
CallbackData any
}
// 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 screen size) 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, opts.AltScreen, opts.Codepage)
if err != nil {
return resp, err
}
// Call the optional callback function after sending the screen.
if opts.PostSendCallback != nil {
if err := opts.PostSendCallback(opts.CallbackData); err != nil {
return resp, err
}
}
if !opts.NoResponse {
resp, err = readResponse(conn, fm, opts.AltScreen, opts.Codepage)
if err != nil {
return resp, err
}
// Strip spaces from field values unless the caller requested that we
// maintain whitespace.
for _, fld := range screen {
if !fld.KeepSpaces {
if _, ok := resp.Values[fld.Name]; ok {
resp.Values[fld.Name] =
strings.TrimSpace(resp.Values[fld.Name])
}
}
}
}
return resp, nil
}
// Deprecated: use ShowScreenOpts with default/empty ScreenOpts.
//
// NOTE: this deprecated function is NOT codepage-aware. The global code
// page set by SetCodepage will always be used.
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.
//
// NOTE: this deprecated function is NOT codepage-aware. The global code
// page set by SetCodepage will always be used.
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, dev DevInfo,
cp Codepage) (fieldmap, error) {
// Provide default Codepage implementation
if cp == nil {
cp = defaultCodepage
}
rows, cols := 24, 80
if dev != nil {
rows, cols = dev.altDimensions()
}
var b bytes.Buffer
var fm = make(fieldmap) // field buffer positions -> name
if clear {
if !(rows == 24 && cols == 80) {
b.WriteByte(0x7e) // Erase/Write Alternate to terminal
} else {
b.WriteByte(0xf5) // Erase/Write to terminal
}
} else {
b.WriteByte(0xf1) // Write to terminal
}
if clear {
b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT
} else {
// Don't clear modified data tag if we're not clearing the screen;
// we still want the client to send any data a user has modified
// in fields.
b.WriteByte(0xc2) // WCC = Reset, Unlock Keyboard (*no* reset MDT)
}
// Build the commands for each field on the screen
for _, fld := range screen {
if fld.Row < 0 || fld.Row > rows-1 || fld.Col < 0 || fld.Col > cols-1 {
// Invalid field position
continue
}
b.Write(sba(fld.Row, fld.Col, cols))
if !fld.PositionOnly {
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(cp.Encode(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*cols + 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 > rows-1 {
crow = 0
}
if ccol < 0 || ccol > cols-1 {
ccol = 0
}
b.Write(ic(crow, ccol, cols))
}
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, cols int) []byte {
result := make([]byte, 1, 3)
result[0] = 0x11 // SBA
result = append(result, getpos(row, col, cols)...)
return result
}
// buildField will return either an sf or sfe command depending for the
// field, or an sa if only setting attributes.
func buildField(f Field) []byte {
var buf bytes.Buffer
if f.AttributeOnly {
buf.WriteByte(0x28) // sa - "set attribute"
// We will always set both highlighting and color bytes
buf.WriteByte(0x41)
buf.WriteByte(byte(f.Highlighting))
buf.WriteByte(0x28) // sa - "set attribute"
buf.WriteByte(0x42)
buf.WriteByte(byte(f.Color))
return buf.Bytes()
}
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, cols int) []byte {
result := make([]byte, 0, 3)
result = append(result, sba(row, col, cols)...)
result = append(result, 0x13) // IC
return result
}
// getpos translates row and col to buffer address control characters.
func getpos(row, col, cols int) []byte {
address := row*cols + col
// Use 12-bit addressing if the buffer address fits in 12 bits
if address < 1<<12 {
hi := (address & 0xfc0) >> 6
lo := address & 0x3f
return []byte{codes[hi], codes[lo]}
}
// Otherwise, use 14-bit addressing. The library limits terminal size to
// fit within 14-bit addressing, because 16-bit addressing would require
// us to track state that the current API design doesn't lend itself to.
// Someday, perhaps in a v2 library version, we'll support absurdly large
// terminal sizes. But for now, 14 bits is as big as we can go.
hi := (address & 0x3f00) >> 8
lo := address & 0xff
// It's possible the low byte is 0xff, in which case we need to telnet-
// escape it.
if lo == 0xff {
return []byte{byte(hi), 0xff, byte(lo)}
}
return []byte{byte(hi), byte(lo)}
}