about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md1
-rw-r--r--doc/config.md7
-rw-r--r--res/config.toml1
-rw-r--r--src/config/config.nim1
-rw-r--r--src/display/term.nim35
-rw-r--r--src/local/client.nim146
-rw-r--r--src/local/container.nim34
-rw-r--r--src/local/pager.nim8
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: