CHA-API(7)
๐ CHA-API(7) โ Chawanโs command API ๐
okay bestie, welcome to Chawanโs command API โ the CHA-API(7) page from the Miscellaneous Information Manual. This is the group-chat lore for how keybindings, JavaScript actions, and Chawanโs built-in objects all vibe together. โจ
As described in cha-config(5), users can bind keypress combinations to actions. Such an action can be either a JavaScript expression, or a command defined in the [cmd] section of config.toml.
For example, the following works:
'g p n' = 'n => pager.alert(n)' # e.g. 2gpn prints `2' to the status line
Note however, that JavaScript functions must be called with an appropriate this value. So e.g. the following does not work:
'g p n' = 'pager.alert' # broken
To work around this limitation, actions have to wrap the target function in a closure, as above. However, this has poor reusability; for more complex actions, you would have to copy and paste the entire function every time you re-bind it or call it from a different function.
To fix this, it is possible to define a command in the [cmd] section:
[cmd.me]
showNumber = 'n => pager.alert(n)'
me can be anything you want; it exists only to avoid collisions with built-in commands. The only restriction is that the first component (in this case, โmeโ) must not contain an upper-case letter.
Now you can call cmd.me.showNumber() from any other function, or include it in a keybinding (in that case, cmd. is optional):
'g p n' = 'me.showNumber'
# same as
'g p n' = 'cmd.me.showNumber'
๐ง Interfaces ๐ง
Note that there are also many private functions not documented here. Such functions have no stability guarantee and may disappear at any time, so using them is not recommended. fr fr, donโt build your whole setup on them.
๐ Client ๐
The global object (globalThis) implements the Client interface. Public functions of this are:
quit()โ Exit the browser.suspend()โ Temporarily suspend the browser, by delivering the client process aSIGTSTPsignal. Note: this suspends all processes, including buffers, the loader and CGI.readFile(path)โ Read a file atpath. Returns the fileโs content as a string, ornullif the file does not exist.writeFile(path, content)โ Writecontentto the file atpath. Throws aTypeErrorif this failed for whatever reason.getenv(name, fallback = null)โ Get an environment variable byname. Returnsfallbackif the variable does not exist.setenv(name, value)โ Set an environment variable byname. Throws aTypeErrorif the operation failed (e.g. because the variableโs size exceeded an OS-specified limit.)pagerโ The pager object. ImplementsPager, as described below.lineโ The line editor. ImplementsLineEdit, as described below.bufferโ The currently focused buffer. ImplementsBuffer, as described below.selectโ The currently focused select menu. ImplementsSelect, as described below.configโ The config object. A currently incomplete interface for retrieving and setting configuration options. In general, names are the same as inconfig.toml, except all-(ASCII hyphen) characters are stripped and the next character is upper-cased. e.g.external.cgi-dircan be queried asconfig.external.cgiDir, etc. Setting individual options sometimes works, but sometimes they do not get propagated as expected. Consider this an experimental API. Currently,siteconfandomnirulevalues are not exposed to JS. The configuration directory itself can be queried asconfig.dir.
Client is derived from Window, so it indirectly implements various web standards normally available on the Window object on websites, e.g. fetch(). However, it does not give access to JS objects in buffers, so for example, globalThis.document always returns a blank document.
๐ Pager ๐
Pager is a separate interface from Client that gives access to the pager (i.e. browser chrome). It is accessible as globalThis.pager, or simply pager.
Note: there is a quirk of questionable value where some (but not all) functions defined onBufferare also defined onPager. For example,pager.urldispatches tobuffer.url. This quirk is deprecated and may be removed in the future; users are encouraged to use thebuffervariable directly. lowkey, just usebuffer.
Following properties (functions/getters) are defined by Pager:
load(url = buffer.url)โ Opens a prompt to load a resource, withurlpre-filled.loadSubmit(url)โ Act as ifurlhad been entered to the URL bar.loadSubmitdiffers fromgotoURLin that it also evaluates omni-rules, tries to prepend a scheme for URLs without one/local files, etc.gotoURL(url, options = {replace: null, contentType: null, save: false, charset: null})โ Go to the specified URL immediately (without a prompt). This differs fromloadSubmitin that it loads the exact URL as passed (no prependinghttps, etc.)- When
replaceis set to aBufferobject, the new buffer will replace the old one if it loads successfully. - When
contentTypeis set, the new bufferโs content type is forcibly set to that string. - When
saveistrue, the user is prompted to save the resource instead of displaying it in a buffer. - When
charsetis notnull, the specified charset label is forced instead of regular charset detection.
- When
traverse(dir)โ Switch to the next buffer in directiondir, interpreted as inBuffer#find.nextBuffer(),prevBuffer()โ Same astraverse("next")andtraverse("prev").dupeBuffer()โ Duplicate the current buffer by loading its source in a new buffer.discardBuffer(buffer = globalThis.buffer, dir = pager.navDirection)โ Discardbuffer, then move back to the buffer opposite todir(interpreted as inBuffer#find).discardTree()โ Discard all subsequent siblings of the current buffer. This function is deprecated, and may be removed in the future.addTab(target)โ Open a new tab. Iftargetis a buffer, it is removed from its current tab and added to the newly created tab. Otherwise,targetis interpreted as a URL to open withgotoURL.prevTab(),nextTab()โ Switch to the previous/next tab in the tab list.discardTab()โ Discard the current tab.reload()โ Open a new buffer with the current bufferโs URL, replacing the current buffer.reshape()โ Reshape the current buffer (=render the current page anew.)redraw()โ Redraw screen contents. Useful if something messed up the display.toggleSource()โ If viewing an HTML buffer, open a new buffer with its source. Otherwise, open the current bufferโs contents as HTML.lineInfo()โ Display information about the current line.searchForward(),searchBackward()โ Search forward/backward for a string in the current buffer.isearchForward(),isearchBackward()โ Incremental-search forward/backward for a string, highlighting the first result.gotoLine(n?)โ Go to the line passed as the first argument. If no arguments were specified, an input window for entering a line is shown.searchNext(n = 1),searchPrev(n = 1)โ Jump to thenth next/previous search result.peek()โ Display an alert message of the current URL.peekCursor()โ Display an alert message of the URL or title under the cursor. Multiple calls allow cycling through the two. (i.e. by default, pressuonce -> title, press again -> URL)showFullAlert()โ Show the last alert inside the line editor.ask(prompt)โ Ask the user for confirmation. Returns a promise which resolves to a boolean value indicating whether the user responded with yes. Can be used to implement an exit prompt like this:
q = 'pager.ask("Do you want to exit Chawan?").then(x => x && pager.quit())'
askChar(prompt)โ Ask the user for any character. Likepager.ask, but the return value is a character, and(y/n)is not appended to the prompt.clipboardWrite(s)โ Writesto the clipboard (copy). By default, it tries using OSC 52; if that fails, it tries to runexternal.copy-cmd(defaults toxsel). Returnstrueif the copy succeeded,falseotherwise. (There may be false positives in case OSC 52 is used and the terminal doesnโt consume the text, although Chawan will try its best to avoid this.)extern(cmd, options = {env: { ... }, suspend: true, wait: false})โ Run an external commandcmd. By default, the$CHA_URLand$CHA_CHARSETvariables are set; change this using theenvoption.options.suspendsuspends the pager while the command is being executed, andoptions.waitmakes it so the user must press a key before the pager is resumed. Returnstrueif the command exited successfully,falseotherwise.externCapture(cmd)โ Likeextern(), but redirect the commandโs stdout string into the result.nullis returned if the command wasnโt executed successfully, or if the command returned a non-zero exit value.externInto(cmd, ins)โ Likeextern(), but redirectinsinto the commandโs standard input stream.trueis returned if the command exits successfully,falseotherwise.externFilterSource(cmd, buffer = null, contentType = null)โ Redirects the specified (or ifbufferisnull, the current) bufferโs source intocmd. Then, it pipes the output into a new buffer, with the content typecontentType(or, ifcontentTypeisnull, the original bufferโs content type). Returnsundefined. (It should return a promise; TODO.)openEditor(text)โ Open โtextโ in the command configured asexternal.editor(this is typically just$EDITOR.) If the editor signals an error (crash or non-zero exit code),nullis returned. Otherwise, the userโs input is returned as a string.openMenu(x?, y?)โ Opens the context menu at the specified position.openBufferMenu(x?, y?)โ Opens the buffer selection menu at the specified position.openCustomMenu(name, init = {x: buffer.acursorx, y: buffer.acursory})โ pops open a custom menu right where you tell it to ๐nameis the menu you defined inmenu.js, andinitis an object with the optional fieldsx/y(both numbers). Peep the Menu section for the deets.
๐จ ok so tea: an older version of this function tookinitas the first parameter and readnameout of the object. That still works, but it's deprecated and is getting yeeted in the future. Don't build new stuff on it, bestie.
closeMenu()โ closes the menu if it's open. bye bestie ๐bufferโ same thing asglobalThis.buffer(and you can grab that without typingglobalThis, i.e. justbuffer). but likeโฆ use that one instead, ok? ๐ซถmenuโ getter for the menu that's currently on screen. Returns aSelectobject.navDirectionโ the direction you last moved in the buffer list usingtraverse. possible values areprev,next,anyโจrevDirectionโ literally justPager.oppositeDir(pager.navDirection). same thing, different font ๐
And also, there's this static function chillin' on Pager itself:
Pager.oppositeDir(dir)โ returns a string for the direction opposite todir. For"next"it's"prev", for"prev"it's"next", and for"any"it'sโฆ the same. iconic non-behavior ๐
๐ฆ Buffer
Every buffer gets exposed as an object implementing the Buffer interface. Want a reference to the buffer that's currently on screen? Just grab buffer. easy ๐
Heads up: remember that deprecated quirk we mentioned earlier, where Pager dispatches some properties onto the current buffer? yeah. still a thing. ๐
These properties (functions/getters) are defined by Buffer:
cursorUp(n = 1),cursorDown(n = 1)โ moves the cursor up/down bynlines, or by 1 if you don't sayn.cursorLeft(n = 1),cursorRight(n = 1)โ moves the cursor left/right byncells, or by 1 ifnis unspecified.
๐ซ note:ncurrently means cells, but honestly it should mean characters. (the difference being that a numberedcursorLeftorcursorRightis currently broken for double-width chars. yes really. it's giving bug.)
cursorLineBegin(),cursorLineEnd()โ moves the cursor to the first/last cell of the line.cursorLineTextStart()โ moves the cursor to the first non-blank character of the line.cursorNextWord(),cursorNextViWord(),cursorNextBigWord()โ moves the cursor to the beginning of the next word.cursorPrevWord(),cursorPrevViWord(),cursorPrevBigWord()โ moves the cursor to the end of the previous word.cursorWordEnd(),cursorViWordEnd(),cursorBigWordEnd()โ moves the cursor to the end of the current word, or if it's already there, to the end of the next word.cursorWordBegin(),cursorViWordBegin(),cursorBigWordBegin()โ moves the cursor to the beginning of the current word, or if it's already there, to the end of the previous word.async getCurrentWord(x = this.cursorx, y = this.cursory)โ returns the word currently under the cursor as a string.cursorNextLink(),cursorPrevLink()โ moves the cursor to the beginning of the next/previous clickable element.cursorLinkNavDown(n = 1),cursorLinkNavUp(n = 1)โ moves the cursor to the beginning of the *n*th next/previous clickable element. the buffer scrolls pagewise, and wraps to the beginning/end if the content is less than one page long ๐ฅcursorNextParagraph(n = 1),cursorPrevParagraph(n = 1)โ moves the cursor to the beginning/end of the *n*th next/previous paragraph.cursorNthLink(n = 1)โ moves the cursor to the *n*th link of the document.cursorRevNthLink(n = 1)โ moves the cursor to the *n*th link of the document, but counting backwards from the document's last line. reverse girlie ๐pageUp(n = 1),pageDown(n = 1),pageLeft(n = 1),pageRight(n = 1)โ scroll up/down/left/right bynpages.halfPageUp(n = 1),halfPageDown(n = 1),halfPageLeft(n = 1),halfPageRight(n = 1)โ scroll up/down/left/right bynhalf pages. half the drama, same energy ๐scrollUp(n = 1),scrollDown(n = 1),scrollLeft(n = 1),scrollRight(n = 1)โ scroll up/down/left/right bynlines.click(n = 1)โ clicks the HTML element currently under the cursor.ncontrols the number of clicks, e.g.n = 2is a double click. (the click count is only relevant in JS apps, fyi ๐)cursorFirstLine(),cursorLastLine()โ moves to the first/last line in the buffer.cursorTop(),cursorMiddle(),cursorBottom()โ moves to the first/middle/bottom line on the screen. (equivalent toH/M/Lin vi, iykyk ๐ธ)lowerPage(n = this.cursory)โ moves the cursor to linen, then scrolls up so the cursor sits on the top line of the screen. (ztin vim.)lowerPageBegin(n = this.cursory)โ moves the cursor to the first non-blank character of linen, then scrolls up so the cursor sits on the top line of the screen. (z<CR>in vi.)centerLine(n = this.cursory)โ centers the screen around linen. (zzin vim.) ๐ฏcenterLineBegin(n = this.cursory)โ centers the screen around linen, and moves the cursor to that line's first non-blank character. (z.in vi.)raisePage(n = this.cursory)โ moves the cursor to linen, then scrolls down so the cursor sits on the top line of the screen. (zbin vim.)raisePageBegin(n = this.cursory)โ moves the cursor to the first non-blank character of linen, then scrolls up so the cursor sits on the last line of the screen. (z^in vi.)nextPageBegin(n = this.cursory)โ ifnwas given, moves to the screen before the *n*th line and raises the page. otherwise, goes to the previous screen's last line and raises the page. (z+in vi.)cursorLeftEdge(),cursorMiddleColumn(),cursorRightEdge()โ moves to the first/middle/last column on the screen.centerColumn()โ centers the screen around the current column. symmetrical slay โจfindPrevMatch(regex, x, y, wrap = false, n = 1),findNextMatch(regex, x, y, wrap = false, n = 1)โ finds the previous/next match for a regex.regexis aRegExpobject (like, from the/this syntax/thing).xandyare the starting position in the buffer,wrapdecides whether the search should wrap over the document or not, andnis how many occurrences to find. Returns an array of elements[x, y, w]wherexandyare the matched coordinates andwis the width of the matched text. If nothing matches, the result is[-1, -1, 0]. no match no problem ๐ญfindNextMark(x = this.cursorx, y = this.cursory),findPrevMark(x = this.cursorx, y = this.cursory)โ finds the next/previous mark after/beforex,yif there is one, and returns its id (ornullif none were found).setMark(id, x = this.cursorx, y = this.cursory)โ sets a mark at (x,y) using the nameid. Returnstrueif no other mark already exists withid. If one does already exist, it gets overridden and the function returnsfalse. ๐clearMark(id)โ clears the mark with the nameid. Returnstrueif the mark existed,falseotherwise.gotoMark(id)โ if the markidexists, jumps to its position and returnstrue. otherwise does nothing and returnsfalse.gotoMarkY(id)โ if the markidexists, jumps to the beginning of the line at its Y position and returnstrue. otherwise does nothing and returnsfalse.getMarkPos(id)โ if the markidexists, returns its position as an array where element zero is the X position and element one is the Y position. otherwise returnsnull. ๐งcursorToggleSelection(n = 1, opts = {selectionType: "normal"})โ starts a vim-style visual selection. the cursor gets moved to the right byncells.selectionTypemay be"normal"(regular selection),"line"(line-based selection) and"column"(column-based selection). pick your fighter ๐getSelectionText()โ gets the currently selected text. returns a promise, so consumers mustawaitit to actually get the text. no cap, don't forget the await ๐markURL()โ converts URL-like strings to anchors on the current page.showLinkHints()โ displays link hints on the page. mainly intended for the built-intoggleLinkHintscommand. returns an array of objects withxrepresenting the x position andythe y position of a link. ๐toggleImages()โ toggles display of images in this buffer.saveLink()โ saves the URL pointed to by the cursor.saveSource()โ saves the source of this buffer. ๐พsetCursorX(x),setCursorY(y),setCursorXY(x, y),setCursorXCenter(x),setCursorYCenter(y),setCursorXYCenter(x, y)โ sets the cursor position toxandyrespectively, scrolling the view if necessary. the variants ending in"Center"also center the screen around the position if it's outside the screen. bougie but useful ๐setFromX(x),setFromY(y),setFromXY(x, y)โ sets the starting position of the displayed area on the screen.find(dir)โ finds the next buffer in the list in a specific direction. possible values ofdirare"prev","next", and"any"."next"and"prev"return the next/previous buffer respectively, while"any"returns either"prev", or if it'snull,"next". chaotic but valid ๐urlโ getter for the buffer's URL.
โ ๏ธ note: this returns a URL object, not a string. do not come for me when .toUpperCase() doesn't exist on it, bestie ๐
hoverTitle,hoverLink,hoverImageโ getter for the string representation of the element title/link/image currently under the cursor. returns the empty string if no title is found.cursorx,cursoryโ the x/y position of the cursor inside the buffer.
fun lil gotcha: the status line display is 1-indexed, but these values are 0-indexed (i.e. cursory = 0 is the first line). so if your math is off by one, that's why ๐ซ
fromx,fromyโ the x/y position of the first line displayed on the screen.acursorx,acursoryโ the absolute x/y position of the cursor on the screen. same asbuffer.cursorx - buffer.fromxandbuffer.cursory - buffer.fromy. the math girlies ate ๐งฎnumLinesโ the number of lines currently loaded in the buffer.width,heightโ the width and height of the buffer's window (i.e. the visible part of the canvas). ๐processโ the process ID of the buffer.titleโ text from the title element, or the buffer's URL if there's no title.nextโ next buffer in the buffer list. may benull.prevโ previous buffer in the buffer list. may benull.selectโ reference to the current select elementโs widget, ornullif no select element is open. This object implements theSelectinterface, which is somewhat compatible with theBufferinterface with some exceptions. (TODO: elaborate)
๐ Select
The currently focused select menu widget can be retrieved using globalThis.select (or, normally, just select). slay.
A select widget โlineโ is defined to include both options and separator lines. Keep that in mind, bestie โ it matters for the cursor stuff.
Following properties (functions/getters) are defined by Select:
click()โ Submit the widget with the selected option. go go go.cancel()โ Close the widget without submitting anything.cursorFirstLine(),cursorLastLine()โ Move to the first/last selectable option.cursorTop(),cursorMiddle(),cursorBottom()โ Move to the top/middle/bottom visible option on the screen.cursorUp(n = 1),cursorDown(n = 1)โ Move the cursor up/down bynoptions. Separator lines are skipped.scrollUp(n = 1),scrollDown(n = 1)โ Scroll the menu up/down bynlines.unselect()โ Set the menu to an โunselectedโ state in which it cannot be submitted until an option is selected again.setCursorY(y)โ Set the cursor to theyโth line. If the line is a separator line, then the menu switches to an โunselectedโ state.width,heightโ Width/height of the select menu.x,yโ x/y position of the menu on the screen.numLinesโ Count of lines in the menu.fromyโ First line to be displayed.cursoryโ Line pointed at by the cursor.
๐ Menu
By default, the main menu and buffer selection menu can be opened using openMenu and openBufferMenu as described above (Pager section).
Aside from these, it is also possible to define custom menus. First, create a file $CHA_DIR/menu.js. (The path can also be changed using external.menu-file.) Then, export your menu as follows:
export function myMenu(m) {
m.item("item not bound to any key", () => console.log("pressed item"))
m.item("item bound to key `k'", () => console.log("pressed item"), "k")
m.line() /* separator line */
m.line("some unselectable text")
m.menu("pop up another menu",
() => pager.openCustomMenu("anotherMenu"))
}
Now you can open the custom menu using pager.openCustomMenu("myMenu"). custom menu era ๐
.
The parameter m is an instance of the SelectBuilder interface, for which public methods are:
item(label, command, key?)โ Add an item to the menu withlabelthat executescommandwhen itโs selected.keyis an optional parameter for a key shortcut.menu(label, command, key?)โ Likeitem, butcommandis expected to open a popup menu. Selecting such items does not close the menu, and hovering over them with the mouse may executecommandautomatically. (Currently, this only happens if the mouse is near the menuโs right border.)line(label?)โ A separator line. Iflabelis not passed, it is set to a line as wide as the menu itself; otherwise, it is alabel(useful for displaying keymap hints).bind(command, key)โ Bind acommandto thekeyspecified, using the syntax described incha-config(config.md#keybinding-format)โs Keybinding format section. Such keybindings are distinct from the configโs[select]section in that they are limited to a single menu, while keys specified in[select]apply to all menus as well as HTML select tags.select()โ Select the previous menu item defined usingitem()ormenu().
โจ LineEdit
The line editor at the bottom of the screen is exposed to the JavaScript context as globalThis.line, or simply line, and implements the LineEdit interface.
Note that there is no singleLineEditobject; a new one is created every time the line editor is opened, and when the line editor is closed,globalThis.linesimply returnsnull. manifesting a fresh line editor every time โจ.
Following properties (functions/getters) are defined by LineEdit:
submit()โ Submit line.cancel()โ Cancel operation.backspace()โ Delete character before cursor.delete()โ Delete character after cursor.clear()โ Clear text before cursor.kill()โ Clear text after cursor.clearWord()โ Delete word before cursor.killWord()โ Delete word after cursor.backward(),forward()โ Move cursor backward/forward by one character.nextWord(),prevWord()โ Move cursor to the next/previous word by one character.begin(),end()โ Move cursor to the beginning/end of the line.escape()โ Ignore keybindings for next character.nextHist(),prevHist()โ Jump to the previous/next history entry.textโ The currently entered text.
๐ See also
Go peek at cha(1) and cha-config(5) too, bestie ๐ซถ.
Thatโs the whole CHA-API(7) page โ footer says CHA-API(7) and thatโs the vibe, mwah ๐๐
๐ฆ where this page comes from
- arch rolling: chawan 0.5.0-1 ยท Unlicense
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms