Alternate screen size support.

This is a relatively big new feature, which introduces small breaking changes
to the API:

 * NegotiateTelnet() now returns (DevInfo, error) instead of just error.  The
   function signature for the Tx type now has one additional DevInfo parameter.
 * RunTransactions() requires one additional parameter, the DevInfo (which is
   allowed to be nil).

Otherwise, existing behavior continues to work as-is and everything still
defaults to operating on the default 24x80 terminal size.

See example5 for a larger-terminal-aware application.

Pending some time to make sure no new bugs cropped up in this release and that
the alternate screen size support works in the wild, I anticipate this will
become the v1.0.0 release of go3270.
This commit is contained in:
Matthew R. Wilson committed 2025-06-29 12:58:18 -07:00
1 parent 3a38ce6e4a
commit b6fa48f37f
19 files changed
+820 -126

No files matched your search

+1
View File
@@ -3,3 +3,4 @@ example1/example1
example2/example2
example3/example3
example4/example4
example5/example5
+9 -8
View File
@@ -10,7 +10,13 @@ This library allows you to write Go servers for tn3270 clients by building 3270
Usage
-----
See the example folders for quick demonstrations of using the library. example1 uses the lower-level function ShowScreenOpts(), and example2 uses a higher-level function HandleScreen(). example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
See the example folders for quick demonstrations of using the library:
* example1 uses the lower-level function ShowScreenOpts().
* example2 uses a higher-level convenience function HandleScreen().
* example3 demonstrates updating the client's 3270 display while waiting for a response by using an update thread and a waiting response thread.
* example4 demonstrates the RunTransactions() approach to handing control from one screen to the next. This is the recommended way to build applications using go3270.
* example5 demonstrates support for larger-than-default (24x80) terminal sizes.
**NEW**: For larger applications, I recommend using the `RunTransactions()` function to serve as the driver for your application. You can implement transaction functions which pass control from one transaction to another. example4 demonstrates a "larger" application that uses this approach.
@@ -18,22 +24,17 @@ Here's [a video introducing the library][introVideo] as well.
[introVideo]: https://www.youtube.com/watch?v=h9XTjup5W5U
Known Problems
--------------
- The telnet negotiation does not check for any errors or for any responses from the client. We just assume it goes well and we're actually talking to a tn3270 client.
- Screen size is limited to exactly 24x80. In the future, the terminal type could be interrogated and the library could support the screen sizes of other 3270 models.
3270 information
----------------
Everything I know about 3270 data streams I learned from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions].
I started learning about 3270 data streams from [Tommy Sprinkle's tutorial][sprinkle]. The tn3270 telnet negotiation is gleaned from [RFC 1576: TN3270 Current Practices][rfc1576], [RFC 1041: Telnet 3270 Regime Option][rfc1041], and [RFC 854: Telnet Protocol Specification][rfc854]. The IANA maintains a [useful reference of telnet option numbers][telnetOptions]. The reference I use for 3270 data streams is [the 1981 version from IBM][ibmref].
[sprinkle]: http://www.tommysprinkle.com/mvs/P3270/
[rfc1576]: https://tools.ietf.org/html/rfc1576
[rfc1041]: https://tools.ietf.org/html/rfc1041
[rfc854]: https://tools.ietf.org/html/rfc854
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml
[ibmref]: https://bitsavers.org/pdf/ibm/3270/GA23-0059-0_3270_Data_Stream_Programmers_Reference_Jan1981.pdf
License
-------
+14 -2
View File
@@ -9,15 +9,21 @@ import (
)
func TestEncode(t *testing.T) {
encoded := getpos(0, 0)
encoded := getpos(0, 0, 80)
if encoded[0] != 0x40 || encoded[1] != 0x40 {
t.Error("Position (0, 0) not correctly encoded")
}
encoded = getpos(11, 39)
encoded = getpos(11, 39, 80)
if encoded[0] != 0x4e || encoded[1] != 0xd7 {
t.Error("Position (11, 39) not correctly encoded")
}
// Large screen, 14-bit addressing
encoded = getpos(100, 120, 130)
if encoded[0] != 0x33 || encoded[1] != 0x40 {
t.Errorf("Position (100, 120) on 130-col screen not correctly encoded")
}
}
func TestDecode(t *testing.T) {
@@ -30,4 +36,10 @@ func TestDecode(t *testing.T) {
if decoded != 919 {
t.Error("Buffer address incorrectly decoded")
}
// Large screen, 14-bit addressing
decoded = decodeBufAddr([2]byte{0x33, 0x40})
if decoded != 13120 {
t.Error("14-bit buffer address incorrectly decoded")
}
}
+4 -1
View File
@@ -91,7 +91,10 @@ func handle(conn net.Conn) {
defer conn.Close()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
if _, err := go3270.NegotiateTelnet(conn); err != nil {
fmt.Printf("ERROR: %v\n", err)
return
}
fieldValues := make(map[string]string)
var response go3270.Response
+4 -1
View File
@@ -90,7 +90,10 @@ func handle(conn net.Conn) {
defer conn.Close()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
if _, err := go3270.NegotiateTelnet(conn); err != nil {
fmt.Println(err)
return
}
fieldValues := make(map[string]string)
+4 -1
View File
@@ -49,7 +49,10 @@ func handle(conn net.Conn) {
defer conn.Close()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
if _, err := go3270.NegotiateTelnet(conn); err != nil {
fmt.Println(err)
return
}
// First, let's send the initial screen and wait forever for the user to
// press PF3, and when we get it, send a message on the done channel.
+7 -2
View File
@@ -75,8 +75,13 @@ func handle(conn net.Conn, db DB, gblstate *global) {
}()
// Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn)
err := go3270.RunTransactions(conn, state.login, nil)
devinfo, err := go3270.NegotiateTelnet(conn)
if err != nil {
fmt.Println(err)
return
}
err = go3270.RunTransactions(conn, devinfo, state.login, nil)
if err != nil {
fmt.Println(err)
}
+2 -1
View File
@@ -48,7 +48,8 @@ var helpScreen = go3270.Screen{
// generator function for it is a stand-alone function and not a method on the
// session.
func help(returnTransaction go3270.Tx) go3270.Tx {
return func(conn net.Conn, data any) (go3270.Tx, any, error) {
return func(conn net.Conn, _ go3270.DevInfo, data any) (
go3270.Tx, any, error) {
_, err := go3270.HandleScreen(
helpScreen, // the screen to display
nil, // (no) rules to enforce
+4 -2
View File
@@ -58,7 +58,8 @@ var loginScreenRules = go3270.Rules{
// login transaction accepts a string value in data if the login screen
// should be initialized with an error message.
func (sess *session) login(conn net.Conn, data any) (go3270.Tx, any, error) {
func (sess *session) login(conn net.Conn, _ go3270.DevInfo,
data any) (go3270.Tx, any, error) {
fieldValues := make(map[string]string)
@@ -169,7 +170,8 @@ type newuserData struct {
errmsg string
}
func (sess *session) newuser(conn net.Conn, data any) (go3270.Tx, any, error) {
func (sess *session) newuser(conn net.Conn, _ go3270.DevInfo, data any) (
go3270.Tx, any, error) {
fieldValues := make(map[string]string)
+4 -3
View File
@@ -79,7 +79,8 @@ type mainmenuData struct {
// mainmenu transaction accepts a mainmenuData struct as the data if the
// option field or error message should be populated.
func (sess *session) mainmenu(conn net.Conn, data any) (go3270.Tx, any, error) {
func (sess *session) mainmenu(conn net.Conn, _ go3270.DevInfo, data any) (
go3270.Tx, any, error) {
fieldValues := make(map[string]string)
@@ -182,8 +183,8 @@ var exampleScreen = go3270.Screen{
// exampleFeature is a transaction that will act as a placeholder for real
// application functionality. It accepts a string in the data which will
// be displayed on the panel.
func (sess *session) exampleFeature(conn net.Conn, data any) (go3270.Tx,
any, error) {
func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo,
data any) (go3270.Tx, any, error) {
fieldValues := make(map[string]string)
+124
View File
@@ -0,0 +1,124 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
package main
import (
"fmt"
"net"
"strconv"
"github.com/racingmars/go3270"
)
var biglayout = go3270.Screen{
// Column will be calculated at runtime for the following field:
{Row: 0, Intense: true, Content: "3270 Screen Size Example"},
{Row: 2, Col: 0,
Content: "This screen is using the full size that your terminal supports."},
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
{Row: 4, Col: 21, Name: "termtype", Intense: true},
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
{Row: 5, Col: 21, Name: "rows", Intense: true},
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
{Row: 6, Col: 21, Name: "cols", Intense: true},
{Row: 8, Col: 0, Content: "To visit a default sized screen, press"},
{Row: 8, Col: 39, Content: "PF1", Color: go3270.Yellow, Intense: true},
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
// a blank field for error messages
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
}
func bigscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
go3270.Tx, any, error) {
rows, cols := devinfo.AltDimensions()
termtype := devinfo.TerminalType()
// Make a local copy of the screen definition that we can append lines to.
screen := make(go3270.Screen, len(biglayout))
copy(screen, biglayout)
// Center the title on any screen width
screen[0].Col = (cols / 2) - (len(biglayout[0].Content) / 2)
// We'll start writing "data lines" at row 13 up to the penultimate row on
// the terminal
for i := 13; i < rows-1; i++ {
newfield := go3270.Field{Row: i, Col: 0,
Content: fmt.Sprintf("This is data row %d.", i-12)}
if i == rows-2 {
newfield.Content += " (The last.)"
}
screen = append(screen, newfield)
// And demonstrate that we can position to the full width, too.
newfield = go3270.Field{Row: i, Col: cols - 5, Content: "<**>"}
screen = append(screen, newfield)
}
// And an input field on the last row, to make sure field buffer address
// decoding works on larger screens.
newfield := go3270.Field{Row: rows - 1, Col: 0,
Content: "Enter data here:", Color: go3270.Pink, Intense: true}
screen = append(screen, newfield)
newfield = go3270.Field{Row: rows - 1, Col: 17, Name: "inputdata",
Write: true}
screen = append(screen, newfield)
newfield = go3270.Field{Row: rows - 1, Col: cols - 1} // "stop" field
screen = append(screen, newfield)
fieldValues := map[string]string{
"termtype": termtype,
"rows": strconv.Itoa(rows),
"cols": strconv.Itoa(cols),
}
if data != nil {
fieldValues["errormsg"] = fmt.Sprintf("You said: %s", data.(string))
}
resp, err := go3270.HandleScreenAlt(
screen, // the screen to display
nil, // (no) rules to enforce
fieldValues, // pre-populated values in fields
[]go3270.AID{ // keys we accept -- validating
go3270.AIDEnter,
},
[]go3270.AID{ // keys we accept -- non-validating
go3270.AIDPF1,
go3270.AIDPF3,
},
"errormsg", // name of field to put error messages in
rows-1, 18, // cursor coordinates
conn, // network connection
devinfo, // device info for alternate screen size support
)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDEnter:
// Re-run current transaction, echoing back input
return bigscreen, resp.Values["inputdata"], err
case go3270.AIDPF1:
// Go to default screen size transaction
return normalscreen, nil, nil
case go3270.AIDPF3:
// Exit
return nil, nil, nil
default:
// re-run current transaction
return bigscreen, nil, nil
}
}
+48
View File
@@ -0,0 +1,48 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
// Example 5 demonstrates support for larger-than-default alternate screen
// sizes in terminals that are larger than 24x80.
package main
import (
"fmt"
"net"
"github.com/racingmars/go3270"
)
func main() {
ln, err := net.Listen("tcp", ":3270")
if err != nil {
panic(err)
}
fmt.Println("LISTENING ON PORT 3270 FOR CONNECTIONS")
fmt.Println("Press Ctrl-C to end server.")
for {
conn, err := ln.Accept()
if err != nil {
panic(err)
}
go handle(conn)
}
}
// handle is the handler for individual user connections.
func handle(conn net.Conn) {
defer conn.Close()
// Always begin new connection by negotiating the telnet options
devinfo, err := go3270.NegotiateTelnet(conn)
if err != nil {
fmt.Println(err)
return
}
err = go3270.RunTransactions(conn, devinfo, bigscreen, nil)
if err != nil {
fmt.Println(err)
}
}
+103
View File
@@ -0,0 +1,103 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2025 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
package main
import (
"fmt"
"net"
"strconv"
"github.com/racingmars/go3270"
)
var normallayout = go3270.Screen{
{Row: 0, Col: 28, Intense: true, Content: "3270 Screen Size Example"},
{Row: 2, Col: 0,
Content: "This screen is using the default size that all terminals support, 24x80."},
{Row: 3, Col: 0,
Content: "But I know the following information about your particular terminal:"},
{Row: 4, Col: 0, Content: "Terminal Type . . ."},
{Row: 4, Col: 21, Name: "termtype", Intense: true},
{Row: 5, Col: 0, Content: "Rows . . . . . . . ."},
{Row: 5, Col: 21, Name: "rows", Intense: true},
{Row: 5, Col: 28, Content: "(but currently using 24)"},
{Row: 6, Col: 0, Content: "Columns . . . . . ."},
{Row: 6, Col: 21, Name: "cols", Intense: true},
{Row: 6, Col: 28, Content: "(but currently using 80)"},
{Row: 8, Col: 0, Content: "To visit a large sized screen, press"},
{Row: 8, Col: 37, Content: "PF1", Color: go3270.Yellow, Intense: true},
{Row: 9, Col: 0, Content: "To exit and disconnect, press"},
{Row: 9, Col: 30, Content: "PF3", Color: go3270.Yellow, Intense: true},
// a blank field for error messages
{Row: 11, Col: 0, Intense: true, Color: go3270.Red, Name: "errormsg"},
}
func normalscreen(conn net.Conn, devinfo go3270.DevInfo, data any) (
go3270.Tx, any, error) {
rows, cols := devinfo.AltDimensions()
termtype := devinfo.TerminalType()
// Make a local copy of the screen definition that we can append lines to.
screen := make(go3270.Screen, len(normallayout))
copy(screen, normallayout)
// We'll start writing "data lines" at row 13 up to 24
for i := 13; i < 24; i++ {
newfield := go3270.Field{Row: i, Col: 0,
Content: fmt.Sprintf("This is data row %d.", i-12)}
if i == 23 {
newfield.Content += " (The last.)"
}
screen = append(screen, newfield)
newfield = go3270.Field{Row: i, Col: 80 - 5, Content: "<**>"}
screen = append(screen, newfield)
}
fieldValues := map[string]string{
"termtype": termtype,
"rows": strconv.Itoa(rows),
"cols": strconv.Itoa(cols),
}
// We can call the old HandleScreen(), or we could have used the new
// HandleScreenAlt() and provided a nil DevInfo.
resp, err := go3270.HandleScreen(
screen, // the screen to display
nil, // (no) rules to enforce
fieldValues, // pre-populated values in fields
nil, // keys we accept -- validating
[]go3270.AID{ // keys we accept -- non-validating
go3270.AIDPF1,
go3270.AIDPF3,
},
"errormsg", // name of field to put error messages in
1, 1, // cursor coordinates
conn, // network connection
)
if err != nil {
return nil, nil, err
}
switch resp.AID {
case go3270.AIDPF1:
// Go to big screen size transaction
return bigscreen, nil, nil
case go3270.AIDPF3:
// Exit
return nil, nil, nil
default:
// re-run current transaction
return normalscreen, nil, nil
}
}
+15 -1
View File
@@ -86,9 +86,23 @@ type FieldRules struct {
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
// other cases, HandleScreen will re-present the screen to the user again,
// possibly with an error message set in the errorField field.
//
// For alternate screen support (larger than 24x80), use HandleScreenAlt().
func HandleScreen(screen Screen, rules Rules, values map[string]string,
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
conn net.Conn) (Response, error) {
return HandleScreenAlt(screen, rules, values, pfkeys, exitkeys, errorField,
crow, ccol, conn, nil)
}
// HandleScreenAlt is identical to HandleScreen, but writes to the "alternate"
// screen size provided by dev. To write a non-24-by-80 screen, use this
// HandleScreenAlt function with a non-nil dev. If dev is nil, the behavior is
// identical to HandleScreen, which is limited to 24x80 and will set larger
// terminals to the default 24x80 mode.
func HandleScreenAlt(screen Screen, rules Rules, values map[string]string,
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
conn net.Conn, dev DevInfo) (Response, error) {
// Save the original field values for any named fields to support
// the MustChange rule. Also build a map of named fields.
@@ -128,7 +142,7 @@ mainloop:
}
resp, err := ShowScreenOpts(screen, myValues, conn,
ScreenOpts{CursorRow: crow, CursorCol: ccol})
ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev})
if err != nil {
return resp, err
}
+21 -23
View File
@@ -6,9 +6,7 @@ package go3270
import (
"bytes"
"fmt"
"net"
"os"
)
// Response encapsulates data received from a 3270 client in response to the
@@ -61,9 +59,11 @@ const (
AIDPA2 AID = 0x6E
AIDPA3 AID = 0x6B
AIDClear AID = 0x6D
aidQueryResponse AID = 0x88
)
func readResponse(c net.Conn, fm fieldmap) (Response, error) {
func readResponse(c net.Conn, fm fieldmap, dev DevInfo) (Response, error) {
var r Response
aid, err := readAID(c)
if err != nil {
@@ -79,7 +79,12 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
return r, nil
}
row, col, _, err := readPosition(c)
cols := 80
if dev != nil {
_, cols = dev.altDimensions()
}
row, col, _, err := readPosition(c, cols)
if err != nil {
return r, err
}
@@ -87,7 +92,7 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
r.Row = row
var fieldValues map[string]string
if fieldValues, err = readFields(c, fm); err != nil {
if fieldValues, err = readFields(c, fm, cols); err != nil {
return r, err
}
@@ -114,7 +119,7 @@ func readAID(c net.Conn) (AID, error) {
}
}
func readPosition(c net.Conn) (row, col, addr int, err error) {
func readPosition(c net.Conn, cols int) (row, col, addr int, err error) {
raw := make([]byte, 2)
// Read two bytes
@@ -128,8 +133,8 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
// Decode the raw position
addr = decodeBufAddr([2]byte{raw[0], raw[1]})
col = addr % 80
row = (addr - col) / 80
col = addr % cols
row = (addr - col) / cols
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
addr)
@@ -137,7 +142,7 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
return row, col, addr, nil
}
func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
func readFields(c net.Conn, fm fieldmap, cols int) (map[string]string, error) {
var infield bool
var fieldpos int
var fieldval bytes.Buffer
@@ -174,7 +179,7 @@ func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
fieldval = bytes.Buffer{}
fieldpos = 0
if _, _, fieldpos, err = readPosition(c); err != nil {
if _, _, fieldpos, err = readPosition(c, cols); err != nil {
return nil, err
}
continue
@@ -203,20 +208,13 @@ func handleField(addr int, value []byte, fm fieldmap, values map[string]string)
}
// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the
// integer value of the address (i.e. 0-1919)
// integer value of the address.
func decodeBufAddr(raw [2]byte) int {
if decodes[raw[0]] > 254 {
fmt.Fprintf(os.Stderr,
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
raw[0], raw[1])
}
if decodes[raw[1]] > 254 {
fmt.Fprintf(os.Stderr,
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n",
raw[0], raw[1])
// 16-bit addressing
if raw[0]&0xc0 == 0 {
return int(raw[0])<<8 + int(raw[1])
}
hi := decodes[raw[0]] << 6
lo := decodes[raw[1]]
return hi | lo
// 12-bit addressing
return int(raw[0]&0x3f)<<6 + int(raw[1]&0x3f)
}
+71 -26
View File
@@ -98,6 +98,19 @@ 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
// NoResponse will draw the screen and immediately return, without
// waiting for any input data from the remote client.
NoResponse bool
@@ -107,12 +120,16 @@ type ScreenOpts struct {
// 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 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. Maximum value is 79.
// 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
@@ -159,13 +176,15 @@ type fieldmap map[int]string
// 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,
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.CursorCol, conn, !opts.NoClear, opts.AltScreen)
if err != nil {
return resp, err
}
@@ -178,7 +197,7 @@ func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
}
if !opts.NoResponse {
resp, err = readResponse(conn, fm)
resp, err = readResponse(conn, fm, opts.AltScreen)
if err != nil {
return resp, err
}
@@ -216,13 +235,22 @@ func ShowScreenNoResponse(screen Screen, values map[string]string,
}
func showScreenInternal(screen Screen, values map[string]string,
crow, ccol int, conn net.Conn, clear bool) (fieldmap, error) {
crow, ccol int, conn net.Conn, clear bool, dev DevInfo) (fieldmap, error) {
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 {
b.WriteByte(0xf5) // Erase/Write to terminal
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
}
@@ -237,12 +265,12 @@ func showScreenInternal(screen Screen, values map[string]string,
// 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 {
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))
b.Write(sba(fld.Row, fld.Col, cols))
b.Write(buildField(fld))
// Use fld.Content, unless the field is named and appears in the
@@ -262,7 +290,7 @@ func showScreenInternal(screen Screen, values map[string]string,
// 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
bufaddr := fld.Row*cols + fld.Col
fm[bufaddr+1] = fld.Name
}
}
@@ -271,13 +299,13 @@ func showScreenInternal(screen Screen, values map[string]string,
// caller-provided coordinates.
if clear {
// Set cursor position. Correct out-of-bounds values to 0.
if crow < 0 || crow > 23 {
if crow < 0 || crow > rows-1 {
crow = 0
}
if ccol < 0 || ccol > 79 {
if ccol < 0 || ccol > cols-1 {
ccol = 0
}
b.Write(ic(crow, ccol))
b.Write(ic(crow, ccol, cols))
}
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR
@@ -292,10 +320,10 @@ func showScreenInternal(screen Screen, values map[string]string,
}
// sba is the "set buffer address" 3270 command.
func sba(row, col int) []byte {
func sba(row, col, cols int) []byte {
result := make([]byte, 1, 3)
result[0] = 0x11 // SBA
result = append(result, getpos(row, col)...)
result = append(result, getpos(row, col, cols)...)
return result
}
@@ -372,20 +400,37 @@ func sfAttribute(write, intense, hidden, skip, numeric bool) byte {
// ic is the "insert cursor" 3270 command. This function will include the
// appropriate SBA command.
func ic(row, col int) []byte {
func ic(row, col, cols int) []byte {
result := make([]byte, 0, 3)
result = append(result, sba(row, col)...)
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 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
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)}
}
+360 -26
View File
@@ -1,48 +1,337 @@
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
// Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
// See LICENSE in the project root for license information.
package go3270
import (
"errors"
"net"
"time"
)
// DevInfo provides information about the terminal that is connected.
//
// 3270 terminals operate at a default screen size of 24 rows that are 80
// columns wide. The normal "Write/Erase" datastream command always writes to
// the default 24x80 buffer. But some terminals support more rows and/or
// columns, and the alternate sized buffer may be written to with the
// "Write/Erase Alternate" command.
type DevInfo interface {
// AltDimensions returns the number or rows and columns on the alternate
// screen size.
AltDimensions() (rows, cols int)
// TerminalType reports the terminal-provided identification string. All
// modern tn3270 clients will report one of the IBM-3278 models (-2, -3,
// -4, or -5), or IBM-DYNAMIC if the alternate screen size isn't one of
// the fixed sizes of the 3278 models. This string is purely
// informational; the actual size of the alternate screen is available
// from AltDimensions().
TerminalType() string
// Private version of AltDimensions() so callers can't fake us out; only
// real implementations returned by NegotiateTelnet() will work.
altDimensions() (rows, cols int)
}
const (
binary = 0
send = 1
se = 240 // f0
sb = 250 // fa
will = 251 // fb
wont = 252 // fc
do = 253 // fd
dont = 254 // fe
iac = 255 // ff
terminalType = 24 // 18
eoroption = 25 // 19
eor = 239 // f1
se = 240 // 0xf0
sb = 250 // 0xfa
will = 251 // 0xfb
wont = 252 // 0xfc
do = 253 // 0xfd
dont = 254 // 0xfe
iac = 255 // 0xff
// Options
binaryOption = 0
eorOption = 25 // 0x19
eor = 239 // 0xf1
terminalType = 24 // 0x18
terminalTypeIs = 0
terminalTypeSend = 1
)
// NegotiateTelnet will naively (e.g. not checking client responses) negotiate
// the options necessary for tn3270 on a new telnet connection, conn.
func NegotiateTelnet(conn net.Conn) error {
conn.Write([]byte{iac, do, terminalType})
conn.Write([]byte{iac, sb, terminalType, send, iac, se})
conn.Write([]byte{iac, do, eoroption})
conn.Write([]byte{iac, do, binary})
conn.Write([]byte{iac, will, eoroption, iac, will, binary})
flushConnection(conn, time.Second*5)
// ErrNo3270 indicates that the telnet client did not respond properly to the
// options negotiation that are expected for a tn3270 client.
var ErrNo3270 = errors.New("couldn't negotiate telnet options for tn3270")
// ErrTelnetError indicates an unexpected response was encountered in the
// telnet protocol.
var ErrTelnetError = errors.New("telnet or 3270 protocol error")
// ErrUnknownTerminal indicates the client did not identify itself as an
// IBM-3277, 3278, 3279, or IBM-DYNAMIC model. All modern tn3270 clients
// should report as IBM-3278 models or IBM-DYNAMIC.
var ErrUnknownTerminal = errors.New("unknown terminal type")
var errOptionRejected = errors.New("option rejected")
// NegotiateTelnet will negotiate the options necessary for tn3270 on a new
// telnet connection, conn.
func NegotiateTelnet(conn net.Conn) (DevInfo, error) {
// Enable terminal type option
if _, err := conn.Write([]byte{iac, do, terminalType}); err != nil {
return nil, err
}
err := checkOptionResponse(conn, terminalType, do)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Switch to the first available terminal type
conn.Write([]byte{iac, sb, terminalType, terminalTypeSend, iac, se})
devtype, err := getTerminalType(conn)
if err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Request end of record mode
conn.Write([]byte{iac, do, eorOption})
err = checkOptionResponse(conn, eorOption, do)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Request binary mode
conn.Write([]byte{iac, do, binaryOption})
err = checkOptionResponse(conn, binaryOption, do)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Enter end of record mode
conn.Write([]byte{iac, will, eorOption})
err = checkOptionResponse(conn, eorOption, will)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Enter binary mode
conn.Write([]byte{iac, will, binaryOption})
err = checkOptionResponse(conn, binaryOption, will)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
devinfo, err := makeDeviceInfo(conn, devtype)
if err != nil {
return nil, err
}
return devinfo, nil
}
// checkOptionResponse will check for the client's "will/wont" (if mode is do)
// or "do/dont" (if mode is will) response. mode is the option command the
// server just sent, and option is the option code to check for.
func checkOptionResponse(conn net.Conn, option, mode byte) error {
var buf [3]byte
var expectedYes, expectedNo byte
switch mode {
case do:
expectedYes = will
expectedNo = wont
case will:
expectedYes = do
expectedNo = dont
default:
return ErrTelnetError
}
n, err := conn.Read(buf[:])
if err != nil {
return err
}
if n < 3 || buf[0] != iac {
return ErrTelnetError
}
if buf[1] == expectedNo {
// Was the correct option rejected?
if buf[2] != option {
return ErrTelnetError
}
return errOptionRejected
}
if buf[1] != expectedYes {
return ErrTelnetError
}
// We have "will" now. But for the right option?
if buf[2] != option {
return ErrTelnetError
}
// All good, client accepted the option we requested.
return nil
}
// getTerminalType reads the response to a "send terminal type" option
// subfield command.
func getTerminalType(conn net.Conn) (string, error) {
var buf [100]byte
var termtype string
n, err := conn.Read(buf[:])
if err != nil {
return termtype, err
}
// At a minimum, with a one-character terminal type name, we expect
// 7 bytes
if n < 7 {
return termtype, ErrTelnetError
}
// We'll check the expected control bytes all in one go...
if buf[0] != iac || buf[1] != sb || buf[2] != terminalType ||
buf[3] != terminalTypeIs || buf[n-2] != iac || buf[n-1] != se {
return termtype, ErrTelnetError
}
// Everything looks good. The terminal type is an ASCII string between all
// the control/command bytes.
return string(buf[4 : n-2]), nil
}
func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) {
// Known fixed size device types. All modern tn3270 clients should
// report as 3278, but we'll also include 3277 and 3279 just in case.
switch termtype {
case "IBM-3277-2", "IBM-3277-2-E", "IBM-3278-2", "IBM-3278-2-E",
"IBM-3279-2", "IBM-3279-2-E":
return &deviceInfo{24, 80, termtype}, nil
case "IBM-3278-3", "IBM-3278-3-E", "IBM-3279-3", "IBM-3279-3-E":
return &deviceInfo{32, 80, termtype}, nil
case "IBM-3278-4", "IBM-3278-4-E":
return &deviceInfo{43, 80, termtype}, nil
case "IBM-3278-5", "IBM-3278-5-E":
return &deviceInfo{27, 132, termtype}, nil
}
// If it's not a fixed-size type, it should be IBM-DYNAMIC. If it isn't,
// we don't know how to deal with it.
if termtype != "IBM-DYNAMIC" {
return nil, ErrUnknownTerminal
}
// For IBM-DYNAMIC, we need to discover the alternate screen size with
// a structured field query.
// First, we perform an ERASE / WRITE ALTERNATE to clear the screen
// and put it in alternate screen mode. (EWA, reset WCC, telnet EOR)
if _, err := conn.Write([]byte{0x7e, 0xc3, 0xff, 0xef}); err != nil {
return nil, err
}
// Now we need to send the Write Structured Field command (0xf3) with the
// "Read Partition - Query" structured field. Note that we're
// telnet-escaping the 0xff in the data, but the subfield length is the
// *unescaped* length (7).
if _, err := conn.Write([]byte{0xf3, 0, 7, 0x01, 0xff, 0xff, 0x02,
0xff, 0xef}); err != nil {
return nil, err
}
var aid [1]byte
n, err := conn.Read(aid[:])
if err != nil {
return nil, err
}
if n != 1 || aid[0] != byte(aidQueryResponse) {
return nil, ErrTelnetError
}
var rows, cols int
// There are an arbitrary number of query reply structured fields. We
// are only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field,
// so we'll just consume any others. Consume all data until the EOR is
// received.
for {
// Two bytes are big-endian length.
buf, err := telnetReadN(conn, 2)
if err != nil {
return nil, err
}
if buf == nil {
// EOR. We're out of fields.
break
}
var l int = int(buf[0])<<8 + int(buf[1])
// Field length includes the 2 length bytes
buf, err = telnetReadN(conn, l-2)
if err != nil {
return nil, err
}
if buf == nil {
return nil, ErrTelnetError
}
// Note that because length isn't at the beginning, offsets in buf
// are 2 less than in the 3270 datastream documentation.
if !(buf[0] == 0x81 && buf[1] == 0x81) {
// Not 'Usable Area' query reply
continue
}
// A valid Usable Area reply will always include at least 18 (20 with
// length) bytes.
if l < 18 {
return nil, ErrTelnetError
}
// big-endian two byte values
cols = int(buf[4])<<8 + int(buf[5])
rows = int(buf[6])<<8 + int(buf[7])
}
if rows == 0 || cols == 0 {
// We got an IBM-DYNAMIC device type, but it didn't include a
// Usable Area query response.
return nil, ErrUnknownTerminal
}
// We support 12- and 14-bit addressing. Using 16-bit addressing would
// require a mode change and the current API design doesn't support
// tracking the state necessary for that.
//
// We'll limit the reported screen size to what fits in 14-bit addressing
// by removing rows if necessary.
for rows*cols >= 1<<14 {
rows--
}
return &deviceInfo{rows, cols, termtype}, nil
}
// UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
// to restore the telnet options state to what it was before NegotiateTelnet()
// was called.
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
conn.Write([]byte{iac, wont, eoroption, iac, wont, binary})
conn.Write([]byte{iac, dont, binary})
conn.Write([]byte{iac, dont, eoroption})
conn.Write([]byte{iac, wont, eorOption, iac, wont, binaryOption})
conn.Write([]byte{iac, dont, binaryOption})
conn.Write([]byte{iac, dont, eorOption})
conn.Write([]byte{iac, dont, terminalType})
flushConnection(conn, timeout)
return nil
@@ -138,3 +427,48 @@ func telnetRead(c net.Conn, passEOR bool) (b byte, valid, isEor bool, err error)
}
}
}
// telnetReadN reads n unescaped, valid, non-EOR characters. The returned byte
// slice will always be length n (see special case below, though), unless
// error is non-nil, in which case the byte slice will be nil. Invalid or
// early EOR will return ErrTelnetError.
//
// AS A SPECIAL CASE, if the first byte read is EOR, then the returned byte
// slice AND error will be nil.
func telnetReadN(conn net.Conn, n int) ([]byte, error) {
buf := make([]byte, n)
for i := 0; i < n; i++ {
b, valid, isEor, err := telnetRead(conn, true)
if err != nil {
return nil, err
}
if i == 0 && isEor {
// If we're still on the first byte and it's EOR, return a
// non-error nil value.
return nil, nil
}
if !valid || isEor {
return nil, ErrTelnetError
}
buf[i] = b
}
return buf, nil
}
type deviceInfo struct {
rows, cols int
termtype string
}
func (d *deviceInfo) AltDimensions() (rows, cols int) {
return d.rows, d.cols
}
func (d *deviceInfo) TerminalType() string {
return d.termtype
}
func (d *deviceInfo) altDimensions() (rows, cols int) {
return d.rows, d.cols
}
+23 -10
View File
@@ -7,30 +7,43 @@ package go3270
import "net"
// Tx is a function that serves as one transaction in a go3270 application.
// The Tx function is called with the network connection to the client, and a
// "data" value provided by the previous transaction. Tx functions return the
// next transaction to run (or nil to indicate the RunTransactions() function
// should terminate), the data to pass into the next transaction, and any
// error. If the error is non-nil, the RunTransactions() function will
// terminate and return the err. A non-nil error is _not_ passed between
// transactions, it terminates transaction processing.
type Tx func(conn net.Conn, data any) (next Tx, newdata any, err error)
// The Tx function is called with the network connection to the client, the
// DevInfo for use with alternate screen writes, and a "data" value provided
// by the previous transaction. Tx functions return the next transaction to
// run (or nil to indicate the RunTransactions() function should terminate),
// the data to pass into the next transaction, and any error. If the error is
// non-nil, the RunTransactions() function will terminate and return the err.
// A non-nil error is _not_ passed between transactions, it terminates
// transaction processing.
type Tx func(conn net.Conn, dev DevInfo, data any) (
next Tx, newdata any, err error)
// RunTransactions begins running transaction functions, starting with the
// initial transaction, until a transaction eventually returns nil for the
// next transaction, or until a transaction function returns a non-nil error
// value. data (which may be nil, if the initial transaction does not require
// data) is passed in as the data to the initial transaction.
func RunTransactions(conn net.Conn, initial Tx, data any) error {
//
// dev is the DevInfo of the connected client, as obtained from
// NegotiateTelnet(). It is safe to pass a nil DevInfo, in which case all
// transactions will only be able to operate with the default 24x80 screen
// size.
func RunTransactions(conn net.Conn, dev DevInfo, initial Tx,
data any) error {
var next Tx
var err error
next = initial
if dev == nil {
dev = &deviceInfo{rows: 24, cols: 80, termtype: "DEFAULT"}
}
// We run transactions until there isn't a next transaction to run, or
// an error.
for {
next, data, err = next(conn, data)
next, data, err = next(conn, dev, data)
if err != nil {
// Error means we bail out and return the error to the caller.
return err
+2 -19
View File
@@ -23,8 +23,8 @@ func debugf(format string, a ...interface{}) {
fmt.Fprintf(Debug, format, a...)
}
// codes are the 3270 control character I/O codes, pre-computed as provided
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm
// codes are the 3270 control character I/O codes for 12-bit addressing,
// from Figure D-1 of GA23-0059-00. (Figure C-1 in later editions.)
var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60,
@@ -32,23 +32,6 @@ var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f}
// decodes is the inverse of the above table; -1 is used in invalid positions
var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, -1, -1, -1, -1, -1,
-1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, -1, -1, -1, -1, -1, -1,
-1, -1, 42, 43, 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2,
3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, 21, 22,
23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40,
41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1,
-1, -1, -1, -1}
// AIDtoString returns a string representation of an AID key name.
func AIDtoString(aid AID) string {
switch aid {