about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-18 21:27:07 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-18 21:27:07 +0100
commit44451ed4505c4a38d8763ad4736aeaacbaeef4de (patch)
tree190af134cb7a4e132810673bd67999e9fb736400 /src
parent50ee9b03e320628b73d511c7d5ae217b07f00cce (diff)
downloadchawan-44451ed4505c4a38d8763ad4736aeaacbaeef4de.tar.gz
client: refactor input
* move mouse handling to term
* do not use File for input just to disable buffering anyway
Diffstat (limited to 'src')
-rw-r--r--src/io/posixstream.nim10
-rw-r--r--src/local/client.nim140
-rw-r--r--src/local/pager.nim16
-rw-r--r--src/local/term.nim154
-rw-r--r--src/main.nim6
5 files changed, 159 insertions, 167 deletions
diff --git a/src/io/posixstream.nim b/src/io/posixstream.nim
index 911f384b..0b06c572 100644
--- a/src/io/posixstream.nim
+++ b/src/io/posixstream.nim
@@ -43,6 +43,16 @@ method recvData*(s: PosixStream, buffer: pointer, len: int): int =
     s.isend = true
   return n
 
+proc sreadChar*(s: PosixStream): char =
+  let n = read(s.fd, addr result, 1)
+  if n < 0:
+    raisePosixIOError()
+  if n == 0:
+    if unlikely(s.isend):
+      raise newException(EOFError, "eof")
+    s.isend = true
+  assert n == 1
+
 proc recvData*(s: PosixStream, buffer: var openArray[uint8]): int {.inline.} =
   return s.recvData(addr buffer[0], buffer.len)
 
diff --git a/src/local/client.nim b/src/local/client.nim
index 8b16339f..7c5fabb9 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -61,7 +61,6 @@ type
     consoleWrapper: ConsoleWrapper
     fdmap: Table[int, Container]
     feednext: bool
-    ibuf: string
     jsctx: JSContext
     jsrt: JSRuntime
     pager {.jsget.}: Pager
@@ -87,15 +86,8 @@ template loader(client: Client): FileLoader =
 template forkserver(client: Client): ForkServer =
   client.pager.forkserver
 
-proc readChar(client: Client): char =
-  if client.ibuf == "":
-    try:
-      return client.pager.infile.readChar()
-    except EOFError:
-      quit(1)
-  else:
-    result = client.ibuf[0]
-    client.ibuf.delete(0..0)
+template readChar(client: Client): char =
+  client.pager.term.readChar()
 
 proc finalize(client: Client) {.jsfin.} =
   if client.jsctx != nil:
