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 example2/example2
example3/example3 example3/example3
example4/example4 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 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. **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 [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 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/ [sprinkle]: http://www.tommysprinkle.com/mvs/P3270/
[rfc1576]: https://tools.ietf.org/html/rfc1576 [rfc1576]: https://tools.ietf.org/html/rfc1576
[rfc1041]: https://tools.ietf.org/html/rfc1041 [rfc1041]: https://tools.ietf.org/html/rfc1041
[rfc854]: https://tools.ietf.org/html/rfc854 [rfc854]: https://tools.ietf.org/html/rfc854
[telnetOptions]: https://www.iana.org/assignments/telnet-options/telnet-options.xhtml [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 License
------- -------
+14 -2
View File
@@ -9,15 +9,21 @@ import (
) )
func TestEncode(t *testing.T) { func TestEncode(t *testing.T) {
encoded := getpos(0, 0) encoded := getpos(0, 0, 80)
if encoded[0] != 0x40 || encoded[1] != 0x40 { if encoded[0] != 0x40 || encoded[1] != 0x40 {
t.Error("Position (0, 0) not correctly encoded") t.Error("Position (0, 0) not correctly encoded")
} }
encoded = getpos(11, 39) encoded = getpos(11, 39, 80)
if encoded[0] != 0x4e || encoded[1] != 0xd7 { if encoded[0] != 0x4e || encoded[1] != 0xd7 {
t.Error("Position (11, 39) not correctly encoded") 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) { func TestDecode(t *testing.T) {
@@ -30,4 +36,10 @@ func TestDecode(t *testing.T) {
if decoded != 919 { if decoded != 919 {
t.Error("Buffer address incorrectly decoded") 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() defer conn.Close()
// Always begin new connection by negotiating the telnet options // 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) fieldValues := make(map[string]string)
var response go3270.Response var response go3270.Response
+4 -1
View File
@@ -90,7 +90,10 @@ func handle(conn net.Conn) {
defer conn.Close() defer conn.Close()
// Always begin new connection by negotiating the telnet options // 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) fieldValues := make(map[string]string)
+4 -1
View File
@@ -49,7 +49,10 @@ func handle(conn net.Conn) {
defer conn.Close() defer conn.Close()
// Always begin new connection by negotiating the telnet options // 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 // 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. // 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 // Always begin new connection by negotiating the telnet options
go3270.NegotiateTelnet(conn) devinfo, err := go3270.NegotiateTelnet(conn)
err := go3270.RunTransactions(conn, state.login, nil) if err != nil {
fmt.Println(err)
return
}
err = go3270.RunTransactions(conn, devinfo, state.login, nil)
if err != nil { if err != nil {
fmt.Println(err) 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 // generator function for it is a stand-alone function and not a method on the
// session. // session.
func help(returnTransaction go3270.Tx) go3270.Tx { 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( _, err := go3270.HandleScreen(
helpScreen, // the screen to display helpScreen, // the screen to display
nil, // (no) rules to enforce 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 // login transaction accepts a string value in data if the login screen
// should be initialized with an error message. // 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) fieldValues := make(map[string]string)
@@ -169,7 +170,8 @@ type newuserData struct {
errmsg string 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) 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 // mainmenu transaction accepts a mainmenuData struct as the data if the
// option field or error message should be populated. // 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) 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 // exampleFeature is a transaction that will act as a placeholder for real
// application functionality. It accepts a string in the data which will // application functionality. It accepts a string in the data which will
// be displayed on the panel. // be displayed on the panel.
func (sess *session) exampleFeature(conn net.Conn, data any) (go3270.Tx, func (sess *session) exampleFeature(conn net.Conn, _ go3270.DevInfo,
any, error) { data any) (go3270.Tx, any, error) {
fieldValues := make(map[string]string) 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 // 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, // other cases, HandleScreen will re-present the screen to the user again,
// possibly with an error message set in the errorField field. // 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, func HandleScreen(screen Screen, rules Rules, values map[string]string,
pfkeys, exitkeys []AID, errorField string, crow, ccol int, pfkeys, exitkeys []AID, errorField string, crow, ccol int,
conn net.Conn) (Response, error) { 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 // Save the original field values for any named fields to support
// the MustChange rule. Also build a map of named fields. // the MustChange rule. Also build a map of named fields.
@@ -128,7 +142,7 @@ mainloop:
} }
resp, err := ShowScreenOpts(screen, myValues, conn, resp, err := ShowScreenOpts(screen, myValues, conn,
ScreenOpts{CursorRow: crow, CursorCol: ccol}) ScreenOpts{CursorRow: crow, CursorCol: ccol, AltScreen: dev})
if err != nil { if err != nil {
return resp, err return resp, err
} }
+21 -23
View File
@@ -6,9 +6,7 @@ package go3270
import ( import (
"bytes" "bytes"
"fmt"
"net" "net"
"os"
) )
// Response encapsulates data received from a 3270 client in response to the // Response encapsulates data received from a 3270 client in response to the
@@ -61,9 +59,11 @@ const (
AIDPA2 AID = 0x6E AIDPA2 AID = 0x6E
AIDPA3 AID = 0x6B AIDPA3 AID = 0x6B
AIDClear AID = 0x6D 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 var r Response
aid, err := readAID(c) aid, err := readAID(c)
if err != nil { if err != nil {
@@ -79,7 +79,12 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
return r, nil 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 { if err != nil {
return r, err return r, err
} }
@@ -87,7 +92,7 @@ func readResponse(c net.Conn, fm fieldmap) (Response, error) {
r.Row = row r.Row = row
var fieldValues map[string]string 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 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) raw := make([]byte, 2)
// Read two bytes // Read two bytes
@@ -128,8 +133,8 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
// Decode the raw position // Decode the raw position
addr = decodeBufAddr([2]byte{raw[0], raw[1]}) addr = decodeBufAddr([2]byte{raw[0], raw[1]})
col = addr % 80 col = addr % cols
row = (addr - col) / 80 row = (addr - col) / cols
debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1], debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1],
addr) addr)
@@ -137,7 +142,7 @@ func readPosition(c net.Conn) (row, col, addr int, err error) {
return row, col, addr, nil 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 infield bool
var fieldpos int var fieldpos int
var fieldval bytes.Buffer var fieldval bytes.Buffer
@@ -174,7 +179,7 @@ func readFields(c net.Conn, fm fieldmap) (map[string]string, error) {
fieldval = bytes.Buffer{} fieldval = bytes.Buffer{}
fieldpos = 0 fieldpos = 0
if _, _, fieldpos, err = readPosition(c); err != nil { if _, _, fieldpos, err = readPosition(c, cols); err != nil {
return nil, err return nil, err
} }
continue 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 // 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 { func decodeBufAddr(raw [2]byte) int {
if decodes[raw[0]] > 254 { // 16-bit addressing
fmt.Fprintf(os.Stderr, if raw[0]&0xc0 == 0 {
"UNEXPECTED VALUE: decodeBufAddr got raw value of %02x %02x\n", return int(raw[0])<<8 + int(raw[1])
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])
} }
hi := decodes[raw[0]] << 6 // 12-bit addressing
lo := decodes[raw[1]] return int(raw[0]&0x3f)<<6 + int(raw[1]&0x3f)
return hi | lo
} }
+71 -26
View File
@@ -98,6 +98,19 @@ type Screen []Field
// ScreenOpts are the options that callers may set when sending a screen // ScreenOpts are the options that callers may set when sending a screen
// to the 3270 client. // to the 3270 client.
type ScreenOpts struct { 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 // NoResponse will draw the screen and immediately return, without
// waiting for any input data from the remote client. // waiting for any input data from the remote client.
NoResponse bool NoResponse bool
@@ -107,12 +120,16 @@ type ScreenOpts struct {
// the current screen. // the current screen.
NoClear bool NoClear bool
// CursorRow sets the row (0-indexed) to position the cursor after // CursorRow sets the row (0-indexed) to position the cursor after sending
// sending the screen, when NoClear is false. Maximum value is 23. // 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 CursorRow int
// CursorCol sets the column (0-indexed) to position the cursor after // 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 CursorCol int
// PostSendCallback is a function that, if non-nil, will be called after // 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 // 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 // actions from another thread that could layout the user input fields
// differently. // 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) { opts ScreenOpts) (Response, error) {
var resp Response var resp Response
fm, err := showScreenInternal(screen, values, opts.CursorRow, fm, err := showScreenInternal(screen, values, opts.CursorRow,
opts.CursorCol, conn, !opts.NoClear) opts.CursorCol, conn, !opts.NoClear, opts.AltScreen)
if err != nil { if err != nil {
return resp, err return resp, err
} }
@@ -178,7 +197,7 @@ func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
} }
if !opts.NoResponse { if !opts.NoResponse {
resp, err = readResponse(conn, fm) resp, err = readResponse(conn, fm, opts.AltScreen)
if err != nil { if err != nil {
return resp, err return resp, err
} }
@@ -216,13 +235,22 @@ func ShowScreenNoResponse(screen Screen, values map[string]string,
} }
func showScreenInternal(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 b bytes.Buffer
var fm = make(fieldmap) // field buffer positions -> name var fm = make(fieldmap) // field buffer positions -> name
if clear { 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 { } else {
b.WriteByte(0xf1) // Write to terminal 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 // Build the commands for each field on the screen
for _, fld := range 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 // Invalid field position
continue continue
} }
b.Write(sba(fld.Row, fld.Col)) b.Write(sba(fld.Row, fld.Col, cols))
b.Write(buildField(fld)) b.Write(buildField(fld))
// Use fld.Content, unless the field is named and appears in the // 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, // because we get the position of the field's first input position,
// not the position of the field attribute byte). // not the position of the field attribute byte).
if fld.Write { if fld.Write {
bufaddr := fld.Row*80 + fld.Col bufaddr := fld.Row*cols + fld.Col
fm[bufaddr+1] = fld.Name fm[bufaddr+1] = fld.Name
} }
} }
@@ -271,13 +299,13 @@ func showScreenInternal(screen Screen, values map[string]string,
// caller-provided coordinates. // caller-provided coordinates.
if clear { if clear {
// Set cursor position. Correct out-of-bounds values to 0. // Set cursor position. Correct out-of-bounds values to 0.
if crow < 0 || crow > 23 { if crow < 0 || crow > rows-1 {
crow = 0 crow = 0
} }
if ccol < 0 || ccol > 79 { if ccol < 0 || ccol > cols-1 {
ccol = 0 ccol = 0
} }
b.Write(ic(crow, ccol)) b.Write(ic(crow, ccol, cols))
} }
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR 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. // 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 := make([]byte, 1, 3)
result[0] = 0x11 // SBA result[0] = 0x11 // SBA
result = append(result, getpos(row, col)...) result = append(result, getpos(row, col, cols)...)
return result 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 // ic is the "insert cursor" 3270 command. This function will include the
// appropriate SBA command. // appropriate SBA command.
func ic(row, col int) []byte { func ic(row, col, cols int) []byte {
result := make([]byte, 0, 3) result := make([]byte, 0, 3)
result = append(result, sba(row, col)...) result = append(result, sba(row, col, cols)...)
result = append(result, 0x13) // IC result = append(result, 0x13) // IC
return result return result
} }
// getpos translates row and col to buffer address control characters. // getpos translates row and col to buffer address control characters.
func getpos(row, col int) []byte { func getpos(row, col, cols int) []byte {
result := make([]byte, 2) address := row*cols + col
address := row*80 + col
hi := (address & 0xfc0) >> 6 // Use 12-bit addressing if the buffer address fits in 12 bits
lo := address & 0x3f if address < 1<<12 {
result[0] = codes[hi] hi := (address & 0xfc0) >> 6
result[1] = codes[lo] lo := address & 0x3f
return result 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/ // This file is part of https://github.com/racingmars/go3270/
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See // Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
// LICENSE in the project root for license information. // See LICENSE in the project root for license information.
package go3270 package go3270
import ( import (
"errors"
"net" "net"
"time" "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 ( const (
binary = 0 se = 240 // 0xf0
send = 1 sb = 250 // 0xfa
se = 240 // f0 will = 251 // 0xfb
sb = 250 // fa wont = 252 // 0xfc
will = 251 // fb do = 253 // 0xfd
wont = 252 // fc dont = 254 // 0xfe
do = 253 // fd iac = 255 // 0xff
dont = 254 // fe
iac = 255 // ff // Options
terminalType = 24 // 18
eoroption = 25 // 19 binaryOption = 0
eor = 239 // f1
eorOption = 25 // 0x19
eor = 239 // 0xf1
terminalType = 24 // 0x18
terminalTypeIs = 0
terminalTypeSend = 1
) )
// NegotiateTelnet will naively (e.g. not checking client responses) negotiate // ErrNo3270 indicates that the telnet client did not respond properly to the
// the options necessary for tn3270 on a new telnet connection, conn. // options negotiation that are expected for a tn3270 client.
func NegotiateTelnet(conn net.Conn) error { var ErrNo3270 = errors.New("couldn't negotiate telnet options for tn3270")
conn.Write([]byte{iac, do, terminalType})
conn.Write([]byte{iac, sb, terminalType, send, iac, se}) // ErrTelnetError indicates an unexpected response was encountered in the
conn.Write([]byte{iac, do, eoroption}) // telnet protocol.
conn.Write([]byte{iac, do, binary}) var ErrTelnetError = errors.New("telnet or 3270 protocol error")
conn.Write([]byte{iac, will, eoroption, iac, will, binary})
flushConnection(conn, time.Second*5) // 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 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 // UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
// to restore the telnet options state to what it was before NegotiateTelnet() // to restore the telnet options state to what it was before NegotiateTelnet()
// was called. // was called.
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error { func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
conn.Write([]byte{iac, wont, eoroption, iac, wont, binary}) conn.Write([]byte{iac, wont, eorOption, iac, wont, binaryOption})
conn.Write([]byte{iac, dont, binary}) conn.Write([]byte{iac, dont, binaryOption})
conn.Write([]byte{iac, dont, eoroption}) conn.Write([]byte{iac, dont, eorOption})
conn.Write([]byte{iac, dont, terminalType}) conn.Write([]byte{iac, dont, terminalType})
flushConnection(conn, timeout) flushConnection(conn, timeout)
return nil 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" import "net"
// Tx is a function that serves as one transaction in a go3270 application. // 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 // The Tx function is called with the network connection to the client, the
// "data" value provided by the previous transaction. Tx functions return the // DevInfo for use with alternate screen writes, and a "data" value provided
// next transaction to run (or nil to indicate the RunTransactions() function // by the previous transaction. Tx functions return the next transaction to
// should terminate), the data to pass into the next transaction, and any // run (or nil to indicate the RunTransactions() function should terminate),
// error. If the error is non-nil, the RunTransactions() function will // the data to pass into the next transaction, and any error. If the error is
// terminate and return the err. A non-nil error is _not_ passed between // non-nil, the RunTransactions() function will terminate and return the err.
// transactions, it terminates transaction processing. // A non-nil error is _not_ passed between transactions, it terminates
type Tx func(conn net.Conn, data any) (next Tx, newdata any, err error) // 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 // RunTransactions begins running transaction functions, starting with the
// initial transaction, until a transaction eventually returns nil for the // initial transaction, until a transaction eventually returns nil for the
// next transaction, or until a transaction function returns a non-nil error // 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 // value. data (which may be nil, if the initial transaction does not require
// data) is passed in as the data to the initial transaction. // 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 next Tx
var err error var err error
next = initial 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 // We run transactions until there isn't a next transaction to run, or
// an error. // an error.
for { for {
next, data, err = next(conn, data) next, data, err = next(conn, dev, data)
if err != nil { if err != nil {
// Error means we bail out and return the error to the caller. // Error means we bail out and return the error to the caller.
return err return err
+2 -19
View File
@@ -23,8 +23,8 @@ func debugf(format string, a ...interface{}) {
fmt.Fprintf(Debug, format, a...) fmt.Fprintf(Debug, format, a...)
} }
// codes are the 3270 control character I/O codes, pre-computed as provided // codes are the 3270 control character I/O codes for 12-bit addressing,
// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm // 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, var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8,
0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4, 0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4,
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 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, 0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f} 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. // AIDtoString returns a string representation of an AID key name.
func AIDtoString(aid AID) string { func AIDtoString(aid AID) string {
switch aid { switch aid {