diff options
-rw-r--r-- | bonus/w3m.toml | 181 | ||||
-rw-r--r-- | res/chawan.html | 4 | ||||
-rw-r--r-- | res/config.toml | 555 | ||||
-rw-r--r-- | src/config/config.nim | 94 | ||||
-rw-r--r-- | src/config/toml.nim | 3 | ||||
-rw-r--r-- | src/local/client.nim | 18 | ||||
-rw-r--r-- | src/main.nim | 12 |
7 files changed, 554 insertions, 313 deletions
diff --git a/bonus/w3m.toml b/bonus/w3m.toml index 3ed27cf9..a8ab54e0 100644 --- a/bonus/w3m.toml +++ b/bonus/w3m.toml @@ -3,73 +3,10 @@ # ~/.config/chawan/w3m.toml, then at the beginning of # ~/.config/chawan/chawan.toml, include = "w3m.toml".) -[page] -# Page/cursor movement -' ' = 'n => pager.scrollDown(pager.height * (n ?? 1))' -C-v = 'n => pager.scrollDown(pager.height * (n ?? 1))' -b = 'n => pager.scrollUp(pager.height * (n ?? 1))' -M-v = 'n => pager.scrollUp(pager.height * (n ?? 1))' -'M-[6~' = 'n => pager.scrollDown(pager.height * (n ?? 1))' -'M-[5~' = 'n => pager.scrollUp(pager.height * (n ?? 1))' -l = 'n => pager.cursorRight(n)' -h = 'n => pager.cursorLeft(n)' -j = 'n => pager.cursorDown(n)' -k = 'n => pager.cursorUp(n)' -C-f = 'n => pager.cursorRight(n)' -C-b = 'n => pager.cursorLeft(n)' -C-n = 'n => pager.cursorDown(n)' -C-p = 'n => pager.cursorUp(n)' -J = 'n => pager.scrollUp(n)' -K = 'n => pager.scrollDown(n)' -'^' = 'pager.cursorLineBegin()' -C-a = 'pager.cursorLineBegin()' -'$' = 'pager.cursorLineEnd()' -C-e = 'pager.cursorLineEnd()' -w = 'pager.cursorNextWord()' -W = 'pager.cursorWordBegin()' -'<' = 'n => pager.pageLeft(n)' -'>' = 'n => pager.pageRight(n)' -'.' = 'n => pager.scrollLeft(n)' -',' = 'n => pager.scrollRight(n)' -g = 'n => n ? pager.gotoLine(n) : pager.cursorFirstLine()' -'M-<' = 'pager.cursorFirstLine()' -G = 'n => n ? pager.gotoLine(n) : pager.cursorLastLine()' -'M->' = 'pager.cursorLastLine()' -M-g = 'n => pager.gotoLine(n)' -Z = 'pager.centerColumn()' -z = 'pager.centerLine()' -C-i = 'n => pager.cursorNextLink(n)' -C-u = 'n => pager.cursorPrevLink(n)' -M-C-i = 'n => pager.cursorPrevLink(n)' -'[' = 'n => pager.cursorNthLink(n)' -']' = 'n => pager.cursorRevNthLink(n)' -# Hyperlink selection -C-j = 'pager.click()' -C-m = 'pager.click()' -c = 'pager.peek()' -u = 'pager.peekCursor()' -#TODO download, etc -# File and URL-related actions -U = 'pager.load()' -V = 'pager.load()' #TODO file only -#TODO exec shell -# Buffer operations -B = 'pager.discardBuffer()' -v = 'pager.toggleSource()' -#TODO edit -C-l = 'pager.redraw()' -R = 'pager.reload()' -#TODO save, save source -E = ''' -() => { - if (pager.url.protocol == "file:") - pager.extern(pager.getEditorCommand(pager.url.pathname)) - else - pager.alert("Can't edit other than local file"); -} -''' -#TODO buffer selection mode -'C-@' = ''' +[cmd.w3m.buffer] +pageDown = 'n => pager.scrollDown(pager.height * (n ?? 1))' +pageUp = 'n => pager.scrollUp(pager.height * (n ?? 1))' +mark = ''' () => { /* id is always the current position; this way we can clear by setting a mark twice at the same position. */ @@ -80,7 +17,10 @@ E = ''' pager.clearMark(id); } ''' -M-p = ''' +gotoLine = 'n => pager.gotoLine(n)' +centerColumn = '() => pager.centerColumn()' +centerLine = '() => pager.centerLine()' +prevMark = ''' () => { const next = pager.findPrevMark(); if (next) @@ -89,30 +29,105 @@ M-p = ''' pager.alert("No mark exists before here"); } ''' -M-n = ''' +nextMark = ''' () => { const next = pager.findNextMark(); if (next) pager.gotoMark(next); else - pager.alert("No mark exists after here"); + pager.alert("No mark exists before here"); } ''' -# Search -'/' = 'pager.searchForward()' -C-s = 'pager.searchForward()' -'?' = 'pager.searchBackward()' -C-r = 'pager.searchBackward()' -n = 'pager.searchNext()' -N = 'pager.searchPrev()' -C-w = ''' -config.search.wrap = !config.search.wrap; -pager.alert("Wrap search " + (config.search.wrap ? "on" : "off")); + +[cmd.w3m.pager] +saveFile = ''' +() => { + if (pager.url.protocol == "file:") + pager.extern(pager.getEditorCommand(pager.url.pathname)) + else + pager.alert("Can't edit other than local file"); +} +''' +askQuit = ''' +() => pager.ask("Do you want to exit Chawan?").then(x => x ? quit() : void(0)) ''' + +[page] +# Page/cursor movement +' ' = 'cmd.w3m.buffer.pageDown' +C-v = 'cmd.w3m.buffer.pageDown' +b = 'cmd.w3m.buffer.pageUp' +M-v = 'cmd.w3m.buffer.pageUp' +'M-[6~' = 'cmd.w3m.buffer.pageDown' +'M-[5~' = 'cmd.w3m.buffer.pageUp' +C-f = 'cmd.buffer.cursorRight' +C-b = 'cmd.buffer.cursorLeft' +C-n = 'cmd.buffer.cursorDown' +C-p = 'cmd.buffer.cursorUp' +J = 'cmd.buffer.scrollUp' +K = 'cmd.buffer.scrollDown' +'^' = 'cmd.buffer.cursorLineBegin' +C-a = 'cmd.buffer.cursorLineBegin' +'$' = 'cmd.buffer.cursorLineEnd' +C-e = 'cmd.buffer.cursorLineEnd' +w = 'cmd.buffer.cursorNextWord' +W = 'cmd.buffer.cursorWordBegin' +'<' = 'cmd.buffer.pageLeft' +'>' = 'cmd.buffer.pageRight' +'.' = 'cmd.buffer.scrollLeft' +',' = 'cmd.buffer.scrollRight' +g = 'cmd.buffer.gotoLineOrStart' +'M-<' = 'cmd.buffer.cursorFirstLine' +G = 'cmd.buffer.gotoLineOrEnd' +'M->' = 'cmd.buffer.cursorLastLine' +M-g = 'cmd.w3m.buffer.gotoLine' +Z = 'cmd.w3m.buffer.centerColumn' +z = 'cmd.w3m.buffer.centerLine' +C-i = 'cmd.buffer.cursorNextLink' +C-u = 'cmd.buffer.cursorPrevLink' +M-C-i = 'cmd.buffer.cursorPrevLink' +'[' = 'cmd.buffer.cursorNthLink' +']' = 'cmd.buffer.cursorRevNthLink' +# Hyperlink selection +C-j = 'cmd.buffer.click' +C-m = 'cmd.buffer.click' +c = 'cmd.pager.peek' +u = 'cmd.pager.peekCursor' +a = 'cmd.pager.saveLink' +M-C-j = 'cmd.buffer.saveLink' +M-C-m = 'cmd.buffer.saveLink' +I = 'cmd.buffer.viewImage' +#TODO save image +# File and URL-related actions +U = 'cmd.pager.load' +V = 'cmd.pager.load' #TODO file only +#TODO exec shell +# Buffer operations +B = 'cmd.pager.discardBuffer' +v = 'cmd.pager.toggleSource' +#TODO edit +C-l = 'cmd.buffer.redraw' +R = 'cmd.pager.reload' +E = 'cmd.pager.editFile' +M-s = 'cmd.pager.saveSource' +#TODO save screen, edit screen +#TODO buffer selection mode +'C-@' = 'cmd.w3m.buffer.mark' +M-p = 'cmd.w3m.buffer.prevMark' +M-n = 'cmd.w3m.buffer.nextMark' +# Search +'/' = 'cmd.pager.searchForward' +C-s = 'cmd.pager.searchForward' +'?' = 'cmd.pager.searchBackward' +C-r = 'cmd.pager.searchBackward' +n = 'cmd.pager.searchNext' +N = 'cmd.pager.searchPrev' +C-w = 'cmd.pager.toggleWrap' # Misc #TODO shell out, help file, options, cookies -C-c = 'pager.cancel()' -q = 'pager.ask("Do you want to exit Chawan?").then(x => x ? quit() : void(0))' -Q = 'quit()' +C-c = 'cmd.pager.cancel' +q = 'cmd.w3m.pager.askQuit' +Q = 'cmd.pager.quit' +C-d = '' # w3m line editing is equivalent to Chawan's defaults. diff --git a/res/chawan.html b/res/chawan.html index 169c6800..6ce6950b 100644 --- a/res/chawan.html +++ b/res/chawan.html @@ -67,7 +67,7 @@ up/down by one row <li><kbd>C-d</kbd>, <kbd>C-u</kbd>: scroll up/down by half a page <li><kbd>C-f</kbd>, <kbd>C-b</kbd> (or <kbd>PgDn</kbd>, <kbd>PgUp</kbd>)</kbd>: scroll up/down by an entire page -<li><kbd>{number}G<kbd> (or <kbd>{number}gg</kbd>): jump to {number}'th line +<li><kbd>{number}G</kbd> (or <kbd>{number}gg</kbd>): jump to {number}'th line <li><kbd>g0<kbd>: jump to first character of the current line's visible part <li><kbd>gc<kbd>: jump to center of the current line's visible part <li><kbd>g$<kbd>: jump to last character of the current line's visible part @@ -79,7 +79,7 @@ screen to the left/right by one cell <li><kbd>/</kbd>, <kbd>?</kbd>: on-page search (or search backwards) <li><kbd>n</kbd>, <kbd>N</kbd>: next/previous match <li><kbd>C-z</kbd>: suspend the browser -<li><kbd>M-C-c</kbd>: cancel loading +<li><kbd>C-c</kbd>: cancel loading <li><kbd>H</kbd>, <kbd>M</kbd>, <kbd>L</kbd>: move cursor to the Highest/Middle/Lowest rows <li><kbd>zz</kbd>, <kbd>z.</kbd>: center on current line (and move to diff --git a/res/config.toml b/res/config.toml index d7186c24..739a7099 100644 --- a/res/config.toml +++ b/res/config.toml @@ -4,6 +4,214 @@ startup-script = "" headless = false console-buffer = true +[cmd.pager] +quit = '() => quit()' +suspend = '() => suspend()' +copyURL = ''' +() => { + if (pager.externInto('xsel -bi', pager.url)) + pager.alert("Copied URL to clipboard."); + else + pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); +} +''' +copyCursorLink = ''' +() => { + const link = pager.hoverLink; + if (!link) + pager.alert("Please move the cursor above a link and try again."); + else if (pager.externInto('xsel -bi', link)) + pager.alert("Copied URL to clipboard."); + else + pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); +} +''' +yankCursorImage = ''' +() => { + const link = pager.hoverImage; + if (!link) + pager.alert("Please move the cursor above an image and try again."); + else if (pager.externInto('xsel -bi', link)) + pager.alert("Copied URL to clipboard."); + else + pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); +} +''' +gotoClipboardURL = ''' +() => { + const s = pager.externCapture('xsel -bo'); + if (s === null) + pager.alert("Failed to read URL from clipboard. (Is xsel installed?)"); + else + pager.loadSubmit(s); +} +''' +peek = '() => pager.alert(pager.url)' +peekCursor = '() => pager.peekCursor()' +toggleWrap = ''' +() => { + config.search.wrap = !config.search.wrap; + pager.alert("Wrap search " + (config.search.wrap ? "on" : "off")); +} +''' +dupeBuffer = '() => pager.dupeBuffer()' +load = '() => pager.load()' +webSearch = '() => pager.load("ddg:")' +openBookmarks = '() => pager.loadSubmit("~/.w3m/bookmark.html")' +reloadBuffer = '() => pager.reload()' +lineInfo = '() => pager.lineInfo()' +toggleSource = '() => pager.toggleSource()' +discardBuffer = '() => pager.discardBuffer()' +discardTree = '() => pager.discardTree()' +prevBuffer = '() => pager.prevBuffer()' +prevSiblingBuffer = '() => pager.prevSiblingBuffer()' +nextBuffer = '() => pager.nextBuffer()' +nextSiblingBuffer = '() => pager.nextSiblingBuffer()' +parentBuffer = '() => pager.parentBuffer()' +enterCommand = '() => pager.command()' +searchForward = '() => pager.searchForward()' +searchBackward = '() => pager.searchBackward()' +isearchForward = '() => pager.isearchForward()' +isearchBackward = '() => pager.isearchBackward()' +searchNext = 'n => pager.searchNext(n)' +searchPrev = 'n => pager.searchPrev(n)' +toggleCommandMode = ''' +() => { + if ((pager.commandMode = consoleBuffer != pager.buffer)) + console.show(); + else + console.hide(); +} +''' + +[cmd.buffer] +cursorLeft = 'n => pager.cursorLeft(n)' +cursorDown = 'n => pager.cursorDown(n)' +cursorUp = 'n => pager.cursorUp(n)' +cursorRight = 'n => pager.cursorRight(n)' +cursorLineBegin = '() => pager.cursorLineBegin()' +cursorLineTextStart = '() => pager.cursorLineTextStart()' +cursorLineEnd = '() => pager.cursorLineEnd()' +cursorNextWord = '() => pager.cursorNextWord()' +cursorNextViWord = '() => pager.cursorNextViWord()' +cursorNextBigWord = '() => pager.cursorNextBigWord()' +cursorWordBegin = '() => pager.cursorWordBegin()' +cursorViWordBegin = '() => pager.cursorViWordBegin()' +cursorBigWordBegin = '() => pager.cursorBigWordBegin()' +cursorWordEnd = '() => pager.cursorWordEnd()' +cursorViWordEnd = '() => pager.cursorViWordEnd()' +cursorBigWordEnd = '() => pager.cursorBigWordEnd()' +cursorPrevLink = 'n => pager.cursorPrevLink(n)' +cursorNextLink = 'n => pager.cursorNextLink(n)' +cursorPrevParagraph = 'n => pager.cursorPrevParagraph(n)' +cursorNextParagraph = 'n => pager.cursorNextParagraph(n)' +cursorTop = 'n => pager.cursorTop(n)' +cursorMiddle = '() => pager.cursorMiddle()' +cursorBottom = 'n => pager.cursorBottom(n)' +cursorLeftEdge = '() => pager.cursorLeftEdge()' +cursorMiddleColumn = '() => pager.cursorMiddleColumn()' +cursorRightEdge = '() => pager.cursorRightEdge()' +halfPageDown = 'n => pager.halfPageDown(n)' +halfPageUp = 'n => pager.halfPageUp(n)' +pageDown = 'n => pager.pageDown(n)' +pageUp = 'n => pager.pageUp(n)' +pageLeft = 'n => pager.pageLeft(n)' +pageRight = 'n => pager.pageRight(n)' +scrollDown = 'n => pager.scrollDown(n)' +scrollUp = 'n => pager.scrollUp(n)' +scrollLeft = 'n => pager.scrollLeft(n)' +scrollRight = 'n => pager.scrollRight(n)' +click = '() => pager.click()' +viewImage = '() => pager.gotoURL(pager.hoverImage)' +markURL = '() => pager.markURL()' +redraw = '() => pager.redraw()' +reshape = '() => pager.reshape()' +cancel = '() => pager.cancel()' +# vi G +gotoLineOrEnd = 'n => n ? pager.gotoLine(n) : pager.cursorLastLine()' +# vim gg +gotoLineOrStart = 'n => n ? pager.gotoLine(n) : pager.cursorFirstLine()' +# vi z. z^M z- +centerLineBegin = 'n => pager.centerLineBegin(n)' +raisePageBegin = 'n => pager.raisePageBegin(n)' +lowerPageBegin = 'n => pager.lowerPageBegin(n)' +# vi z+ z^ +nextPageBegin = 'n => pager.nextPageBegin(n)' +previousPageBegin = 'n => pager.previousPageBegin(n)' +# vim zz zb zt +centerLine = 'n => pager.centerLine(n)' +raisePage = 'n => pager.raisePage(n)' +lowerPage = 'n => pager.lowerPage(n)' +cursorToggleSelection = 'n => pager.cursorToggleSelection(n)' +cursorToggleSelectionLine = 'n => pager.cursorToggleSelection(n, {selectionType: "line"})' +cursorToggleSelectionBlock = 'n => pager.cursorToggleSelection(n, {selectionType: "block"})' +sourceEdit = ''' +() => { + const url = pager.url; + pager.extern(pager.getEditorCommand(url.protocol == "file:" ? + url.pathname : + pager.cacheFile)); +} +''' +saveLink = '() => pager.saveLink()' +saveSource = '() => pager.saveSource()' +mark = ''' +async () => { + const c = await pager.askChar("m"); + if (c.charCodeAt() != 3) /* ctrl-c */ + pager.setMark(c); +} +''' +gotoMark = ''' +async () => { + const c = await pager.askChar('`'); + if (c.charCodeAt() != 3) /* C-c */ + pager.gotoMark(c); +} +''' +gotoMarkY = ''' +async () => { + const c = await pager.askChar('`'); + if (c.charCodeAt() != 3) /* C-c */ + pager.gotoMarkY(c); +} +''' +copySelection = ''' +async () => { + if (!pager.currentSelection) { + feedNext(); + return; + } + const text = await pager.getSelectionText(pager.currentSelection); + if (pager.externInto('xsel -bi', text)) + pager.alert("Copied selection to clipboard."); + else + pager.alert("Failed to copy selection to clipboard. (Is xsel installed?)"); + pager.cursorToggleSelection(); +} +''' +cursorNthLink = 'n => pager.cursorNthLink(n)' +cursorRevNthLink = 'n => pager.cursorRevNthLink(n)' + +[cmd.line] +submit = '() => line.submit()' +backspace = '() => line.backspace()' +delete = '() => line.delete()' +cancel = '() => line.cancel()' +prevWord = '() => line.prevWord()' +nextWord = '() => line.nextWord()' +backward = '() => line.backward()' +forward = '() => line.forward()' +clear = '() => line.clear()' +kill = '() => line.kill()' +clearWord = '() => line.clearWord()' +killWord = '() => line.killWord()' +begin = '() => line.begin()' +end = '() => line.end()' +escape = '() => line.escape()' +prevHist = '() => line.prevHist()' +nextHist = '() => line.nextHist()' + [search] wrap = true ignore-case = true @@ -80,218 +288,139 @@ match = '^ddg:' substitute-url = '(x) => "https://lite.duckduckgo.com/lite/?kp=-1&kd=-1&q=" + encodeURIComponent(x.split(":").slice(1).join(":"))' [page] -q = 'quit()' -C-z = 'suspend()' -h = 'n => pager.cursorLeft(n)' -j = 'n => pager.cursorDown(n)' -k = 'n => pager.cursorUp(n)' -l = 'n => pager.cursorRight(n)' -'M-[D' = 'n => pager.cursorLeft(n)' -'M-[B' = 'n => pager.cursorDown(n)' -'M-[A' = 'n => pager.cursorUp(n)' -'M-[C' = 'n => pager.cursorRight(n)' -'0' = 'pager.cursorLineBegin()' -'^' = 'pager.cursorLineTextStart()' -'$' = 'pager.cursorLineEnd()' -b = 'pager.cursorViWordBegin()' -e = 'pager.cursorViWordEnd()' -w = 'pager.cursorNextViWord()' -B = 'pager.cursorBigWordBegin()' -E = 'pager.cursorBigWordEnd()' -W = 'pager.cursorNextBigWord()' -'[' = 'n => pager.cursorPrevLink(n)' -']' = 'n => pager.cursorNextLink(n)' -'{' = 'n => pager.cursorPrevParagraph(n)' -'}' = 'n => pager.cursorNextParagraph(n)' -H = 'n => pager.cursorTop(n)' -M = '() => pager.cursorMiddle()' -L = 'n => pager.cursorBottom(n)' -g0 = 'pager.cursorLeftEdge()' -gc = 'pager.cursorMiddleColumn()' -'g$' = 'pager.cursorRightEdge()' -C-d = 'n => pager.halfPageDown(n)' -C-u = 'n => pager.halfPageUp(n)' -C-f = 'n => pager.pageDown(n)' -C-b = 'n => pager.pageUp(n)' -'M-[6~' = 'n => pager.pageDown(n)' -'M-[5~' = 'n => pager.pageUp(n)' -'zH'= 'n => pager.pageLeft(n)' -'zL' = 'n => pager.pageRight(n)' -'<' = 'n => pager.pageLeft(n)' -'>' = 'n => pager.pageRight(n)' -C-e = 'n => pager.scrollDown(n)' -C-y = 'n => pager.scrollUp(n)' -sE = ''' -() => { - const url = pager.url; - pager.extern(pager.getEditorCommand(url.protocol == "file:" ? - url.pathname : - pager.cacheFile)); -} -''' -sC-m = 'pager.saveLink()' -sC-j = 'pager.saveLink()' -sS = 'pager.saveSource()' -m = ''' -async () => { - const c = await pager.askChar("m"); - if (c.charCodeAt() != 3) /* ctrl-c */ - pager.setMark(c); -} -''' -'`' = ''' -async () => { - const c = await pager.askChar('`'); - if (c.charCodeAt() != 3) /* C-c */ - pager.gotoMark(c); -} -''' -"'" = ''' -async () => { - const c = await pager.askChar('`'); - if (c.charCodeAt() != 3) /* C-c */ - pager.gotoMarkY(c); -} -''' -'zh'= 'n => pager.scrollLeft(n)' -'zl' = 'n => pager.scrollRight(n)' -J = 'n => pager.scrollDown(n)' -K = 'n => pager.scrollUp(n)' -'-'= 'n => pager.scrollLeft(n)' -'+' = 'n => pager.scrollRight(n)' -C-m = 'pager.click()' -C-j = 'pager.click()' -I = 'pager.gotoURL(pager.hoverImage)' -M-u = 'pager.dupeBuffer()' -C-l = 'pager.load()' -C-k = 'pager.load("ddg:")' -M-b = 'pager.loadSubmit("~/.w3m/bookmark.html")' -U = 'pager.reload()' -r = 'pager.redraw()' -R = 'pager.reshape()' -M-C-c = 'pager.cancel()' -gg = 'n => n ? pager.gotoLine(n) : pager.cursorFirstLine()' -G = 'n => n ? pager.gotoLine(n) : pager.cursorLastLine()' -M-g = 'pager.gotoLine()' -'z.' = 'n => pager.centerLineBegin(n)' -'zC-m' = 'n => pager.raisePageBegin(n)' -'zC-j' = 'n => pager.raisePageBegin(n)' -'z-' = 'n => pager.lowerPageBegin(n)' -zz = 'n => pager.centerLine(n)' -'zt' = 'n => pager.raisePage(n)' -'zb' = 'n => pager.lowerPage(n)' -'z+' = 'n => pager.nextPageBegin(n)' -'z^' = 'n => pager.previousPageBegin(n)' -C-g = 'pager.lineInfo()' -v = 'n => pager.cursorToggleSelection(n)' -V = 'n => pager.cursorToggleSelection(n, {selectionType: "line"})' -C-v = 'n => pager.cursorToggleSelection(n, {selectionType: "block"})' -y = ''' -async () => { - if (!pager.currentSelection) { - feedNext(); - return; - } - const text = await pager.getSelectionText(pager.currentSelection); - if (pager.externInto('xsel -bi', text)) - pager.alert("Copied selection to clipboard."); - else - pager.alert("Failed to copy selection to clipboard. (Is xsel installed?)"); - pager.cursorToggleSelection(); -} -''' -'\' = 'pager.toggleSource()' -D = 'pager.discardBuffer()' -M-d = 'pager.discardTree()' -',' = 'pager.prevBuffer()' -'M-,' = 'pager.prevSiblingBuffer()' -'.' = 'pager.nextBuffer()' -'M-.' = 'pager.nextSiblingBuffer()' -'M-/' = 'pager.parentBuffer()' -M-c = 'pager.command()' -'/' = 'pager.isearchForward()' -'?' = 'pager.isearchBackward()' -n = 'n => pager.searchNext(n)' -N = 'n => pager.searchPrev(n)' -c = 'pager.peek()' -u = 'pager.peekCursor()' -C-w = ''' -config.search.wrap = !config.search.wrap; -pager.alert("Wrap search " + (config.search.wrap ? "on" : "off")); -''' -M-y = ''' -() => { - if (pager.externInto('xsel -bi', pager.url)) - pager.alert("Copied URL to clipboard."); - else - pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); -} -''' -yc = '() => pager.alert("Please use `yu` to copy URLs instead!");' -yu = ''' -() => { - const link = pager.hoverLink; - if (!link) - pager.alert("Please move the cursor above a link and try again."); - else if (pager.externInto('xsel -bi', link)) - pager.alert("Copied URL to clipboard."); - else - pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); -} -''' -yI = ''' -() => { - const link = pager.hoverImage; - if (!link) - pager.alert("Please move the cursor above an image and try again."); - else if (pager.externInto('xsel -bi', link)) - pager.alert("Copied URL to clipboard."); - else - pager.alert("Failed to copy URL to clipboard. (Is xsel installed?)"); -} -''' -M-p = ''' -() => { - const s = pager.externCapture('xsel -bo'); - if (s === null) - pager.alert("Failed to read URL from clipboard. (Is xsel installed?)"); - else - pager.loadSubmit(s); -} -''' -':' = 'pager.markURL()' +# buffer commands +q = 'cmd.pager.quit' +C-z = 'cmd.pager.suspend' +h = 'cmd.buffer.cursorLeft' +j = 'cmd.buffer.cursorDown' +k = 'cmd.buffer.cursorUp' +l = 'cmd.buffer.cursorRight' +'M-[D' = 'cmd.buffer.cursorLeft' +'M-[B' = 'cmd.buffer.cursorDown' +'M-[A' = 'cmd.buffer.cursorUp' +'M-[C' = 'cmd.buffer.cursorRight' +'0' = 'cmd.buffer.cursorLineBegin' +'^' = 'cmd.buffer.cursorLineTextStart' +'$' = 'cmd.buffer.cursorLineEnd' +b = 'cmd.buffer.cursorViWordBegin' +e = 'cmd.buffer.cursorViWordEnd' +w = 'cmd.buffer.cursorNextViWord' +B = 'cmd.buffer.cursorBigWordBegin' +E = 'cmd.buffer.cursorBigWordEnd' +W = 'cmd.buffer.cursorNextBigWord' +'[' = 'cmd.buffer.cursorPrevLink' +']' = 'cmd.buffer.cursorNextLink' +'{' = 'cmd.buffer.cursorPrevParagraph' +'}' = 'cmd.buffer.cursorNextParagraph' +H = 'cmd.buffer.cursorTop' +M = 'cmd.buffer.cursorMiddle' +L = 'cmd.buffer.cursorBottom' +g0 = 'cmd.buffer.cursorLeftEdge' +gc = 'cmd.buffer.cursorMiddleColumn' +'g$' = 'cmd.buffer.cursorRightEdge' +C-d = 'cmd.buffer.halfPageDown' +C-u = 'cmd.buffer.halfPageUp' +C-f = 'cmd.buffer.pageDown' +C-b = 'cmd.buffer.pageUp' +'M-[6~' = 'cmd.buffer.pageDown' +'M-[5~' = 'cmd.buffer.pageUp' +'zH'= 'cmd.buffer.pageLeft' +'zL' = 'cmd.buffer.pageRight' +'<' = 'cmd.buffer.pageLeft' +'>' = 'cmd.buffer.pageRight' +C-e = 'cmd.buffer.scrollDown' +C-y = 'cmd.buffer.scrollUp' +J = 'cmd.buffer.scrollDown' +K = 'cmd.buffer.scrollUp' +sE = 'cmd.buffer.sourceEdit' +sC-m = 'cmd.buffer.saveLink' +sC-j = 'cmd.buffer.saveLink' +m = 'cmd.buffer.mark' +'`' = 'cmd.buffer.gotoMark' +"'" = 'cmd.buffer.gotoMarkY' +'zh'= 'cmd.buffer.scrollLeft' +'zl' = 'cmd.buffer.scrollRight' +'-'= 'cmd.buffer.scrollLeft' +'+' = 'cmd.buffer.scrollRight' +C-m = 'cmd.buffer.click' +C-j = 'cmd.buffer.click' +I = 'cmd.buffer.viewImage' +':' = 'cmd.buffer.markURL' +r = 'cmd.buffer.redraw' +R = 'cmd.buffer.reshape' +C-c = 'cmd.buffer.cancel' +gg = 'cmd.buffer.gotoLineOrStart' +G = 'cmd.buffer.gotoLineOrEnd' +'z.' = 'cmd.buffer.centerLineBegin' +'zC-m' = 'cmd.buffer.raisePageBegin' +'zC-j' = 'cmd.buffer.raisePageBegin' +'z-' = 'cmd.buffer.lowerPageBegin' +zz = 'cmd.buffer.centerLine' +'zt' = 'cmd.buffer.raisePage' +'zb' = 'cmd.buffer.lowerPage' +'z+' = 'cmd.buffer.nextPageBegin' +'z^' = 'cmd.buffer.previousPageBegin' +y = 'cmd.buffer.copySelection' +v = 'cmd.buffer.cursorToggleSelection' +V = 'cmd.buffer.cursorToggleSelectionLine' +C-v = 'cmd.buffer.cursorToggleSelectionBlock' + +# pager commands +sS = 'cmd.pager.saveSource' +M-u = 'cmd.pager.dupeBuffer' +C-l = 'cmd.pager.load' +C-k = 'cmd.pager.webSearch' +M-b = 'cmd.pager.openBookmarks' +U = 'cmd.pager.reloadBuffer' +C-g = 'cmd.pager.lineInfo' +'\' = 'cmd.pager.toggleSource' +D = 'cmd.pager.discardBuffer' +M-d = 'cmd.pager.discardTree' +',' = 'cmd.pager.prevBuffer' +'M-,' = 'cmd.pager.prevSiblingBuffer' +'.' = 'cmd.pager.nextBuffer' +'M-.' = 'cmd.pager.nextSiblingBuffer' +'M-/' = 'cmd.pager.parentBuffer' +M-c = 'cmd.pager.enterCommand' +'/' = 'cmd.pager.isearchForward' +'?' = 'cmd.pager.isearchBackward' +n = 'cmd.pager.searchNext' +N = 'cmd.pager.searchPrev' +c = 'cmd.pager.peek' +u = 'cmd.pager.peekCursor' +C-w = 'cmd.pager.toggleWrap' +M-y = 'cmd.pager.copyURL' +yc = 'pager.alert("Please use `yu` to copy URLs")' +yu = 'cmd.pager.copyCursorLink' +yI = 'cmd.pager.copyCursorImage' +M-p = 'cmd.pager.gotoClipboardURL' [line] -C-m = 'line.submit()' -C-j = 'line.submit()' -C-h = 'line.backspace()' -'C-?' = 'line.backspace()' -C-d = 'line.delete()' -C-c = 'line.cancel()' -M-b = 'line.prevWord()' -M-f = 'line.nextWord()' -C-b = 'line.backward()' -C-f = 'line.forward()' -C-u = 'line.clear()' -C-_ = 'line.clear()' -M-k = 'line.clear()' -C-k = 'line.kill()' -C-w = 'line.clearWord()' -M-C-h = 'line.clearWord()' -'M-C-?' = 'line.clearWord()' -M-d = 'line.killWord()' -C-a = 'line.begin()' -C-e = 'line.end()' -C-v = 'line.escape()' -C-p = 'line.prevHist()' -C-n = 'line.nextHist()' -M-c = ''' -if ((pager.commandMode = consoleBuffer != pager.buffer)) - console.show(); -else - console.hide(); -''' -'M-[D' = 'line.backward()' -'M-[B' = 'line.nextHist()' -'M-[A' = 'line.prevHist()' -'M-[C' = 'line.forward()' +C-m = 'cmd.line.submit' +C-j = 'cmd.line.submit' +C-h = 'cmd.line.backspace' +'C-?' = 'cmd.line.backspace' +C-d = 'cmd.line.delete' +C-c = 'cmd.line.cancel' +M-b = 'cmd.line.prevWord' +M-f = 'cmd.line.nextWord' +C-b = 'cmd.line.backward' +C-f = 'cmd.line.forward' +C-u = 'cmd.line.clear' +C-_ = 'cmd.line.clear' +M-k = 'cmd.line.clear' +C-k = 'cmd.line.kill' +C-w = 'cmd.line.clearWord' +M-C-h = 'cmd.line.clearWord' +'M-C-?' = 'cmd.line.clearWord' +M-d = 'cmd.line.killWord' +C-a = 'cmd.line.begin' +C-e = 'cmd.line.end' +C-v = 'cmd.line.escape' +C-p = 'cmd.line.prevHist' +C-n = 'cmd.line.nextHist' +M-c = 'cmd.pager.toggleCommandMode' +'M-[D' = 'cmd.line.backward' +'M-[B' = 'cmd.line.nextHist' +'M-[A' = 'cmd.line.prevHist' +'M-[C' = 'cmd.line.forward' diff --git a/src/config/config.nim b/src/config/config.nim index ebf36d49..287c2e54 100644 --- a/src/config/config.nim +++ b/src/config/config.nim @@ -1,8 +1,10 @@ import std/options import std/os import std/streams +import std/strutils import std/tables +import bindings/quickjs import config/chapath import config/mailcap import config/mimetypes @@ -12,6 +14,7 @@ import js/fromjs import js/javascript import js/propertyenumlist import js/regex +import js/tojs import loader/headers import types/cell import types/color @@ -70,6 +73,11 @@ type display_charset* {.jsgetset.}: Option[Charset] document_charset* {.jsgetset.}: seq[Charset] + CommandConfig = object + jsObj*: JSValue + init*: seq[tuple[k, cmd: string]] # initial k/v map + map*: Table[string, JSValue] # qualified name -> function + ExternalConfig = object tmpdir* {.jsgetset.}: ChaPathResolved editor* {.jsgetset.}: string @@ -130,6 +138,7 @@ type #TODO getset siteconf*: seq[SiteConfig] omnirule*: seq[OmniRule] + cmd*: CommandConfig page* {.jsget.}: ActionMap line* {.jsget.}: ActionMap @@ -331,6 +340,8 @@ proc parseConfigValue(ctx: var ConfigParser; x: var Mailcap; v: TomlValue; k: string) proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue; k: string) +proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue; + k: string) proc typeCheck(v: TomlValue; t: TomlValueType; k: string) = if v.t != t: @@ -626,6 +637,28 @@ proc parseConfigValue(ctx: var ConfigParser; x: var URIMethodMap; v: TomlValue; x.parseURIMethodMap(f.readAll()) x.append(DefaultURIMethodMap) +func isCompatibleIdent(s: string): bool = + if s.len == 0 or s[0] notin AsciiAlpha + {'_', '$'}: + return false + for i in 1 ..< s.len: + if s[i] notin AsciiAlphaNumeric + {'_', '$'}: + return false + return true + +proc parseConfigValue(ctx: var ConfigParser; x: var CommandConfig; v: TomlValue; + k: string) = + typeCheck(v, tvtTable, k) + for kk, vv in v: + let kkk = k & "." & kk + typeCheck(vv, {tvtTable, tvtString}, kkk) + if not kk.isCompatibleIdent(): + raise newException(ValueError, "invalid command name: " & kkk) + if vv.t == tvtTable: + ctx.parseConfigValue(x, vv, kkk) + else: # tvtString + # skip initial "cmd.", we don't need it + x.init.add((kkk.substr("cmd.".len), vv.s)) + type ParseConfigResult* = object success*: bool warnings*: seq[string] @@ -700,15 +733,64 @@ proc getNormalAction*(config: Config; s: string): string = proc getLinedAction*(config: Config; s: string): string = return config.line.getOrDefault(s) -proc readConfig*(pathOverride: Option[string]; jsctx: JSContext): Config = - result = Config(jsctx: jsctx) - discard result.parseConfig("res", newStringStream(defaultConfig)) #TODO TODO TODO +type ReadConfigResult = tuple + config: Config + res: ParseConfigResult + +proc readConfig*(pathOverride: Option[string]; jsctx: JSContext): + ReadConfigResult = + let config = Config(jsctx: jsctx) + var res = config.parseConfig("res", newStringStream(defaultConfig)) + if not res.success: + return (nil, res) if pathOverride.isNone: when defined(debug): - discard result.readConfig(getCurrentDir() / "res", "config.toml") - discard result.readConfig(getConfigDir() / "chawan", "config.toml") + res = config.readConfig(getCurrentDir() / "res", "config.toml") + if not res.success: + return (nil, res) + res = config.readConfig(getConfigDir() / "chawan", "config.toml") else: - discard result.readConfig(getCurrentDir(), pathOverride.get) + res = config.readConfig(getCurrentDir(), pathOverride.get) + if not res.success: + return (nil, res) + return (config, res) + +# called after parseConfig returns +proc initCommands*(config: Config): Err[string] = + let ctx = config.jsctx + let obj = JS_NewObject(ctx) + defer: JS_FreeValue(ctx, obj) + if JS_IsException(obj): + return err(ctx.getExceptionStr()) + for i in countdown(config.cmd.init.high, 0): + let (k, cmd) = config.cmd.init[i] + if k in config.cmd.map: + # already in map; skip + continue + var objIt = obj + let name = k.afterLast('.') + if name.len < k.len: + for ss in k.substr(0, k.high - name.len - 1).split('.'): + var prop = JS_GetPropertyStr(ctx, objIt, cstring(ss)) + if JS_IsUndefined(prop): + prop = JS_NewObject(ctx) + ctx.definePropertyE(objIt, ss, prop) + if JS_IsException(prop): + return err(ctx.getExceptionStr()) + objIt = prop + if cmd == "": + config.cmd.map[k] = JS_UNDEFINED + continue + let fun = ctx.eval(cmd, "<" & k & ">", JS_EVAL_TYPE_GLOBAL) + if JS_IsException(fun): + return err(ctx.getExceptionStr()) + if not JS_IsFunction(ctx, fun): + return err(k & " is not a function") + ctx.definePropertyE(objIt, name, JS_DupValue(ctx, fun)) + config.cmd.map[k] = fun + config.cmd.jsObj = JS_DupValue(ctx, obj) + config.cmd.init = @[] + ok() proc addConfigModule*(ctx: JSContext) = ctx.registerType(ActionMap) diff --git a/src/config/toml.nim b/src/config/toml.nim index 5f471b27..760f335d 100644 --- a/src/config/toml.nim +++ b/src/config/toml.nim @@ -183,6 +183,7 @@ proc consumeString(state: var TomlParser, first: char): multiline = true state.seek(2) if multiline and state.peek(0) == '\n': + inc state.line discard state.consume() var escape = false var ml_trim = false @@ -223,6 +224,8 @@ proc consumeString(state: var TomlParser, first: char): if c notin {'\n', ' ', '\t'}: res &= c ml_trim = false + if c == '\n': + inc state.line else: if c == '\n': inc state.line diff --git a/src/local/client.nim b/src/local/client.nim index 72c85744..c63a18db 100644 --- a/src/local/client.nim +++ b/src/local/client.nim @@ -181,8 +181,13 @@ proc handlePagerEvents(client: Client) = if container != nil: client.pager.handleEvents(container) -proc evalAction(client: Client, action: string, arg0: int32): EmptyPromise = - var ret = client.evalJS(action, "<command>") +proc evalActionJS(client: Client; action: string): JSValue = + client.config.cmd.map.withValue(action, p): + return JS_DupValue(client.jsctx, p[]) + return client.evalJS(action, "<command>") + +proc evalAction(client: Client; action: string; arg0: int32): EmptyPromise = + var ret = client.evalActionJS(action) let ctx = client.jsctx var p = EmptyPromise() p.resolve() @@ -814,16 +819,13 @@ proc newClient*(config: Config; forkserver: ForkServer; jsctx: JSContext; tmpdir: config.external.tmpdir )) pager.setLoader(loader) - let client = Client( - config: config, - jsrt: jsrt, - jsctx: jsctx, - pager: pager - ) + let client = Client(config: config, jsrt: jsrt, jsctx: jsctx, pager: pager) jsrt.setInterruptHandler(interruptHandler, cast[pointer](client)) var global = JS_GetGlobalObject(jsctx) jsctx.registerType(Client, asglobal = true) setGlobal(jsctx, global, client) + jsctx.definePropertyE(global, "cmd", config.cmd.jsObj) + config.cmd.jsObj = JS_NULL JS_FreeValue(jsctx, global) client.addJSModules(jsctx) return client diff --git a/src/main.nim b/src/main.nim index 193a3575..20d9c564 100644 --- a/src/main.nim +++ b/src/main.nim @@ -175,14 +175,24 @@ proc main() = inc ctx.i let jsrt = newJSRuntime() let jsctx = jsrt.newJSContext() - let config = readConfig(ctx.configPath, jsctx) var warnings = newSeq[string]() + let (config, res) = readConfig(ctx.configPath, jsctx) + if not res.success: + stderr.write(res.errorMsg) + quit(1) + warnings.add(res.warnings) for opt in ctx.opts: let res = config.parseConfig(getCurrentDir(), opt, laxnames = true) if not res.success: stderr.write(res.errorMsg) quit(1) + warnings.add(res.warnings) config.css.stylesheet &= ctx.stylesheet + block commands: + let res = config.initCommands() + if res.isErr: + stderr.write("Error parsing commands: " & res.error) + quit(1) set_cjk_ambiguous(config.display.double_width_ambiguous) if pages.len == 0 and stdin.isatty(): if ctx.visual: |