@@ -110,14 +102,15 @@ proc fetch[T: Request|string](client: Client, req: T,
 
 proc interruptHandler(rt: JSRuntime, opaque: pointer): cint {.cdecl.} =
   let client = cast[Client](opaque)
-  if client.console == nil or client.pager.infile == nil: return
+  if client.console == nil or client.pager.term.istream == nil:
+    return 0
   try:
-    let c = client.pager.infile.readChar()
+    let c = client.pager.term.istream.sreadChar()
     if c == char(3): #C-c
-      client.ibuf = ""
+      client.pager.term.ibuf = ""
       return 1
     else:
-      client.ibuf &= c
+      client.pager.term.ibuf &= c
   except IOError:
     discard
   return 0
@@ -212,83 +205,6 @@ proc evalAction(client: Client, action: string, arg0: int32): EmptyPromise =
   JS_FreeValue(ctx, ret)
   return p
 
-type
-  MouseInputType = enum
-    mitPress = "press", mitRelease = "release", mitMove = "move"
-
-  MouseInputMod = enum
-    mimShift = "shift", mimCtrl = "ctrl", mimMeta = "meta"
-
-  MouseInputButton = enum
-    mibLeft = (1, "left")
-    mibMiddle = (2, "middle")
-    mibRight = (3, "right")
-    mibWheelUp = (4, "wheelUp")
-    mibWheelDown = (5, "wheelDown")
-    mibWheelLeft = (6, "wheelLeft")
-    mibWheelRight = (7, "wheelRight")
-    mibThumbInner = (8, "thumbInner")
-    mibThumbTip = (9, "thumbTip")
-    mibButton10 = (10, "button10")
-    mibButton11 = (11, "button11")
-
-  MouseInput = object
-    t: MouseInputType
-    button: MouseInputButton
-    mods: set[MouseInputMod]
-    col: int
-    row: int
-
-proc parseMouseInput(client: Client): Opt[MouseInput] =
-  template fail =
-    return err()
-  var btn = 0
-  while (let c = client.readChar(); c != ';'):
-    let n = decValue(c)
-    if n == -1:
-      fail
-    btn *= 10
-    btn += n
-  var mods: set[MouseInputMod] = {}
-  if (btn and 4) != 0:
-    mods.incl(mimShift)
-  if (btn and 8) != 0:
-    mods.incl(mimCtrl)
-  if (btn and 16) != 0:
-    mods.incl(mimMeta)
-  var px = 0
-  while (let c = client.readChar(); c != ';'):
-    let n = decValue(c)
-    if n == -1:
-      fail
-    px *= 10
-    px += n
-  var py = 0
-  var c: char
-  while (c = client.readChar(); c notin {'m', 'M'}):
-    let n = decValue(c)
-    if n == -1:
-      fail
-    py *= 10
-    py += n
-  var t = if c == 'M': mitPress else: mitRelease
-  if (btn and 32) != 0:
-    t = mitMove
-  var button = (btn and 3) + 1
-  if (btn and 64) != 0:
-    button += 3
-  if (btn and 128) != 0:
-    button += 7
-  if button notin int(MouseInputButton.low)..int(MouseInputButton.high):
-    return err()
-  ok(MouseInput(
-    t: t,
-    mods: mods,
-    button: MouseInputButton(button),
-    col: px - 1,
-    row: py - 1
-  ))
-
 # The maximum number we are willing to accept.
 # This should be fine for 32-bit signed ints (which precnum currently is).
 # We can always increase it further (e.g. by switching to uint32, uint64...) if
@@ -315,7 +231,7 @@ proc handleCommandInput(client: Client, c: char): EmptyPromise =
     return p
   if client.config.input.use_mouse:
     if client.pager.inputBuffer == "\e[<":
-      let input = client.parseMouseInput()
+      let input = client.pager.term.parseMouseInput()
       if input.isSome:
         let input = input.get
         let container = client.pager.container
@@ -516,11 +432,8 @@ proc acceptBuffers(client: Client) =
     pager.handleEvents(container)
   pager.procmap.setLen(0)
 
-proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
-  importc: "setvbuf", header: "<stdio.h>", tags: [].}
-
 proc handleRead(client: Client; fd: int) =
-  if client.pager.infile != nil and fd == client.pager.infile.getFileHandle():
+  if client.pager.term.istream != nil and fd == client.pager.term.istream.fd:
     client.input().then(proc() =
       client.handlePagerEvents()
     )
@@ -583,7 +496,7 @@ proc flushConsole*(client: Client) {.jsfunc.} =
   client.handleRead(client.forkserver.estream.fd)
 
 proc handleError(client: Client, fd: int) =
-  if client.pager.infile != nil and fd == client.pager.infile.getFileHandle():
+  if client.pager.term.istream != nil and fd == client.pager.term.istream.fd:
     #TODO do something here...
     stderr.write("Error in tty\n")
     quit(1)
@@ -616,8 +529,7 @@ proc handleError(client: Client, fd: int) =
 
 proc inputLoop(client: Client) =
   let selector = client.selector
-  discard c_setvbuf(client.pager.infile, nil, IONBF, 0)
-  selector.registerHandle(int(client.pager.infile.getFileHandle()), {Read}, 0)
+  selector.registerHandle(int(client.pager.term.istream.fd), {Read}, 0)
   let sigwinch = selector.registerSignal(int(SIGWINCH), 0)
   while true:
     let events = client.selector.select(-1)
@@ -775,18 +687,19 @@ proc dumpBuffers(client: Client) =
   stdout.close()
 
 proc launchClient*(client: Client; pages: seq[string];
-    contentType: Option[string]; cs: Charset; dump: bool;
-    warnings: seq[string]) =
-  var infile: File
+    contentType: Option[string]; cs: Charset; dump: bool) =
+  var istream: PosixStream
   var dump = dump
   if not dump:
     if stdin.isatty():
-      infile = stdin
-    if stdout.isatty():
-      if infile == nil:
-        dump = not open(infile, "/dev/tty", fmRead)
-    else:
-      dump = true
+      istream = newPosixStream(stdin.getFileHandle())
+    if istream == nil:
+      if stdout.isatty():
+        istream = newPosixStream("/dev/tty", O_RDONLY, 0)
+        if istream == nil:
+          dump = true
+      else:
+        dump = true
   let selector = newSelector[int]()
   let efd = int(client.forkserver.estream.fd)
   selector.registerHandle(efd, {Read}, 0)
@@ -794,15 +707,14 @@ proc launchClient*(client: Client; pages: seq[string];
     selector.registerHandle(fd, {Read}, 0)
   client.loader.unregisterFun = proc(fd: int) =
     selector.unregister(fd)
-  client.pager.launchPager(infile, selector)
-  client.pager.alerts.add(warnings)
+  client.pager.launchPager(istream, selector)
   let clearFun = proc() =
     client.clearConsole()
   let showFun = proc() =
     client.showConsole()
   let hideFun = proc() =
     client.hideConsole()
-  client.consoleWrapper = addConsole(client.pager, interactive = infile != nil,
+  client.consoleWrapper = client.pager.addConsole(interactive = istream != nil,
     clearFun, showFun, hideFun)
   #TODO passing console.err here makes it impossible to change it later. maybe
   # better associate it with jsctx
@@ -883,12 +795,12 @@ proc addJSModules(client: Client, ctx: JSContext) =
 func getClient(client: Client): Client {.jsfget: "client".} =
   return client
 
-proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext):
-    Client =
+proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext;
+    warnings: seq[string]): Client =
   setControlCHook(proc() {.noconv.} = quit(1))
   let jsrt = JS_GetRuntime(jsctx)
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
-  let pager = newPager(config, forkserver, jsctx)
+  let pager = newPager(config, forkserver, jsctx, warnings)
   let loader = forkserver.newFileLoader(LoaderConfig(
     urimethodmap: config.external.urimethodmap,
     w3mCGICompat: config.external.w3m_cgi_compat,
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 7c986eff..627a7b08 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -271,14 +271,15 @@ proc quit*(pager: Pager, code = 0) =
   pager.term.quit()
   pager.dumpAlerts()
 
-proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext): Pager =
-  let pager = Pager(
+proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext;
+    alerts: seq[string]): Pager =
+  return Pager(
     config: config,
     forkserver: forkserver,
     proxy: config.getProxy(),
-    term: newTerminal(stdout, config)
+    term: newTerminal(stdout, config),
+    alerts: alerts
   )
-  return pager
 
 proc genClientKey(pager: Pager): ClientKey =
   var key: ClientKey
@@ -303,18 +304,15 @@ proc setLoader*(pager: Pager, loader: FileLoader) =
   )
   loader.key = pager.addLoaderClient(pager.loader.clientPid, config)
 
-proc launchPager*(pager: Pager; infile: File; selector: Selector[int]) =
+proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) =
   pager.selector = selector
-  case pager.term.start(infile)
+  case pager.term.start(istream)
   of tsrSuccess: discard
   of tsrDA1Fail:
     pager.alert("Failed to query DA1, please set display.query-da1 = false")
   pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1)
   pager.statusgrid = newFixedGrid(pager.attrs.width)
 
-func infile*(pager: Pager): File =
-  return pager.term.infile
-
 proc clearDisplay(pager: Pager) =
   pager.display = newFixedGrid(pager.display.width, pager.display.height)
 
diff --git a/src/local/term.nim b/src/local/term.nim
index ffba3f1f..996a8508 100644
--- a/src/local/term.nim
+++ b/src/local/term.nim
@@ -1,7 +1,6 @@
 import std/options
 import std/os
 import std/posix
-import std/streams
 import std/strutils
 import std/tables
 import std/termios
@@ -9,6 +8,7 @@ import std/unicode
 
 import bindings/termcap
 import config/config
+import io/posixstream
 import types/cell
 import types/color
 import types/opt
@@ -51,7 +51,7 @@ type
   TerminalObj = object
     cs*: Charset
     config: Config
-    infile*: File
+    istream*: PosixStream
     outfile: File
     cleared: bool
     canvas: FixedGrid
@@ -63,12 +63,12 @@ type
     tc: Termcap
     tname: string
     set_title: bool
-    stdin_unblocked: bool
-    orig_flags: cint
-    orig_flags2: cint
+    stdinUnblocked: bool
+    stdinWasUnblocked: bool
     orig_termios: Termios
     defaultBackground: RGBColor
     defaultForeground: RGBColor
+    ibuf*: string # buffer for chars when we can't process them
 
 # control sequence introducer
 template CSI(s: varargs[string, `$`]): string =
@@ -146,6 +146,13 @@ else:
   proc write(term: Terminal, s: string) =
     term.write(cstring(s))
 
+proc readChar*(term: Terminal): char =
+  if term.ibuf.len == 0:
+    result = term.istream.sreadChar()
+  else:
+    result = term.ibuf[0]
+    term.ibuf.delete(0..0)
+
 template SGR*(s: varargs[string, `$`]): string =
   CSI(s) & "m"
 
@@ -190,17 +197,17 @@ proc clearDisplay(term: Terminal): string =
   else:
     return ED()
 
-proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "<unistd.h>".}
-proc isatty*(f: File): bool =
-  return isatty(f.getFileHandle()) != 0
+proc isatty*(file: File): bool =
+  return file.getFileHandle().isatty() != 0
 
 proc isatty*(term: Terminal): bool =
-  term.infile != nil and term.infile.isatty() and term.outfile.isatty()
+  return term.istream != nil and term.istream.fd.isatty() != 0 and
+    term.outfile.isatty()
 
 proc anyKey*(term: Terminal; msg = "[Hit any key]") =
   if term.isatty():
     term.outfile.write(term.clearEnd() & msg)
-    discard term.infile.readChar()
+    discard term.istream.sreadChar()
 
 proc resetFormat(term: Terminal): string =
   when termcap_found:
@@ -588,32 +595,26 @@ proc clearCanvas*(term: Terminal) =
 
 # see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
 proc disableRawMode(term: Terminal) =
-  let fd = term.infile.getFileHandle()
-  discard tcSetAttr(fd, TCSAFLUSH, addr term.orig_termios)
+  discard tcSetAttr(term.istream.fd, TCSAFLUSH, addr term.orig_termios)
 
 proc enableRawMode(term: Terminal) =
-  let fd = term.infile.getFileHandle()
-  discard tcGetAttr(fd, addr term.orig_termios)
+  discard tcGetAttr(term.istream.fd, addr term.orig_termios)
   var raw = term.orig_termios
   raw.c_iflag = raw.c_iflag and not (BRKINT or ICRNL or INPCK or ISTRIP or IXON)
   raw.c_oflag = raw.c_oflag and not (OPOST)
   raw.c_cflag = raw.c_cflag or CS8
   raw.c_lflag = raw.c_lflag and not (ECHO or ICANON or ISIG or IEXTEN)
-  discard tcSetAttr(fd, TCSAFLUSH, addr raw)
+  discard tcSetAttr(term.istream.fd, TCSAFLUSH, addr raw)
 
 proc unblockStdin*(term: Terminal) =
   if term.isatty():
-    let fd = term.infile.getFileHandle()
-    term.orig_flags = fcntl(fd, F_GETFL, 0)
-    let flags = term.orig_flags or O_NONBLOCK
-    discard fcntl(fd, F_SETFL, flags)
-    term.stdin_unblocked = true
+    term.istream.setBlocking(false)
+    term.stdinUnblocked = true
 
 proc restoreStdin*(term: Terminal) =
-  if term.stdin_unblocked:
-    let fd = term.infile.getFileHandle()
-    discard fcntl(fd, F_SETFL, term.orig_flags)
-    term.stdin_unblocked = false
+  if term.stdinUnblocked:
+    term.istream.setBlocking(true)
+    term.stdinUnblocked = false
 
 proc quit*(term: Terminal) =
   if term.isatty():
@@ -627,13 +628,9 @@ proc quit*(term: Terminal) =
       term.disableMouse()
     term.showCursor()
     term.cleared = false
-    if term.stdin_unblocked:
-      let fd = term.infile.getFileHandle()
-      term.orig_flags2 = fcntl(fd, F_GETFL, 0)
-      discard fcntl(fd, F_SETFL, term.orig_flags2 and (not O_NONBLOCK))
-      term.stdin_unblocked = false
-    else:
-      term.orig_flags2 = -1
+    if term.stdinUnblocked:
+      term.restoreStdin()
+      term.stdinWasUnblocked = true
   term.flush()
 
 when termcap_found:
@@ -679,9 +676,10 @@ proc queryAttrs(term: Terminal, windowOnly: bool): QueryResult =
       GEOMCELL &
       DA1
     term.outfile.write(outs)
+  term.flush()
   result = QueryResult(success: false, attrs: {})
   while true:
-    template consume(term: Terminal): char = term.infile.readChar()
+    template consume(term: Terminal): char = term.readChar()
     template fail = return
     template expect(term: Terminal, c: char) =
       if term.consume != c:
@@ -799,9 +797,8 @@ proc detectTermAttributes(term: Terminal, windowOnly: bool): TermStartResult =
     term.tname = "dosansi"
   if not term.isatty():
     return
-  let fd = term.infile.getFileHandle()
   var win: IOctl_WinSize
-  if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
+  if ioctl(term.istream.fd, TIOCGWINSZ, addr win) != -1:
     term.attrs.width = int(win.ws_col)
     term.attrs.height = int(win.ws_row)
     term.attrs.ppc = int(win.ws_xpixel) div term.attrs.width
@@ -857,14 +854,91 @@ proc detectTermAttributes(term: Terminal, windowOnly: bool): TermStartResult =
     term.smcup = true
     term.formatmode = {low(FormatFlags)..high(FormatFlags)}
 
+type
+  MouseInputType* = enum
+    mitPress = "press", mitRelease = "release", mitMove = "move"
+
+  MouseInputMod* = enum
+    mimShift = "shift", mimCtrl = "ctrl", mimMeta = "meta"
+
+  MouseInputButton* = enum
+    mibLeft = (1, "left")
+    mibMiddle = (2, "middle")
+    mibRight = (3, "right")
+    mibWheelUp = (4, "wheelUp")
+    mibWheelDown = (5, "wheelDown")
+    mibWheelLeft = (6, "wheelLeft")
+    mibWheelRight = (7, "wheelRight")
+    mibThumbInner = (8, "thumbInner")
+    mibThumbTip = (9, "thumbTip")
+    mibButton10 = (10, "button10")
+    mibButton11 = (11, "button11")
+
+  MouseInput* = object
+    t*: MouseInputType
+    button*: MouseInputButton
+    mods*: set[MouseInputMod]
+    col*: int
+    row*: int
+
+proc parseMouseInput*(term: Terminal): Opt[MouseInput] =
+  template fail =
+    return err()
+  var btn = 0
+  while (let c = term.readChar(); c != ';'):
+    let n = decValue(c)
+    if n == -1:
+      fail
+    btn *= 10
+    btn += n
+  var mods: set[MouseInputMod] = {}
+  if (btn and 4) != 0:
+    mods.incl(mimShift)
+  if (btn and 8) != 0:
+    mods.incl(mimCtrl)
+  if (btn and 16) != 0:
+    mods.incl(mimMeta)
+  var px = 0
+  while (let c = term.readChar(); c != ';'):
+    let n = decValue(c)
+    if n == -1:
+      fail
+    px *= 10
+    px += n
+  var py = 0
+  var c: char
+  while (c = term.readChar(); c notin {'m', 'M'}):
+    let n = decValue(c)
+    if n == -1:
+      fail
+    py *= 10
+    py += n
+  var t = if c == 'M': mitPress else: mitRelease
+  if (btn and 32) != 0:
+    t = mitMove
+  var button = (btn and 3) + 1
+  if (btn and 64) != 0:
+    button += 3
+  if (btn and 128) != 0:
+    button += 7
+  if button notin int(MouseInputButton.low)..int(MouseInputButton.high):
+    return err()
+  ok(MouseInput(
+    t: t,
+    mods: mods,
+    button: MouseInputButton(button),
+    col: px - 1,
+    row: py - 1
+  ))
+
 proc windowChange*(term: Terminal) =
   discard term.detectTermAttributes(windowOnly = true)
   term.applyConfigDimensions()
   term.canvas = newFixedGrid(term.attrs.width, term.attrs.height)
   term.cleared = false
 
-proc start*(term: Terminal, infile: File): TermStartResult =
-  term.infile = infile
+proc start*(term: Terminal; istream: PosixStream): TermStartResult =
+  term.istream = istream
   if term.isatty():
     term.enableRawMode()
   result = term.detectTermAttributes(windowOnly = false)
@@ -880,11 +954,9 @@ proc start*(term: Terminal, infile: File): TermStartResult =
 proc restart*(term: Terminal) =
   if term.isatty():
     term.enableRawMode()
-    if term.orig_flags2 != -1:
-      let fd = term.infile.getFileHandle()
-      discard fcntl(fd, F_SETFL, term.orig_flags2)
-      term.orig_flags2 = 0
-      term.stdin_unblocked = true
+    if term.stdinWasUnblocked:
+      term.unblockStdin()
+      term.stdinWasUnblocked = false
     if term.config.input.use_mouse:
       term.enableMouse()
   if term.smcup:
diff --git a/src/main.nim b/src/main.nim
index 51b1219f..f730de5e 100644
--- a/src/main.nim
+++ b/src/main.nim
@@ -201,15 +201,15 @@ Options:
         if www != "": pages.add(www)
 
   if pages.len == 0 and not config.start.headless:
-    if stdin.isatty:
+    if stdin.isatty():
       help(1)
 
   forks.loadForkServerConfig(config)
   SocketDirectory = config.external.tmpdir
 
-  let c = newClient(config, forks, jsctx)
+  let c = newClient(config, forks, jsctx, warnings)
   try:
-    c.launchClient(pages, ctype, cs, dump, warnings)
+    c.launchClient(pages, ctype, cs, dump)
   except CatchableError:
     c.flushConsole()
     raise