diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | doc/config.md | 7 | ||||
-rw-r--r-- | res/config.toml | 1 | ||||
-rw-r--r-- | src/config/config.nim | 1 | ||||
-rw-r--r-- | src/display/term.nim | 35 | ||||
-rw-r--r-- | src/local/client.nim | 146 | ||||
-rw-r--r-- | src/local/container.nim | 34 | ||||
-rw-r--r-- | src/local/pager.nim | 8 |
8 files changed, 202 insertions, 31 deletions
diff --git a/README.md b/README.md index 7bbf4e5d..2ad9afe6 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Currently implemented features are: * can load user-defined protocols/file formats using [local CGI](doc/localcgi.md), [urimethodmap](doc/urimethodmap.md) and [mailcap](doc/mailcap.md) * man page viewer (based on w3mman) +* mouse support ...with a lot more [planned](todo). diff --git a/doc/config.md b/doc/config.md index 64ed6a44..d4a4f46a 100644 --- a/doc/config.md +++ b/doc/config.md @@ -250,6 +250,13 @@ numeric prefix as their first argument.<br> Note: this only applies for keybindings defined in [page].</td> </tr> +<tr> +<td>use-mouse</td> +<td>boolean</td> +<td>Whether Chawan is allowed to use the mouse.<br> +Currently, the default behavior imitates that of w3m.</td> +</tr> + </table> Examples: diff --git a/res/config.toml b/res/config.toml index f4787eed..5b130762 100644 --- a/res/config.toml +++ b/res/config.toml @@ -50,6 +50,7 @@ default-headers = { [input] vi-numeric-prefix = true +use-mouse = true [display] color-mode = "auto" diff --git a/src/config/config.nim b/src/config/config.nim index e7c0d0aa..1ffef292 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -98,6 +98,7 @@ type InputConfig = object vi_numeric_prefix* {.jsgetset.}: bool + use_mouse* {.jsgetset.}: bool NetworkConfig = object max_redirect* {.jsgetset.}: int32 diff --git a/src/display/term.nim b/src/display/term.nim index a782fe99..15ec6ed3 100644 --- a/src/display/term.nim +++ b/src/display/term.nim @@ -90,6 +90,9 @@ const GEOMPIXEL = CSI(14, "t") # report window size in chars const GEOMCELL = CSI(18, "t") +# allow shift-key to override mouse protocol +const XTSHIFTESCAPE = CSI(">0s") + # device control string template DCS(a, b: char, s: varargs[string]): string = "\eP" & a & b & s.join(';') & "\e\\" @@ -107,13 +110,19 @@ template XTERM_TITLE(s: string): string = const XTGETFG = OSC(10, "?") # get foreground color const XTGETBG = OSC(11, "?") # get background color +# DEC set +template DECSET(s: varargs[string, `$`]): string = + "\e[?" & s.join(';') & 'h' + +# DEC reset +template DECRST(s: varargs[string, `$`]): string = + "\e[?" & s.join(';') & 'l' + +# mouse tracking +const SGRMOUSEBTNON = DECSET(1002, 1006) +const SGRMOUSEBTNOFF = DECRST(1002, 1006) + when not termcap_found: - # DEC set - template DECSET(s: varargs[string, `$`]): string = - "\e[?" & s.join(';') & 'h' - # DEC reset - template DECRST(s: varargs[string, `$`]): string = - "\e[?" & s.join(';') & 'l' const SMCUP = DECSET(1049) const RMCUP = DECRST(1049) const CNORM = DECSET(25) @@ -190,7 +199,7 @@ proc isatty(fd: FileHandle): cint {.importc: "isatty", header: "<unistd.h>".} proc isatty*(f: File): bool = return isatty(f.getFileHandle()) != 0 -proc isatty(term: Terminal): bool = +proc isatty*(term: Terminal): bool = term.infile != nil and term.infile.isatty() and term.outfile.isatty() proc anyKey*(term: Terminal) = @@ -404,6 +413,12 @@ proc setTitle*(term: Terminal, title: string) = title term.outfile.write(XTERM_TITLE(title)) +proc enableMouse*(term: Terminal) = + term.write(XTSHIFTESCAPE & SGRMOUSEBTNON) + +proc disableMouse*(term: Terminal) = + term.write(SGRMOUSEBTNOFF) + proc processOutputString*(term: Terminal, str: string, w: var int): string = if str.validateUTF8Surr() != -1: return "?" @@ -618,6 +633,8 @@ proc quit*(term: Terminal) = term.write(term.disableAltScreen()) else: term.write(term.cursorGoto(0, term.attrs.height - 1)) + if term.config.input.use_mouse: + term.disableMouse() term.showCursor() term.cleared = false if term.stdin_unblocked: @@ -863,6 +880,8 @@ proc start*(term: Terminal, infile: File): TermStartResult = result = term.detectTermAttributes(windowOnly = false) if result == tsrDA1Fail: term.config.display.query_da1 = false + if term.isatty() and term.config.input.use_mouse: + term.enableMouse() term.applyConfig() term.canvas = newFixedGrid(term.attrs.width, term.attrs.height) if term.smcup: @@ -876,6 +895,8 @@ proc restart*(term: Terminal) = discard fcntl(fd, F_SETFL, term.orig_flags2) term.orig_flags2 = 0 term.stdin_unblocked = true + if term.config.input.use_mouse: + term.enableMouse() if term.smcup: term.write(term.enableAltScreen()) diff --git a/src/local/client.nim b/src/local/client.nim index 11b3a8f2..76a61bfa 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -67,6 +67,7 @@ type pager {.jsget.}: Pager selector: Selector[int] timeouts: TimeoutState + pressed: tuple[col: int, row: int] ConsoleWrapper = object console: Console @@ -202,6 +203,83 @@ 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 = (0, "left") + mibMiddle = (1, "middle") + mibRight = (2, "right") + mibWheelUp = (3, "wheelUp") + mibWheelDown = (4, "wheelDown") + mibButton6 = (5, "button6") + mibButton7 = (6, "button7") + mibButton8 = (7, "button8") + mibButton9 = (8, "button9") + mibButton10 = (9, "button10") + mibButton11 = (10, "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 + if (btn and 64) != 0: + button += 3 + if (btn and 128) != 0: + button += 6 + 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 @@ -219,12 +297,61 @@ proc handleCommandInput(client: Client, c: char): EmptyPromise = client.pager.notnum = true client.pager.inputBuffer &= c let action = getNormalAction(client.config, client.pager.inputBuffer) - let p = client.evalAction(action, client.pager.precnum) - if not client.feednext: - client.pager.precnum = 0 - client.pager.notnum = false - client.handlePagerEvents() - return p + if action != "": + let p = client.evalAction(action, client.pager.precnum) + if not client.feednext: + client.pager.precnum = 0 + client.pager.notnum = false + client.handlePagerEvents() + return p + if client.config.input.use_mouse: + if client.pager.inputBuffer == "\e[<": + let input = client.parseMouseInput() + if input.isSome: + let input = input.get + let container = client.pager.container + if container != nil: + case input.button + of mibLeft: + case input.t + of mitPress: + client.pressed = (input.col, input.row) + of mitRelease: + #TODO this does not work very well with double width chars, + # because pressed could be equivalent to two separate cells + if client.pressed == (input.col, input.row): + if input.col == container.acursorx and + input.row == container.acursory: + container.click() + else: + container.setCursorXY(container.fromx + input.col, + container.fromy + input.row) + else: + let diff = (input.col - client.pressed.col, + input.row - client.pressed.row) + if diff[0] > 0: + container.scrollLeft(diff[0]) + else: + container.scrollRight(-diff[0]) + if diff[1] > 0: + container.scrollUp(diff[1]) + else: + container.scrollDown(-diff[1]) + client.pressed = (-1, -1) + else: discard + of mibWheelUp: container.scrollUp(5) + of mibWheelDown: container.scrollDown(5) + of mibButton6, mibButton8: + if input.t == mitPress: + discard client.pager.nextBuffer() + of mibButton7, mibButton9: + if input.t == mitPress: + discard client.pager.prevBuffer() + else: discard + client.pager.inputBuffer = "" + elif "\e[<".startsWith(client.pager.inputBuffer): + client.feednext = true + return nil proc input(client: Client): EmptyPromise = var p: EmptyPromise = nil @@ -266,8 +393,11 @@ proc input(client: Client): EmptyPromise = client.pager.inputBuffer = "" client.pager.refreshStatusMsg() break - client.pager.refreshStatusMsg() - client.pager.draw() + #TODO this is not perfect, because it results in us never displaying + # lone escape. maybe a timeout for escape display would be useful + if not "\e[<".startsWith(client.pager.inputBuffer): + client.pager.refreshStatusMsg() + client.pager.draw() if not client.feednext: client.pager.inputBuffer = "" break diff --git a/src/local/container.nim b/src/local/container.nim index 06c210bb..03cad675 100644 --- a/src/local/container.nim +++ b/src/local/container.nim @@ -603,7 +603,7 @@ proc setCursorY(container: Container, y: int, refresh = true) {.jsfunc.} = if refresh: container.sendCursorPosition() -proc setCursorXY(container: Container, x, y: int, refresh = true) {.jsfunc.} = +proc setCursorXY*(container: Container, x, y: int, refresh = true) {.jsfunc.} = container.setCursorY(y, refresh) container.setCursorX(x, refresh) @@ -1014,30 +1014,36 @@ proc cursorMiddleColumn(container: Container) {.jsfunc.} = proc cursorRightEdge(container: Container) {.jsfunc.} = container.setCursorX(container.fromx + container.width - 1) -proc scrollDown(container: Container, n = 1) {.jsfunc.} = - if container.fromy + container.height < container.numLines: - container.setFromY(container.fromy + n) +proc scrollDown*(container: Container, n = 1) {.jsfunc.} = + let H = container.numLines - 1 + let y = min(container.fromy + container.height + n, H) - container.height + if y > container.fromy: + container.setFromY(y) if container.fromy > container.cursory: container.cursorDown(container.fromy - container.cursory) else: container.cursorDown(n) -proc scrollUp(container: Container, n = 1) {.jsfunc.} = - if container.fromy > 0: - container.setFromY(container.fromy - n) +proc scrollUp*(container: Container, n = 1) {.jsfunc.} = + let y = max(container.fromy - n, 0) + if y < container.fromy: + container.setFromY(y) if container.fromy + container.height <= container.cursory: container.cursorUp(container.cursory - container.fromy - container.height + 1) else: container.cursorUp(n) -proc scrollRight(container: Container, n = 1) {.jsfunc.} = - if container.fromx + container.width + n <= container.maxScreenWidth(): - container.setFromX(container.fromx + n) +proc scrollRight*(container: Container, n = 1) {.jsfunc.} = + let msw = container.maxScreenWidth() + let x = min(container.fromx + container.width + n, msw) - container.width + if x > container.fromx: + container.setFromX(x) -proc scrollLeft(container: Container, n = 1) {.jsfunc.} = - if container.fromx - n >= 0: - container.setFromX(container.fromx - n) +proc scrollLeft*(container: Container, n = 1) {.jsfunc.} = + let x = max(container.fromx - n, 0) + if x < container.fromx: + container.setFromX(x) proc alert(container: Container, msg: string) = container.triggerEvent(ContainerEvent(t: ALERT, msg: msg)) @@ -1521,7 +1527,7 @@ proc onclick(container: Container, res: ClickResult) = ) container.triggerEvent(event) -proc click(container: Container) {.jsfunc.} = +proc click*(container: Container) {.jsfunc.} = if container.select.open: container.select.click() else: diff --git a/src/local/pager.nim b/src/local/pager.nim index 9b417d95..1040c259 100644 --- a/src/local/pager.nim +++ b/src/local/pager.nim @@ -177,12 +177,16 @@ proc getLineHist(pager: Pager, mode: LineMode): LineHistory = proc setLineEdit(pager: Pager, prompt: string, mode: LineMode, current = "", hide = false) = let hist = pager.getLineHist(mode) + if pager.term.isatty() and pager.config.input.use_mouse: + pager.term.disableMouse() let edit = readLine(prompt, pager.attrs.width, current, {}, hide, hist) pager.lineedit = some(edit) pager.linemode = mode proc clearLineEdit(pager: Pager) = pager.lineedit = none(LineEdit) + if pager.term.isatty() and pager.config.input.use_mouse: + pager.term.enableMouse() proc searchForward(pager: Pager) {.jsfunc.} = pager.setLineEdit("/", SEARCH_F) @@ -478,7 +482,7 @@ proc dupeBuffer(pager: Pager) {.jsfunc.} = # The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT # commands by traversing the container tree in a depth-first order. -proc prevBuffer(pager: Pager): bool {.jsfunc.} = +proc prevBuffer*(pager: Pager): bool {.jsfunc.} = if pager.container == nil: return false if pager.container.parent == nil: @@ -494,7 +498,7 @@ proc prevBuffer(pager: Pager): bool {.jsfunc.} = pager.setContainer(pager.container.parent) return true -proc nextBuffer(pager: Pager): bool {.jsfunc.} = +proc nextBuffer*(pager: Pager): bool {.jsfunc.} = if pager.container == nil: return false if pager.container.children.len > 0: |