diff --git a/README.md b/README.md index ad079f3..64338e6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/example3/example3.go b/example3/example3.go new file mode 100644 index 0000000..046caa3 --- /dev/null +++ b/example3/example3.go @@ -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 + } + } + } +} diff --git a/looper.go b/looper.go index 485486b..765283a 100644 --- a/looper.go +++ b/looper.go @@ -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 } diff --git a/screen.go b/screen.go index 4d3d2e5..4beaa33 100644 --- a/screen.go +++ b/screen.go @@ -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