chore: ⬆️ updated deps
This commit is contained in:
+270
@@ -0,0 +1,270 @@
|
||||
//go:build plan9
|
||||
// +build plan9
|
||||
|
||||
// Copyright 2025 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
// You may obtain a copy of the license at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tcell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// p9Tty implements tcell.Tty using Plan 9's /dev/cons and /dev/consctl.
|
||||
// Raw mode is enabled by writing "rawon" to /dev/consctl while the fd stays open.
|
||||
// Resize notifications are read from /dev/wctl: the first read returns geometry,
|
||||
// subsequent reads block until the window changes (rio(4)).
|
||||
//
|
||||
// References:
|
||||
// - kbdfs(8): cons/consctl rawon|rawoff semantics
|
||||
// - rio(4): wctl geometry and blocking-on-change behavior
|
||||
// - vt(1): VT100 emulator typically used for TUI programs on Plan 9
|
||||
//
|
||||
// Limitations:
|
||||
// - We assume VT100-level capabilities (often no colors, no mouse).
|
||||
// - Window size is conservative: we return 80x24 unless overridden.
|
||||
// Set LINES/COLUMNS (or TCELL_LINES/TCELL_COLS) to refine.
|
||||
// - Mouse and bracketed paste are not wired; terminfo/xterm queries
|
||||
// are not attempted because vt(1) may not support them.
|
||||
type p9Tty struct {
|
||||
cons *os.File // /dev/cons (read+write)
|
||||
consctl *os.File // /dev/consctl (write "rawon"/"rawoff")
|
||||
wctl *os.File // /dev/wctl (resize notifications)
|
||||
|
||||
// protect close/stop; Read/Write are serialized by os.File
|
||||
mu sync.Mutex
|
||||
closed atomic.Bool
|
||||
|
||||
// resize callback
|
||||
onResize atomic.Value // func()
|
||||
wg sync.WaitGroup
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func NewDevTty() (Tty, error) { // tcell signature
|
||||
return newPlan9TTY()
|
||||
}
|
||||
|
||||
func NewStdIoTty() (Tty, error) { // also required by tcell
|
||||
// On Plan 9 there is no POSIX tty discipline on stdin/stdout;
|
||||
// use /dev/cons explicitly for robustness.
|
||||
return newPlan9TTY()
|
||||
}
|
||||
|
||||
func NewDevTtyFromDev(_ string) (Tty, error) { // required by tcell
|
||||
// Plan 9 does not have multiple "ttys" in the POSIX sense;
|
||||
// always bind to /dev/cons and /dev/consctl.
|
||||
return newPlan9TTY()
|
||||
}
|
||||
|
||||
func newPlan9TTY() (Tty, error) {
|
||||
cons, err := os.OpenFile("/dev/cons", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open /dev/cons: %w", err)
|
||||
}
|
||||
consctl, err := os.OpenFile("/dev/consctl", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
_ = cons.Close()
|
||||
return nil, fmt.Errorf("open /dev/consctl: %w", err)
|
||||
}
|
||||
// /dev/wctl may not exist (console without rio); best-effort.
|
||||
wctl, _ := os.OpenFile("/dev/wctl", os.O_RDWR, 0)
|
||||
|
||||
t := &p9Tty{
|
||||
cons: cons,
|
||||
consctl: consctl,
|
||||
wctl: wctl,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *p9Tty) Start() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.closed.Load() {
|
||||
return errors.New("tty closed")
|
||||
}
|
||||
|
||||
// Recreate stop channel if absent or closed (supports resume).
|
||||
if t.stopCh == nil || isClosed(t.stopCh) {
|
||||
t.stopCh = make(chan struct{})
|
||||
}
|
||||
|
||||
// Put console into raw mode; remains active while consctl is open.
|
||||
if _, err := t.consctl.Write([]byte("rawon")); err != nil {
|
||||
return fmt.Errorf("enable raw mode: %w", err)
|
||||
}
|
||||
|
||||
// Reopen /dev/wctl on resume; best-effort (system console may lack it).
|
||||
if t.wctl == nil {
|
||||
if f, err := os.OpenFile("/dev/wctl", os.O_RDWR, 0); err == nil {
|
||||
t.wctl = f
|
||||
}
|
||||
}
|
||||
|
||||
if t.wctl != nil {
|
||||
t.wg.Add(1)
|
||||
go t.watchResize()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *p9Tty) Drain() error {
|
||||
// Per tcell docs, this may reasonably be a no-op on non-POSIX ttys.
|
||||
// Read deadlines are not available on plan9 os.File; we rely on Stop().
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *p9Tty) Stop() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
// Signal watcher to stop (if not already).
|
||||
if t.stopCh != nil && !isClosed(t.stopCh) {
|
||||
close(t.stopCh)
|
||||
}
|
||||
|
||||
// Exit raw mode first.
|
||||
_, _ = t.consctl.Write([]byte("rawoff"))
|
||||
|
||||
// Closing wctl unblocks watchResize; nil it so Start() can reopen later.
|
||||
if t.wctl != nil {
|
||||
_ = t.wctl.Close()
|
||||
t.wctl = nil
|
||||
}
|
||||
|
||||
// Ensure watcher goroutine has exited before returning.
|
||||
t.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *p9Tty) Close() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.closed.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if t.stopCh != nil && !isClosed(t.stopCh) {
|
||||
close(t.stopCh)
|
||||
}
|
||||
_, _ = t.consctl.Write([]byte("rawoff"))
|
||||
|
||||
_ = t.cons.Close()
|
||||
_ = t.consctl.Close()
|
||||
if t.wctl != nil {
|
||||
_ = t.wctl.Close()
|
||||
t.wctl = nil
|
||||
}
|
||||
|
||||
t.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *p9Tty) Read(p []byte) (int, error) {
|
||||
return t.cons.Read(p)
|
||||
}
|
||||
|
||||
func (t *p9Tty) Write(p []byte) (int, error) {
|
||||
return t.cons.Write(p)
|
||||
}
|
||||
|
||||
func (t *p9Tty) NotifyResize(cb func()) {
|
||||
if cb == nil {
|
||||
t.onResize.Store((func())(nil))
|
||||
return
|
||||
}
|
||||
t.onResize.Store(cb)
|
||||
}
|
||||
|
||||
func (t *p9Tty) WindowSize() (WindowSize, error) {
|
||||
// Strategy:
|
||||
// 1) honor explicit overrides (TCELL_LINES/TCELL_COLS, LINES/COLUMNS),
|
||||
// 2) otherwise return conservative 80x24.
|
||||
// Reading /dev/wctl gives pixel geometry, but char cell metrics are
|
||||
// not generally available to non-draw clients; vt(1) is fixed-cell.
|
||||
lines, cols := envInt("TCELL_LINES"), envInt("TCELL_COLS")
|
||||
if lines == 0 {
|
||||
lines = envInt("LINES")
|
||||
}
|
||||
if cols == 0 {
|
||||
cols = envInt("COLUMNS")
|
||||
}
|
||||
if lines <= 0 {
|
||||
lines = 24
|
||||
}
|
||||
if cols <= 0 {
|
||||
cols = 80
|
||||
}
|
||||
return WindowSize{Width: cols, Height: lines}, nil
|
||||
}
|
||||
|
||||
// watchResize blocks on /dev/wctl reads; each read returns when the window
|
||||
// changes size/position/state, per rio(4). We ignore the parsed geometry and
|
||||
// just notify tcell to re-query WindowSize().
|
||||
func (t *p9Tty) watchResize() {
|
||||
defer t.wg.Done()
|
||||
|
||||
r := bufio.NewReader(t.wctl)
|
||||
for {
|
||||
select {
|
||||
case <-t.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
// Each read delivers something like:
|
||||
// " minx miny maxx maxy visible current\n"
|
||||
// We don't need to parse here; just signal.
|
||||
_, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
// transient errors: continue
|
||||
}
|
||||
if cb, _ := t.onResize.Load().(func()); cb != nil {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func envInt(name string) int {
|
||||
if s := strings.TrimSpace(os.Getenv(name)); s != "" {
|
||||
if v, err := strconv.Atoi(s); err == nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// helper: safe check if a channel is closed
|
||||
func isClosed(ch <-chan struct{}) bool {
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user