/* Package door: a go implementation of a BBS door for linux and Windows that uses door32.sys, understand CP437 and unicode (if available), detects screen size, and supports TheDraw Fonts. import ( "door" ) int main() { d = door.Door{} d.Init() // Process commandline switches, initialize door, detect screen size. d.Write("Welcome to my awesome door, written in "+door.ColorText("BLINK BOLD WHITE")+"go"+door.Reset+"."+door.CRNL) d.Write("Press a key...") d.Key() d.Write(door.CRNL) } */ package door import ( "bytes" "flag" "fmt" "io" "log" "os" "path/filepath" "runtime/debug" "sync" "time" "golang.org/x/term" ) // debugging output - enable here const DEBUG_DOOR bool = false // For debugging input reader routines. const DEBUG_INPUT bool = false const DEBUG_OUTPUT bool = false // See door_test.go for DEBUG test const // Right now these are strings. Should they be []byte ? const SavePos = "\x1b[s" // Save Cursor Position const RestorePos = "\x1b[u" // Restore Cursor Position // const CRNL = "\r\n" // BBS Line Ending const CRNL = "\n" const Clrscr = "\x1b[0m\x1b[2J\x1b[H" // Clear screen, home cursor const HideCursor = "\x1b[?25l" // Hide Cursor const ShowCursor = "\x1b[?25h" // Show Cursor const Reset string = "\x1b[0m" // ANSI Color Reset const CURSOR_POS = "\x1b[6n" const SAVE_POS = "\x1b[s" const RESTORE_POS = "\x1b[u" // Move these into the door structure, instead of having globals. var Unicode bool // Unicode support detected var CP437 bool // CP437 support detected var Full_CP437 bool // Full CP437 support detected (handles control codes properly) var Height int // Screen height detected var Width int // Screen width detected var Inactivity time.Duration = time.Duration(120) * time.Second // Inactivity timeout type ColorRender func(*bytes.Buffer, []byte) type Updater func(*bytes.Buffer) type UpdaterI interface { Update() bool } type OutputI interface { Output() []byte } type CursorPos struct { X, Y int } type Mouse struct { Button uint8 X uint8 Y uint8 } type MouseMode int16 const ( Off MouseMode = 0 X10 MouseMode = 9 Normal MouseMode = 1000 Button MouseMode = 1002 AnyEvent MouseMode = 1003 ) type ReaderData struct { R rune Ex Extended Err error } /* type noCopy struct{} func (*noCopy) Lock() {} func (*noCopy) Unlock() {} noCopy noCopy */ type BaseWriter struct { Closed bool TranslateToUnicode bool uniBuffer *bytes.Buffer ansiCSI bool ansiCode []byte LastSavedColor []int } type Door struct { Config DropfileConfig READFD int WRITEFD int Writer OSWriter // OS specific writer writeMutex sync.Mutex // Writer lock nlBuffer *bytes.Buffer // NewLines Translate buffer TranslateNL bool // Translate NewLines? ansiCSI bool // ANSI CSI ansiCode []byte // ANSI CSI Codes Disconnected bool // Has User disconnected/Hung up? TimeOut time.Time // Fixed point in time, when time expires StartTime time.Time // Time when User started door Pushback FIFOBuffer[rune] // Key buffer LastColor []int // Track the last color sent for restore color ReaderClosed bool // Reader close readerChannel chan ReaderData // Reading from the User readerMutex sync.Mutex // Reader close mutex ReaderEnd bool // Reader should end LastMouse []Mouse // Store Mouse information LastCursor []CursorPos // Store Cursor pos information mcMutex sync.Mutex // Lock for LastMouse, LastCursor wg sync.WaitGroup // Reader finished tio_default *term.State // Terminal State to restore Mouse MouseMode // Mouse mode enabled } /* Enable mouse support 9 : X10 Support 1000: Normal 1002: Button Event 1003: Any-Event */ func (d *Door) EnableMouse(mode MouseMode) { if d.Mouse != Off { // Disable current mode first d.DisableMouse() } d.Mouse = mode if d.Mouse != Off { var output []byte output = fmt.Appendf(output, "\x1b[?%dh", int(d.Mouse)) d.Write(output) // d.Write(fmt.Sprintf("\x1b[?%dh", int(d.Mouse))) } } // Disable mouse support func (d *Door) DisableMouse() { if d.Mouse != Off { var output []byte output = fmt.Appendf(output, "\x1b[?%dl", int(d.Mouse)) d.Write(output) // d.Write(fmt.Sprintf("\x1b[?%dl", int(d.Mouse))) } d.Mouse = Off } func (d *Door) AddMouse(mouse Mouse) { d.mcMutex.Lock() defer d.mcMutex.Unlock() d.LastMouse = append(d.LastMouse, mouse) } var WantGetMouse bool func (d *Door) GetMouse() (Mouse, bool) { if WantGetMouse { WantGetMouse = false } else { log.Println("GetMouse called, but WantGetMouse not true.") } d.mcMutex.Lock() defer d.mcMutex.Unlock() return ArrayDelete(&d.LastMouse, 0) } func (d *Door) GetCursorPos() (CursorPos, bool) { d.mcMutex.Lock() if DEBUG_DOOR { log.Printf("LastCursor %p/%p %d, %d\n", d, &d.LastCursor, len(d.LastCursor), cap(d.LastCursor)) } defer d.mcMutex.Unlock() if DEBUG_DOOR { log.Printf("LastCursor: %#v\n", d.LastCursor) } return ArrayDelete(&d.LastCursor, 0) } func (d *Door) AddCursorPos(cursor CursorPos) { d.mcMutex.Lock() if DEBUG_DOOR { log.Printf("LastCursor %p/%p %d, %d\n", d, &d.LastCursor, len(d.LastCursor), cap(d.LastCursor)) } defer d.mcMutex.Unlock() d.LastCursor = append(d.LastCursor, cursor) if DEBUG_DOOR { log.Printf("LastCursor now %d, %d\n", len(d.LastCursor), cap(d.LastCursor)) log.Printf("AddCursor: %#v\n", d.LastCursor) } } func (d *Door) ClearMouseCursor() { d.mcMutex.Lock() defer d.mcMutex.Unlock() if DEBUG_DOOR { log.Println("ClearMouseCursor") } d.LastMouse = make([]Mouse, 0, 2) d.LastCursor = make([]CursorPos, 0, 3) } // Return the amount of time left as time.Duration func (d *Door) TimeLeft() time.Duration { return time.Until(d.TimeOut) } func (d *Door) TimeUsed() time.Duration { return time.Since(d.StartTime) } func (d *Door) Disconnect() bool { return d.Disconnected // atomic.LoadInt32(&d.Disconnected) != 0 } func (d *Door) Detect() bool { // detect is destructive ... make it non-destructive // destructive: clears/trashes the screen. var detect bytes.Buffer detect.WriteString("\r\x03\x04" + CURSOR_POS + "\b \b\b \b" + "\r\u2615" + CURSOR_POS + "\b \b\b \b\b \b" + SAVE_POS + "\x1b[999C\x1b[999B" + CURSOR_POS + RESTORE_POS) // hot beverage is 3 bytes long -- need 3 "\b \b" to erase. d.Write(detect.Bytes()) var info []CursorPos = make([]CursorPos, 0, 3) var done bool for !done { _, ex, err := d.WaitKey(time.Second) log.Println("WaitKey:", ex, err) if ex == CURSOR { cursor, ok := d.GetCursorPos() if ok { info = append(info, cursor) if len(info) == 3 { done = true } } } if err != nil { done = true } } if len(info) != 3 { // Detection FAILED. log.Println("Detect FAILED:", info) return false } // Ok! Let's see what we've got... var valid bool // Where did I get these numbers from? // linux term (telnet/ssh) [0].X = 1, [1].X = 3 // VS Code terminal: 1, 3 // https://docs.python.org/3/library/unicodedata.html // \u2615 is a fullwidth (2 char) unicode symbol! // SO, [1].X == 3 // and not 2. // syncterm [0].X = 3, [1].X = 4 // Magiterm [0].X = 3, [1].X = 4 // cp437 + telnet [0].X = 1, [1].X = 4 FullCP437 = False // ^ Fails FullCP437 test - characters codes ignored. // cp437plus + telnet 3, 4. (Has FullCP437 support.) // if (info[0].X == 1 || info[0].X == 3) && // (info[1].X == 2 || info[1].X == 3) { // Breakdown by detected type: // Unicode \x03 \x04 (control codes ignored) // Unicode \u2615 = fullwidth takes up 2 characters // CP437 \x03 \x04 Hearts Diamonds Symbols 2 characters // ^ Only works for FullCP437 // CP437 \u2615 = b'\xe2\x98\x95' 3 bytes // So info[1].X = 4 if info[0].X == 1 && info[1].X == 3 { Unicode = true valid = true } else { // info[1].X = 4 Unicode = false CP437 = true valid = true } if info[0].X == 3 { Full_CP437 = true } if !valid { log.Println("Detect FAILED (not valid):", info) return false } Width, Height = info[2].X, info[2].Y // Putty doesn't seem to restoring the cursor position (when detecting). d.Write(Goto(1, info[0].Y)) log.Printf("Unicode: %t, CP437: %t, Full %t, Screen %v\n", Unicode, CP437, Full_CP437, info[2]) return true } // Initialize door framework. Parse commandline, read dropfile, // detect terminal capabilities. func (d *Door) Init(doorname string) { var dropfile string d.Pushback = NewFIFOBuffer[rune](5) // Get path to binary, and chdir to it. var binaryPath string binaryPath, _ = os.Executable() binaryPath = filepath.Dir(binaryPath) _ = os.Chdir(binaryPath) flag.StringVar(&dropfile, "d", "", "Path to dropfile") flag.Parse() if len(dropfile) == 0 { flag.PrintDefaults() os.Exit(2) } d.ReadDropfile(dropfile) if d.Config.Comm_type == 0 { // RAW MODE d.tio_default, _ = term.MakeRaw(d.READFD) } // Logfile will be doorname - node # var logfilename string = fmt.Sprintf("%s-%d.log", doorname, d.Config.Node) var logf *os.File var err error logf, err = os.OpenFile(logfilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { log.Panicf("Error creating log file %s: %v", logfilename, err) } // Set logging output to logfilename. log.SetOutput(logf) log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) log.Printf("Loading dropfile %s\n", dropfile) log.Printf("BBS %s, User %s / Handle %s / File %d\n", d.Config.BBSID, d.Config.Real_name, d.Config.Handle, d.Config.Comm_handle) d.readerChannel = make(chan ReaderData, 16) // was 8 ? d.ClearMouseCursor() d.setupChannels() d.Writer.Init(d) d.Detect() if Unicode { BOXES = &BOXES_UNICODE BARS = &BARS_UNICODE } else { BOXES = &BOXES_CP437 BARS = &BARS_CP437 } } func (d *Door) Close() { defer func() { if err := recover(); err != nil { log.Println("door.Close FAILURE:", err) // This displays stack trace stderr debug.PrintStack() } }() d.DisableMouse() // This disables anymore Writes. d.Writer.Stop() /* d.writerMutex.Lock() d.WriterClosed = true d.writerMutex.Unlock() */ // I need a way to shutdown the reader. // This isn't shutting it down, we hang on d.wg.Wait() d.readerMutex.Lock() // d.ReaderClosed = true d.ReaderEnd = true d.readerMutex.Unlock() // log.Println("Closing...") // close(d.writerChannel) log.Println("wg.Wait()") d.wg.Wait() log.Println("Closed.") if d.Config.Comm_type == 0 { // Linux - restore console settings to default/original. term.Restore(d.READFD, d.tio_default) } } // Goto X, Y - Position the cursor using ANSI Escape Codes // // Example: // // d.Write(door.Goto(1, 5) + "Now at X=1, Y=5.") func Goto(x int, y int) []byte { var output []byte output = fmt.Appendf(output, "\x1b[%d;%dH", y, x) return output } // No change in benchmarks using this ... func GotoW(x, y int, w io.Writer) { fmt.Fprintf(w, "\x1b[%d;%dH", y, x) } // Why doesn't func Goto2 work? I get wrong answers / tests fail. // If this is called from a go routine .. while another goto is being // done... hell might brake loose. var gotobuff *bytes.Buffer func Goto2(x int, y int) []byte { if gotobuff == nil { gotobuff = &bytes.Buffer{} } gotobuff.Reset() fmt.Fprintf(gotobuff, "\x1b[%d;%dH", y, x) return gotobuff.Bytes() } func GotoS(x int, y int) string { return fmt.Sprintf("\x1b[%d;%dH", y, x) } func (d *Door) setupChannels() { d.wg.Add(1) go Reader(d) }