diff options
author | bptato <nincsnevem662@gmail.com> | 2024-03-18 21:27:07 +0100 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-03-18 21:27:07 +0100 |
commit | 44451ed4505c4a38d8763ad4736aeaacbaeef4de (patch) | |
tree | 190af134cb7a4e132810673bd67999e9fb736400 /src | |
parent | 50ee9b03e320628b73d511c7d5ae217b07f00cce (diff) | |
download | chawan-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.nim | 10 | ||||
-rw-r--r-- | src/local/client.nim | 140 | ||||
-rw-r--r-- | src/local/pager.nim | 16 | ||||
-rw-r--r-- | src/local/term.nim | 154 | ||||
-rw-r--r-- | src/main.nim | 6 |
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 |