Non-screen-clearing update support

New ShowScreenOpts() function, which takes a new ScreenOpts struct, to control
various aspects of the ShowScreen behavior. This should now be used in favor of
the deprecated ShowScreen() and ShowScreenNoResponse() functions, and allows
for the additional of future options without needing to continue making new
functions or breaking the public API.
This commit is contained in:
Matthew R. Wilson committed 2025-04-12 16:56:24 -07:00
1 parent 19c6001a38
commit 5f2d7355e4
4 files changed
+214 -50

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ Everything I know about 3270 data streams I learned from [Tommy Sprinkle's tutor
Usage
-----
See the example folders for a quick demonstration of using the library. The examples are very similar, but example1 uses the lower-level function ShowScreen(), and example2 uses a higher-level function HandleScreen().
See the example folders for a quick demonstration of using the library. example1 uses the lower-level function ShowScreen(), 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.
Here's [a video introducing the library][introVideo] as well.
+105
View File
@@ -0,0 +1,105 @@
// 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.
// This example demonstrates updating portions of the screen while waiting
// for a client response.
package main
import (
"fmt"
"net"
"time"
"github.com/racingmars/go3270"
)
var screen = go3270.Screen{
{Row: 0, Col: 35, Intense: true, Content: "3270 Clock"},
{Row: 1, Col: 0, Color: go3270.White,
Content: "------------------------------------------------------------------------------"},
{Row: 5, Col: 5, Color: go3270.Turquoise, Content: "The current UTC time is:"},
{Row: 5, Col: 30, Color: go3270.Yellow, Intense: true, Content: "XX:XX:XX"},
{Row: 22, Col: 0, Content: "PF3 Exit"},
}
var refresh = go3270.Screen{
screen[3], // Copy the time field that we want to update
}
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
go3270.NegotiateTelnet(conn)
// 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.
done := func() chan bool {
done := make(chan bool)
// This will run in a goroutine so it can block waiting for the
// response, while the rest of the code below (which refreshes the
// screen every second) can continue to run.
go func() {
// Loop forever, sending the background screen until user exits.
for {
screen[3].Content = time.Now().UTC().Format("15:04:05")
response, err := go3270.ShowScreenOpts(screen, nil, conn,
go3270.ScreenOpts{CursorRow: 23, CursorCol: 0})
if err != nil {
// User dropped connection, maybe? We'll end things.
done <- true
return
}
if response.AID == go3270.AIDPF3 {
// User wants to quit.
done <- true
return
}
}
}()
return done
}()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-done:
// User pressed PF3 (or we need to quit for some other reason)
return
case <-ticker.C:
// Send the updated time, without clearing the screen
refresh[0].Content = time.Now().UTC().Format("15:04:05")
_, err := go3270.ShowScreenOpts(refresh, nil, conn,
go3270.ScreenOpts{NoClear: true, NoResponse: true})
if err != nil {
// Bail out
return
}
}
}
}
+14 -13
View File
@@ -69,18 +69,18 @@ type FieldRules struct {
// HandleScreen will loop until all validation rules are satisfied, and only
// return when an expected AID (i.e. PF) key is pressed.
//
// - screen is the Screen to display (see ShowScreen()).
// - rules are the Rules to enforce: each key in the Rules map corresponds to
// a Field.Name in the screen array.
// - values are field values you wish to override (see ShowScreen()).
// - pfkeys and exitkeys are the AID keys that you wish to accept (that is,
// perform validation and return if successful) and treat as exit keys
// (unconditionally return).
// - errorField is the name of a field in the screen array that you wish error
// messages to be written in when HandleScreen loops waiting for a valid
// user submission.
// - crow and ccol are the initial cursor position.
// - conn is the network connection to the 3270 client.
// - screen is the Screen to display (see ShowScreen()).
// - rules are the Rules to enforce: each key in the Rules map corresponds to
// a Field.Name in the screen array.
// - values are field values you wish to override (see ShowScreen()).
// - pfkeys and exitkeys are the AID keys that you wish to accept (that is,
// perform validation and return if successful) and treat as exit keys
// (unconditionally return).
// - errorField is the name of a field in the screen array that you wish error
// messages to be written in when HandleScreen loops waiting for a valid
// user submission.
// - crow and ccol are the initial cursor position.
// - conn is the network connection to the 3270 client.
//
// HandleScreen will return when the user: 1) presses a key in pfkeys AND all
// fields pass validation, OR 2) the user presses a key in exitkeys. In all
@@ -127,7 +127,8 @@ mainloop:
}
}
resp, err := ShowScreen(screen, myValues, crow, ccol, conn)
resp, err := ShowScreenOpts(screen, myValues, conn,
ScreenOpts{CursorRow: crow, CursorCol: ccol})
if err != nil {
return resp, err
}
+94 -36
View File
@@ -95,64 +95,118 @@ const (
// names,
type Screen []Field
// ScreenOpts are the options that callers may set when sending a screen
// to the 3270 client.
type ScreenOpts struct {
// NoResponse will draw the screen and immediately return, without
// waiting for any input data from the remote client.
NoResponse bool
// NoClear will send the data stream to the remote client without
// clearing the screen first. Existing data will be overlayed with
// the current screen.
NoClear bool
// CursorRow sets the row (0-indexed) to position the cursor after
// sending the screen, when NoClear is false. Maximum value is 23.
CursorRow int
// CursorCol sets the column (0-indexed) to position the cursor after
// sending the screen, when NoClear is false. Maximum value is 79.
CursorCol int
}
// fieldmap is a map of field buffer addresses and the corresponding field
// name.
type fieldmap map[int]string
// ShowScreen writes the 3270 datastream for the screen to a connection.
// ShowScreenOpts writes the 3270 datastream for the screen, with the provided
// ScreenOpts, to a connection.
//
// Fields that aren't valid (e.g. outside of the 24x80 screen) are silently
// ignored. If a named field has an entry in the values map, the content of
// the field from the values map is used INSTEAD OF the Field struct's Content
// field. The values map may be nil if no overrides are needed. After writing
// the fields, the cursor is set to crow, ccol, which are 0-based positions:
// row 0-23 and col 0-79. Errors from conn.Write() are returned if
// encountered. ShowScreen will wait for the client to provide data before
// returning, and the data from the client will be returned as a Response.
func ShowScreen(screen Screen, values map[string]string, crow, ccol int,
conn net.Conn) (Response, error) {
// field. The values map may be nil if no overrides are needed.
//
// If opts.NoClear is false, the client screen will be cleared before writing
// the new screen, and the cursor will be repositioned to the values in
// opts.CursorRow and opts.CursorCol. If opts.NoClear is true, the screen will
// NOT be cleared, the cursor will NOT be repositioned, and the new screen
// will be overlayed over the current state of the client screen.
//
// If opts.NoResponse is false, ShowScreenOpts will block before returning,
// waiting for data from the client and returning the Response. If
// opts.NoResponse is true, ShowScreenOpts will immediately return after
// sending the datastream and the Response will be empty.
//
// If using from multiple threads -- one to block and wait for a response, and
// another to send screens with NoResponse and/or NoClear, be aware that if
// you change the input fields on screen after the initial blocking call is
// made, the response fields will not line up correctly and end up being
// invalid. That is to say, while waiting for a response, don't perform other
// actions from another thread that could layout the user input fields
// differently.
func ShowScreenOpts(screen Screen, values map[string]string, conn net.Conn,
opts ScreenOpts) (Response, error) {
fm, err := showScreenInternal(screen, values, crow, ccol, conn)
var resp Response
fm, err := showScreenInternal(screen, values, opts.CursorRow,
opts.CursorCol, conn, !opts.NoClear)
if err != nil {
return Response{}, err
return resp, err
}
response, err := readResponse(conn, fm)
if err != nil {
return response, err
}
if !opts.NoResponse {
resp, err = readResponse(conn, fm)
if err != nil {
return resp, err
}
// Strip trailing spaces from field values. Most applications will want to
// strip leading spaces, too, but it's possible they want them preserved,
// so we'll leave that to the caller.
for _, fld := range screen {
if !fld.KeepSpaces {
if _, ok := response.Values[fld.Name]; ok {
response.Values[fld.Name] =
strings.TrimRight(response.Values[fld.Name], " ")
// Strip trailing spaces from field values. Most applications will
// want to strip leading spaces, too, but it's possible they want them
// preserved, so we'll leave that to the caller.
for _, fld := range screen {
if !fld.KeepSpaces {
if _, ok := resp.Values[fld.Name]; ok {
resp.Values[fld.Name] =
strings.TrimRight(resp.Values[fld.Name], " ")
}
}
}
}
return response, nil
return resp, nil
}
// ShowScreenNoResponse writes the screen to the connection, just like
// ShowScreen(), but immediately returns instead of waiting for data
// from the client.
// Deprecated: use ShowScreenOpts with default/empty ScreenOpts.
func ShowScreen(screen Screen, values map[string]string, crow, ccol int,
conn net.Conn) (Response, error) {
return ShowScreenOpts(screen, values, conn,
ScreenOpts{CursorRow: crow, CursorCol: ccol})
}
// Deprecated: use ShowScreenOpts with ScreenOpts.NoResponse = true.
func ShowScreenNoResponse(screen Screen, values map[string]string,
crow, ccol int, conn net.Conn) error {
_, err := showScreenInternal(screen, values, crow, ccol, conn)
_, err := ShowScreenOpts(screen, values, conn,
ScreenOpts{NoResponse: true, CursorRow: crow, CursorCol: ccol})
return err
}
func showScreenInternal(screen Screen, values map[string]string,
crow, ccol int, conn net.Conn) (fieldmap, error) {
crow, ccol int, conn net.Conn, clear bool) (fieldmap, error) {
var b bytes.Buffer
var fm = make(fieldmap) // field buffer positions -> name
b.WriteByte(0xf5) // Erase/Write to terminal
if clear {
b.WriteByte(0xf5) // Erase/Write to terminal
} else {
b.WriteByte(0xf1) // Write to terminal
}
b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT
// Build the commands for each field on the screen
@@ -187,14 +241,18 @@ func showScreenInternal(screen Screen, values map[string]string,
}
}
// Set cursor position. Correct out-of-bounds values to 0.
if crow < 0 || crow > 23 {
crow = 0
// If we cleared the screen, set the cursor position to the
// caller-provided coordinates.
if clear {
// Set cursor position. Correct out-of-bounds values to 0.
if crow < 0 || crow > 23 {
crow = 0
}
if ccol < 0 || ccol > 79 {
ccol = 0
}
b.Write(ic(crow, ccol))
}
if ccol < 0 || ccol > 79 {
ccol = 0
}
b.Write(ic(crow, ccol))
b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR