From 6958a9b8138fbf6f8b746499f96a48df53e41565 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 10:22:04 -0700 Subject: [PATCH 1/7] Bump node deps; add event-emitter dep --- bower.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bower.json b/bower.json index 78f86cd..b8e6cda 100644 --- a/bower.json +++ b/bower.json @@ -21,8 +21,9 @@ "dependencies": { "purescript-effect": "^4.0.0", "purescript-foreign": "^7.0.0", - "purescript-node-process": "^10.0.0", - "purescript-node-streams": "^7.0.0", + "purescript-node-event-emitter": "https://github.com/purescript-node/purescript-node-event-emitter.git#^3.0.0", + "purescript-node-process": "^11.0.0", + "purescript-node-streams": "^8.0.0", "purescript-options": "^7.0.0", "purescript-prelude": "^6.0.0" } From 9379ca956b42511b22ecfca9f1d0b2d6d606b0c3 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 10:33:05 -0700 Subject: [PATCH 2/7] Implement all events --- src/Node/ReadLine.purs | 91 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Node/ReadLine.purs b/src/Node/ReadLine.purs index a92d315..853c0a5 100644 --- a/src/Node/ReadLine.purs +++ b/src/Node/ReadLine.purs @@ -2,11 +2,20 @@ module Node.ReadLine ( Interface + , toEventEmitter , InterfaceOptions , Completer , LineHandler , createInterface , createConsoleInterface + , closeH + , lineH + , historyH + , pauseH + , resumeH + , sigContH + , sigIntH + , sigStpH , output , completer , terminal @@ -23,18 +32,98 @@ import Prelude import Data.Options (Options, Option, (:=), options, opt) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) import Foreign (Foreign) +import Node.EventEmitter (EventEmitter, EventHandle(..)) +import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1) import Node.Process (stdin, stdout) import Node.Stream (Readable, Writable) +import Unsafe.Coerce (unsafeCoerce) -- | A handle to a console interface. -- | -- | A handle can be created with the `createInterface` function. foreign import data Interface :: Type +toEventEmitter :: Interface -> EventEmitter +toEventEmitter = unsafeCoerce + foreign import createInterfaceImpl :: Foreign -> Effect Interface +-- | The 'close' event is emitted when one of the following occur: +-- | +-- | - The `rl.close()` method is called and the readline.Interface instance has relinquished control over the input and output streams; +-- | - The input stream receives its 'end' event; +-- | - The input stream receives `Ctrl+D` to signal end-of-transmission (`EOT`); +-- | - The input stream receives `Ctrl+C` to signal `SIGINT` and there is no `SIGINT` event listener registered on the readline.Interface instance. +-- | +-- | The listener function is called without passing any arguments. +-- | +-- | The `readline.Interface` instance is finished once the `close` event is emitted. +closeH :: EventHandle0 Interface +closeH = EventHandle "close" identity + +-- | The 'line' event is emitted whenever the input stream receives an end-of-line input (`\n`, `\r`, or `\r\n`). +-- | This usually occurs when the user presses Enter or Return. +-- | +-- | The 'line' event is also emitted if new data has been read from a stream and that +-- | stream ends without a final end-of-line marker. +-- | +-- | The listener function is called with a string containing the single line of received input. +lineH :: EventHandle1 Interface String +lineH = EventHandle "line" mkEffectFn1 + +-- | The 'history' event is emitted whenever the history array has changed. +-- | +-- | The listener function is called with an array containing the history array. It will reflect all changes, added lines and removed lines due to historySize and removeHistoryDuplicates. +-- | +-- | The primary purpose is to allow a listener to persist the history. It is also possible for the listener to change the history object. This could be useful to prevent certain lines to be added to the history, like a password. +historyH :: EventHandle1 Interface (Array String) +historyH = EventHandle "history" mkEffectFn1 + +-- | The 'pause' event is emitted when one of the following occur: +-- | +-- | - The input stream is paused. +-- | - The input stream is not paused and receives the 'SIGCONT' event. (See events 'SIGTSTP' and 'SIGCONT'.) +-- | +-- | The listener function is called without passing any arguments. +pauseH :: EventHandle0 Interface +pauseH = EventHandle "pause" identity + +-- | The 'resume' event is emitted whenever the input stream is resumed. +-- | +-- | The listener function is called without passing any arguments. +resumeH :: EventHandle0 Interface +resumeH = EventHandle "resume" identity + +-- | The 'SIGCONT' event is emitted when a Node.js process previously moved into the background using Ctrl+Z (i.e. SIGTSTP) is then brought back to the foreground using fg(1p). +-- | +-- | If the input stream was paused before the SIGTSTP request, this event will not be emitted. +-- | +-- | The listener function is invoked without passing any arguments. +-- | +-- | **The 'SIGCONT' event is not supported on Windows.** +sigContH :: EventHandle0 Interface +sigContH = EventHandle "SIGCONT" identity + +-- | The 'SIGINT' event is emitted whenever the input stream receives a Ctrl+C input, known typically as SIGINT. If there are no 'SIGINT' event listeners registered when the input stream receives a SIGINT, the 'pause' event will be emitted. +-- | +-- | The listener function is invoked without passing any arguments. +sigIntH :: EventHandle0 Interface +sigIntH = EventHandle "SIGINT" identity + +-- | The 'SIGTSTP' event is emitted when the input stream receives a Ctrl+Z input, typically known as SIGTSTP. If there are no 'SIGTSTP' event listeners registered when the input stream receives a SIGTSTP, the Node.js process will be sent to the background. +-- | +-- | When the program is resumed using fg(1p), the 'pause' and 'SIGCONT' events will be emitted. These can be used to resume the input stream. +-- | +-- | The 'pause' and 'SIGCONT' events will not be emitted if the input was paused before the process was sent to the background. +-- | +-- | The listener function is invoked without passing any arguments. +-- | +-- | **The 'SIGTSTP' event is not supported on Windows.** +sigStpH :: EventHandle0 Interface +sigStpH = EventHandle "SIGSTP" identity + -- | Options passed to `readline`'s `createInterface` foreign import data InterfaceOptions :: Type From 2aa9e0b2e3d05cd28a58facab2ae5c0ef7922439 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 10:39:37 -0700 Subject: [PATCH 3/7] Remove setLineHandler & LineHandler --- CHANGELOG.md | 16 ++++++++++++++++ src/Node/ReadLine.js | 4 ---- src/Node/ReadLine.purs | 22 +++++++++++----------- test/Main.purs | 6 +++--- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 510d1d6..90440a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ Notable changes to this project are documented in this file. The format is based ## [Unreleased] Breaking changes: +- Removed `setLineHandler` and `LineHandler` type alias (#34 by @JordanMartinez) + + `setLineHandler` was previously implemented as + ```js + readline.removeAllListeners("line"); + readline.on("line", cb); + + With the addition of bindings from `EventEmitter`, + this can be done using `on` + ```purs + example = do + removeListener <- interface # on lineH \line -> do + ... + ... + removeListener + ``` New features: diff --git a/src/Node/ReadLine.js b/src/Node/ReadLine.js index b8694c1..66fb9ff 100644 --- a/src/Node/ReadLine.js +++ b/src/Node/ReadLine.js @@ -17,7 +17,3 @@ export const closeImpl = (readline) => readline.close(); export const promptImpl = (readline) => readline.prompt(); export const questionImpl = (readline, text, cb) => readline.question(text, cb); export const setPromptImpl = (readline, prompt) => readline.setPrompt(prompt); -export const setLineHandlerImpl = (readline, cb) => { - readline.removeAllListeners("line"); - readline.on("line", cb); -}; diff --git a/src/Node/ReadLine.purs b/src/Node/ReadLine.purs index 853c0a5..6a71867 100644 --- a/src/Node/ReadLine.purs +++ b/src/Node/ReadLine.purs @@ -5,7 +5,6 @@ module Node.ReadLine , toEventEmitter , InterfaceOptions , Completer - , LineHandler , createInterface , createConsoleInterface , closeH @@ -23,7 +22,6 @@ module Node.ReadLine , noCompletion , prompt , setPrompt - , setLineHandler , close , question ) where @@ -43,6 +41,13 @@ import Unsafe.Coerce (unsafeCoerce) -- | A handle to a console interface. -- | -- | A handle can be created with the `createInterface` function. +-- | +-- | `Interface` extends `EventEmiter` +-- | +-- | From Node docs v18: +-- | > Instances of the `readline.Interface` class are constructed using the `readline.createInterface()` method. +-- | > Every instance is associated with a single input Readable stream and a single output Writable stream. +-- | > The output stream is used to print prompts for user input that arrives on, and is read from, the input stream. foreign import data Interface :: Type toEventEmitter :: Interface -> EventEmitter @@ -193,16 +198,11 @@ setPrompt newPrompt iface = runEffectFn2 setPromptImpl iface newPrompt foreign import setPromptImpl :: EffectFn2 (Interface) (String) (Unit) -- | Close the specified `Interface`. +-- | +-- | The rl.close() method closes the readline.Interface instance and relinquishes control over the input and output streams. When called, the 'close' event will be emitted. +-- | +-- | Calling rl.close() does not immediately stop other events (including 'line') from being emitted by the readline.Interface instance. close :: Interface -> Effect Unit close iface = runEffectFn1 closeImpl iface foreign import closeImpl :: EffectFn1 (Interface) (Unit) - --- | A function which handles each line of input. -type LineHandler a = String -> Effect a - --- | Set the current line handler function. -setLineHandler :: forall a. LineHandler a -> Interface -> Effect Unit -setLineHandler cb iface = runEffectFn2 setLineHandlerImpl iface cb - -foreign import setLineHandlerImpl :: forall a. EffectFn2 (Interface) (LineHandler a) (Unit) diff --git a/test/Main.purs b/test/Main.purs index 4ddb177..3865323 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -4,15 +4,15 @@ import Prelude import Effect (Effect) import Effect.Console (log) - -import Node.ReadLine (prompt, close, setLineHandler, setPrompt, noCompletion, createConsoleInterface) +import Node.EventEmitter (on_) +import Node.ReadLine (close, createConsoleInterface, lineH, noCompletion, prompt, setPrompt) main :: Effect Unit main = do interface <- createConsoleInterface noCompletion setPrompt "> " interface prompt interface - interface # setLineHandler \s -> + interface # on_ lineH \s -> if s == "quit" then close interface else do log $ "You typed: " <> s From 4426875239e16f5389d179898d04053ad6618c17 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 11:43:59 -0700 Subject: [PATCH 4/7] Fix node-process FFI issue --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index b8e6cda..78eaaf3 100644 --- a/bower.json +++ b/bower.json @@ -22,7 +22,7 @@ "purescript-effect": "^4.0.0", "purescript-foreign": "^7.0.0", "purescript-node-event-emitter": "https://github.com/purescript-node/purescript-node-event-emitter.git#^3.0.0", - "purescript-node-process": "^11.0.0", + "purescript-node-process": "^11.0.1", "purescript-node-streams": "^8.0.0", "purescript-options": "^7.0.0", "purescript-prelude": "^6.0.0" From c3e6e0cc06bf2297f4db3444277fbe804d9c2d28 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 11:45:46 -0700 Subject: [PATCH 5/7] Add missing APIs --- CHANGELOG.md | 18 ++ src/Node/ReadLine.js | 47 +++++- src/Node/ReadLine.purs | 371 ++++++++++++++++++++++++++++++++++------- 3 files changed, 366 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90440a2..38e24c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,23 @@ Breaking changes: ``` New features: +- Added missing `createInterface` options (#35 by @JordanMartinez) + + - history + - removeHistoryDuplicates + - prompt + - crlfDelay + - escapeCodeTimeout + - tabSize +- Added missing APIs (#35 by @JordanMartinez) + + - `pause`/`resume` + - `getPrompt` + - `write` exposed as `writeData` and `writeKey` + - `line`, `cursor` + - `getCursorPos`, `clearLine` variants, `clearScreenDown` variants + - `cursorTo` variants, `moveCursor` variants + - `emitKeyPressEvents` Bugfixes: @@ -31,6 +48,7 @@ Other improvements: - Update CI actions to `v3` (#31, #32 by @JordanMartinez) - Format code via `purs-tidy`; enforce formatting in CI (#31, #32 by @JordanMartinez) - Update FFI to use uncurried functions (#33 by @JordanMartinez) +- Reordered export list (#35 by @JordanMartinez) ## [v7.0.0](https://github.com/purescript-node/purescript-node-readline/releases/tag/v7.0.0) - 2022-04-29 diff --git a/src/Node/ReadLine.js b/src/Node/ReadLine.js index 66fb9ff..2dbfe9a 100644 --- a/src/Node/ReadLine.js +++ b/src/Node/ReadLine.js @@ -1,8 +1,8 @@ -// module Node.ReadLine +// module Node.rl -import { createInterface } from "readline"; +import readline from "node:readline"; -export const createInterfaceImpl = (options) => createInterface({ +export const createInterfaceImpl = (options) => readline.createInterface({ input: options.input, output: options.output, completer: options.completer && (line => { @@ -10,10 +10,43 @@ export const createInterfaceImpl = (options) => createInterface({ return [res.completions, res.matched]; }), terminal: options.terminal, + history: options.history, historySize: options.historySize, + removeHistoryDuplicates: options.removeHistoryDuplicates, + prompt: options.prompt, + crlDelay: options.crlDelay, + escapeCodeTimeout: options.escapeCodeTimeout, + tabSize: options.tabSize, + signal: options.signal }); -export const closeImpl = (readline) => readline.close(); -export const promptImpl = (readline) => readline.prompt(); -export const questionImpl = (readline, text, cb) => readline.question(text, cb); -export const setPromptImpl = (readline, prompt) => readline.setPrompt(prompt); +export const closeImpl = (rl) => rl.close(); +export const pauseImpl = (rl) => rl.pause(); +export const promptImpl = (rl) => rl.prompt(); +export const promptOptsImpl = (rl, cursor) => rl.prompt(cursor); +export const questionImpl = (rl, text, cb) => rl.question(text, cb); +export const resumeImpl = (rl) => rl.resume(); +export const setPromptImpl = (rl, prompt) => rl.setPrompt(prompt); +export const getPromptImpl = (rl) => rl.getPrompt(); +export const writeDataImpl = (rl, dataStr) => rl.write(dataStr); +export const writeKeyImpl = (rl, keySeqObj) => rl.write(null, keySeqObj); +export const lineImpl = (rl) => rl.line; +export const cursorImpl = (rl) => rl.cursor; +export const getCursorPosImpl = (rl) => rl.getCursorPos(); + +export const clearLineLeftImpl = (w) => readline.clearLine(w, -1); +export const clearLineLeftCbImpl = (w) => readline.clearLine(w, -1, cb); +export const clearLineRightImpl = (w) => readline.clearLine(w, 1); +export const clearLineRightCbImpl = (w) => readline.clearLine(w, 1, cb); +export const clearEntireLineImpl = (w) => readline.clearLine(w, 0); +export const clearEntireLineCbImpl = (w) => readline.clearLine(w, 0, cb); +export const clearScreenDownImpl = (w) => readline.clearScreenDown(w); +export const clearScreenDownCbImpl = (w, cb) => readline.clearScreenDown(w, cb); +export const cursorToXImpl = (w, x) => readline.cursorTo(w, x); +export const cursorToXCbImpl = (w, x, cb) => readline.cursorTo(w, x, cb); +export const cursorToXYImpl = (w, x, y) => readline.cursorTo(w, x, y); +export const cursorToXYCbImpl = (w, x, y, cb) => readline.cursorTo(w, x, y, cb); +export const emitKeyPressEventsImpl = (r) => readline.emitKeypressEvents(r); +export const emitKeyPressEventsIfaceImpl = (r, iface) => readline.emitKeypressEvents(r, iface); +export const moveCursorXYImpl = (w, x, y) => readline.moveCursor(w, x, y); +export const moveCursorXYCbImpl = (w, x, y, cb) => readline.moveCursor(w, x, y, cb); diff --git a/src/Node/ReadLine.purs b/src/Node/ReadLine.purs index 6a71867..6c225d9 100644 --- a/src/Node/ReadLine.purs +++ b/src/Node/ReadLine.purs @@ -3,10 +3,21 @@ module Node.ReadLine ( Interface , toEventEmitter - , InterfaceOptions - , Completer , createInterface , createConsoleInterface + , InterfaceOptions + , output + , Completer + , completer + , noCompletion + , terminal + , history + , historySize + , removeHistoryDuplicates + , promptStr + , crlfDelay + , escapeCodeTimeout + , tabSize , closeH , lineH , historyH @@ -15,22 +26,43 @@ module Node.ReadLine , sigContH , sigIntH , sigStpH - , output - , completer - , terminal - , historySize - , noCompletion - , prompt - , setPrompt , close + , pause + , prompt + , prompt' , question + , resume + , setPrompt + , getPrompt + , writeData + , writeKey + , line + , cursor + , getCursorPos + , clearLineLeft + , clearLineLeft' + , clearLineRight + , clearLineRight' + , clearEntireLine + , clearEntireLine' + , clearScreenDown + , clearScreenDown' + , cursorToX + , cursorToX' + , cursorToXY + , cursorToXY' + , emitKeyPressEvents + , emitKeyPressEvents' + , moveCursorXY + , moveCursorXY' ) where import Prelude import Data.Options (Options, Option, (:=), options, opt) +import Data.Time.Duration (Milliseconds) import Effect (Effect) -import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4) import Foreign (Foreign) import Node.EventEmitter (EventEmitter, EventHandle(..)) import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1) @@ -53,7 +85,102 @@ foreign import data Interface :: Type toEventEmitter :: Interface -> EventEmitter toEventEmitter = unsafeCoerce -foreign import createInterfaceImpl :: Foreign -> Effect Interface +-- | Builds an interface with the specified options. +-- | The `Readable` arg to this function +-- | will be used as the `input` option below: +-- | +-- | - `input` The Readable stream to listen to. This option is required. +-- | - `output` The Writable stream to write readline data to. +-- | - `completer` An optional function used for Tab autocompletion. +-- | - `terminal` true if the input and output streams should be treated like a TTY, and have ANSI/VT100 escape codes written to it. Default: checking isTTY on the output stream upon instantiation. +-- | - `history` Initial list of history lines. This option makes sense only if terminal is set to true by the user or by an internal output check, otherwise the history caching mechanism is not initialized at all. Default: []. +-- | - `historySize` Maximum number of history lines retained. To disable the history set this value to 0. This option makes sense only if terminal is set to true by the user or by an internal output check, otherwise the history caching mechanism is not initialized at all. Default: 30. +-- | - `removeHistoryDuplicates` If true, when a new input line added to the history list duplicates an older one, this removes the older line from the list. Default: false. +-- | - `prompt` The prompt string to use. Default: '> '. +-- | - `crlfDelay` If the delay between \r and \n exceeds crlfDelay milliseconds, both \r and \n will be treated as separate end-of-line input. crlfDelay will be coerced to a number no less than 100. It can be set to Infinity, in which case \r followed by \n will always be considered a single newline (which may be reasonable for reading files with \r\n line delimiter). Default: 100. +-- | - `escapeCodeTimeout` The duration readline will wait for a character (when reading an ambiguous key sequence in milliseconds one that can both form a complete key sequence using the input read so far and can take additional input to complete a longer key sequence). Default: 500. +-- | - `tabSize` The number of spaces a tab is equal to (minimum 1). Default: 8. +-- | - `signal` Allows closing the interface using an AbortSignal. Aborting the signal will internally call close on the interface. +createInterface + :: forall r + . Readable r + -> Options InterfaceOptions + -> Effect Interface +createInterface input opts = runEffectFn1 createInterfaceImpl + $ options + $ opts + <> opt "input" := input + +foreign import createInterfaceImpl :: EffectFn1 Foreign Interface + +-- | Create an interface with the specified completion function. +createConsoleInterface :: Completer -> Effect Interface +createConsoleInterface compl = + createInterface stdin + $ output := stdout + <> completer := compl + +-- | Options passed to `readline`'s `createInterface` +-- | +-- | - `input` The Readable stream to listen to. This option is required. +-- | - `output` The Writable stream to write readline data to. +-- | - `completer` An optional function used for Tab autocompletion. +-- | - `terminal` true if the input and output streams should be treated like a TTY, and have ANSI/VT100 escape codes written to it. Default: checking isTTY on the output stream upon instantiation. +-- | - `history` Initial list of history lines. This option makes sense only if terminal is set to true by the user or by an internal output check, otherwise the history caching mechanism is not initialized at all. Default: []. +-- | - `historySize` Maximum number of history lines retained. To disable the history set this value to 0. This option makes sense only if terminal is set to true by the user or by an internal output check, otherwise the history caching mechanism is not initialized at all. Default: 30. +-- | - `removeHistoryDuplicates` If true, when a new input line added to the history list duplicates an older one, this removes the older line from the list. Default: false. +-- | - `prompt` The prompt string to use. Default: '> '. +-- | - `crlfDelay` If the delay between \r and \n exceeds crlfDelay milliseconds, both \r and \n will be treated as separate end-of-line input. crlfDelay will be coerced to a number no less than 100. It can be set to Infinity, in which case \r followed by \n will always be considered a single newline (which may be reasonable for reading files with \r\n line delimiter). Default: 100. +-- | - `escapeCodeTimeout` The duration readline will wait for a character (when reading an ambiguous key sequence in milliseconds one that can both form a complete key sequence using the input read so far and can take additional input to complete a longer key sequence). Default: 500. +-- | - `tabSize` The number of spaces a tab is equal to (minimum 1). Default: 8. +-- | - `signal` Allows closing the interface using an AbortSignal. Aborting the signal will internally call close on the interface. +foreign import data InterfaceOptions :: Type + +output :: forall w. Option InterfaceOptions (Writable w) +output = opt "output" + +completer :: Option InterfaceOptions Completer +completer = opt "completer" + +-- | A function which performs tab completion. +-- | +-- | This function takes the partial command as input, and returns a collection of +-- | completions, as well as the matched portion of the input string. +type Completer = + String + -> Effect + { completions :: Array String + , matched :: String + } + +-- | A completion function which offers no completions. +noCompletion :: Completer +noCompletion s = pure { completions: [], matched: s } + +terminal :: Option InterfaceOptions Boolean +terminal = opt "terminal" + +history :: Option InterfaceOptions (Array String) +history = opt "historySize" + +historySize :: Option InterfaceOptions Int +historySize = opt "historySize" + +removeHistoryDuplicates :: Option InterfaceOptions Boolean +removeHistoryDuplicates = opt "removeHistoryDuplicates" + +-- | Option for `prompt`; name is changed to prevent naming clash with `prompt` function +promptStr :: Option InterfaceOptions String +promptStr = opt "prompt" + +crlfDelay :: Option InterfaceOptions Milliseconds +crlfDelay = opt "crlfDelay" + +escapeCodeTimeout :: Option InterfaceOptions Milliseconds +escapeCodeTimeout = opt "escapeCodeTimeout" + +tabSize :: Option InterfaceOptions Int +tabSize = opt "tabSize" -- | The 'close' event is emitted when one of the following occur: -- | @@ -129,53 +256,23 @@ sigIntH = EventHandle "SIGINT" identity sigStpH :: EventHandle0 Interface sigStpH = EventHandle "SIGSTP" identity --- | Options passed to `readline`'s `createInterface` -foreign import data InterfaceOptions :: Type - -output :: forall w. Option InterfaceOptions (Writable w) -output = opt "output" - -completer :: Option InterfaceOptions Completer -completer = opt "completer" - -terminal :: Option InterfaceOptions Boolean -terminal = opt "terminal" - -historySize :: Option InterfaceOptions Int -historySize = opt "historySize" - --- | A function which performs tab completion. +-- | Close the specified `Interface`. -- | --- | This function takes the partial command as input, and returns a collection of --- | completions, as well as the matched portion of the input string. -type Completer = - String - -> Effect - { completions :: Array String - , matched :: String - } +-- | The rl.close() method closes the readline.Interface instance and relinquishes control over the input and output streams. When called, the 'close' event will be emitted. +-- | +-- | Calling rl.close() does not immediately stop other events (including 'line') from being emitted by the readline.Interface instance. +close :: Interface -> Effect Unit +close iface = runEffectFn1 closeImpl iface --- | Builds an interface with the specified options. -createInterface - :: forall r - . Readable r - -> Options InterfaceOptions - -> Effect Interface -createInterface input opts = createInterfaceImpl - $ options - $ opts - <> opt "input" := input +foreign import closeImpl :: EffectFn1 (Interface) (Unit) --- | Create an interface with the specified completion function. -createConsoleInterface :: Completer -> Effect Interface -createConsoleInterface compl = - createInterface stdin - $ output := stdout - <> completer := compl +-- | The rl.pause() method pauses the input stream, allowing it to be resumed later if necessary. +-- | +-- | Calling rl.pause() does not immediately pause other events (including 'line') from being emitted by the readline.Interface instance. +pause :: Interface -> Effect Unit +pause iface = runEffectFn1 pauseImpl iface --- | A completion function which offers no completions. -noCompletion :: Completer -noCompletion s = pure { completions: [], matched: s } +foreign import pauseImpl :: EffectFn1 (Interface) (Unit) -- | Prompt the user for input on the specified `Interface`. prompt :: Interface -> Effect Unit @@ -183,26 +280,174 @@ prompt iface = runEffectFn1 promptImpl iface foreign import promptImpl :: EffectFn1 (Interface) (Unit) +-- | - `preserveCursor` If true, prevents the cursor placement from being reset to 0. +-- | +-- | The rl.prompt() method writes the readline.Interface instances configured prompt to a new line in output in order to provide a user with a new location at which to provide input. +-- | +-- | When called, rl.prompt() will resume the input stream if it has been paused. +-- | +-- | If the readline.Interface was created with output set to null or undefined the prompt is not written. +prompt' :: Boolean -> Interface -> Effect Unit +prompt' preserveCursor iface = runEffectFn2 promptOptsImpl preserveCursor iface + +foreign import promptOptsImpl :: EffectFn2 (Boolean) (Interface) (Unit) + -- | Writes a query to the output, waits -- | for user input to be provided on input, then invokes -- | the callback function +-- | +-- | Args: +-- | - `query` A statement or query to write to output, prepended to the prompt. +-- | - `options` +-- | - `signal` Optionally allows the question() to be canceled using an AbortController. +-- | - `callback` A callback function that is invoked with the user's input in response to the query. +-- | +-- | The `rl.question()` method displays the query by writing it to the output, waits for user input to be provided on input, then invokes the callback function passing the provided input as the first argument. +-- | +-- | When called, `rl.question()` will resume the input stream if it has been paused. +-- | +-- | If the readline.Interface was created with output set to null or undefined the query is not written. +-- | +-- | The callback function passed to `rl.question()` does not follow the typical pattern of accepting an Error object or null as the first argument. The callback is called with the provided answer as the only argument. question :: String -> (String -> Effect Unit) -> Interface -> Effect Unit question text cb iface = runEffectFn3 questionImpl iface text cb foreign import questionImpl :: EffectFn3 (Interface) (String) ((String -> Effect Unit)) Unit --- | Set the prompt. +-- | The rl.resume() method resumes the input stream if it has been paused. +resume :: Interface -> Effect Unit +resume iface = runEffectFn1 resumeImpl iface + +foreign import resumeImpl :: EffectFn1 (Interface) (Unit) + +-- | The rl.setPrompt() method sets the prompt that will be written to output whenever rl.prompt() is called. setPrompt :: String -> Interface -> Effect Unit setPrompt newPrompt iface = runEffectFn2 setPromptImpl iface newPrompt foreign import setPromptImpl :: EffectFn2 (Interface) (String) (Unit) --- | Close the specified `Interface`. --- | --- | The rl.close() method closes the readline.Interface instance and relinquishes control over the input and output streams. When called, the 'close' event will be emitted. --- | --- | Calling rl.close() does not immediately stop other events (including 'line') from being emitted by the readline.Interface instance. -close :: Interface -> Effect Unit -close iface = runEffectFn1 closeImpl iface +getPrompt :: Interface -> Effect String +getPrompt iface = runEffectFn1 getPromptImpl iface -foreign import closeImpl :: EffectFn1 (Interface) (Unit) +foreign import getPromptImpl :: EffectFn1 (Interface) (String) + +writeData :: String -> Interface -> Effect Unit +writeData dataStr iface = runEffectFn2 writeDataImpl dataStr iface + +foreign import writeDataImpl :: EffectFn2 (String) (Interface) (Unit) + +-- | - `name` The name of the a key. +-- | - `ctrl` true to indicate the Ctrl key. +-- | - `meta` true to indicate the Meta key. +-- | - `shift` true to indicate the Shift key. +type KeySequenceObj = + { name :: String + , ctrl :: Boolean + , meta :: Boolean + , shift :: Boolean + } + +writeKey :: KeySequenceObj -> Interface -> Effect Unit +writeKey keySeq iface = runEffectFn2 writeKeyImpl keySeq iface + +foreign import writeKeyImpl :: EffectFn2 (KeySequenceObj) (Interface) (Unit) + +line :: Interface -> Effect String +line iface = runEffectFn1 lineImpl iface + +foreign import lineImpl :: EffectFn1 (Interface) (String) + +cursor :: Interface -> Effect Int +cursor iface = runEffectFn1 cursorImpl iface + +foreign import cursorImpl :: EffectFn1 (Interface) (Int) + +type CursorPos = + { rows :: Int + , cols :: Int + } + +getCursorPos :: Interface -> Effect CursorPos +getCursorPos iface = runEffectFn1 getCursorPosImpl iface + +foreign import getCursorPosImpl :: EffectFn1 (Interface) (CursorPos) + +clearLineLeft :: forall r. Writable r -> Effect Boolean +clearLineLeft stream = runEffectFn1 clearLineLeftImpl stream + +foreign import clearLineLeftImpl :: forall r. EffectFn1 (Writable r) (Boolean) + +clearLineLeft' :: forall r. Writable r -> Effect Unit -> Effect Boolean +clearLineLeft' stream cb = runEffectFn2 clearLineLeftCbImpl stream cb + +foreign import clearLineLeftCbImpl :: forall r. EffectFn2 (Writable r) (Effect Unit) (Boolean) + +clearLineRight :: forall r. Writable r -> Effect Boolean +clearLineRight stream = runEffectFn1 clearLineRightImpl stream + +foreign import clearLineRightImpl :: forall r. EffectFn1 (Writable r) (Boolean) + +clearLineRight' :: forall r. Writable r -> Effect Unit -> Effect Boolean +clearLineRight' stream cb = runEffectFn2 clearLineRightCbImpl stream cb + +foreign import clearLineRightCbImpl :: forall r. EffectFn2 (Writable r) (Effect Unit) (Boolean) + +clearEntireLine :: forall r. Writable r -> Effect Boolean +clearEntireLine stream = runEffectFn1 clearEntireLineImpl stream + +foreign import clearEntireLineImpl :: forall r. EffectFn1 (Writable r) (Boolean) + +clearEntireLine' :: forall r. Writable r -> Effect Unit -> Effect Boolean +clearEntireLine' stream cb = runEffectFn2 clearEntireLineCbImpl stream cb + +foreign import clearEntireLineCbImpl :: forall r. EffectFn2 (Writable r) (Effect Unit) (Boolean) + +clearScreenDown :: forall r. Writable r -> Effect Boolean +clearScreenDown stream = runEffectFn1 clearScreenDownImpl stream + +foreign import clearScreenDownImpl :: forall r. EffectFn1 (Writable r) (Boolean) + +clearScreenDown' :: forall r. Writable r -> Effect Unit -> Effect Boolean +clearScreenDown' stream cb = runEffectFn2 clearScreenDownCbImpl stream cb + +foreign import clearScreenDownCbImpl :: forall r. EffectFn2 (Writable r) (Effect Unit) (Boolean) + +cursorToX :: forall r. Writable r -> Int -> Effect Boolean +cursorToX stream x = runEffectFn2 cursorToXImpl stream x + +foreign import cursorToXImpl :: forall r. EffectFn2 (Writable r) (Int) (Boolean) + +cursorToX' :: forall r. Writable r -> Int -> Effect Unit -> Effect Boolean +cursorToX' stream x cb = runEffectFn3 cursorToXCbImpl stream x cb + +foreign import cursorToXCbImpl :: forall r. EffectFn3 (Writable r) (Int) (Effect Unit) (Boolean) + +cursorToXY :: forall r. Writable r -> Int -> Int -> Effect Boolean +cursorToXY stream x y = runEffectFn3 cursorToXYImpl stream x y + +foreign import cursorToXYImpl :: forall r. EffectFn3 (Writable r) (Int) (Int) (Boolean) + +cursorToXY' :: forall r. Writable r -> Int -> Int -> Effect Unit -> Effect Boolean +cursorToXY' stream x y cb = runEffectFn4 cursorToXYCbImpl stream x y cb + +foreign import cursorToXYCbImpl :: forall r. EffectFn4 (Writable r) (Int) (Int) (Effect Unit) (Boolean) + +emitKeyPressEvents :: forall w. Readable w -> Effect Unit +emitKeyPressEvents stream = runEffectFn1 emitKeyPressEventsImpl stream + +foreign import emitKeyPressEventsImpl :: forall w. EffectFn1 (Readable w) (Unit) + +emitKeyPressEvents' :: forall w. Readable w -> Interface -> Effect Unit +emitKeyPressEvents' stream iface = runEffectFn2 emitKeyPressEventsIfaceImpl stream iface + +foreign import emitKeyPressEventsIfaceImpl :: forall w. EffectFn2 (Readable w) (Interface) (Unit) + +moveCursorXY :: forall r. Writable r -> Int -> Int -> Effect Boolean +moveCursorXY stream x y = runEffectFn3 moveCursorXYImpl stream x y + +foreign import moveCursorXYImpl :: forall r. EffectFn3 (Writable r) (Int) (Int) (Boolean) + +moveCursorXY' :: forall r. Writable r -> Int -> Int -> Effect Unit -> Effect Boolean +moveCursorXY' stream x y cb = runEffectFn4 moveCursorXYCbImpl stream x y cb + +foreign import moveCursorXYCbImpl :: forall r. EffectFn4 (Writable r) (Int) (Int) (Effect Unit) (Boolean) From 578b6d468c3830793859bced0613c2da53580976 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 11:46:28 -0700 Subject: [PATCH 6/7] Update prompt to clarify how to stop in test --- test/Main.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Main.purs b/test/Main.purs index 3865323..9116b09 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -10,7 +10,7 @@ import Node.ReadLine (close, createConsoleInterface, lineH, noCompletion, prompt main :: Effect Unit main = do interface <- createConsoleInterface noCompletion - setPrompt "> " interface + setPrompt "(type 'quit' to stop)\n> " interface prompt interface interface # on_ lineH \s -> if s == "quit" then close interface From e18b1d31695f9ef193decfb1f6205dc0b1bba695 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Fri, 21 Jul 2023 11:51:29 -0700 Subject: [PATCH 7/7] Add missing cb arg in FFI --- src/Node/ReadLine.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/ReadLine.js b/src/Node/ReadLine.js index 2dbfe9a..8792a35 100644 --- a/src/Node/ReadLine.js +++ b/src/Node/ReadLine.js @@ -35,11 +35,11 @@ export const cursorImpl = (rl) => rl.cursor; export const getCursorPosImpl = (rl) => rl.getCursorPos(); export const clearLineLeftImpl = (w) => readline.clearLine(w, -1); -export const clearLineLeftCbImpl = (w) => readline.clearLine(w, -1, cb); +export const clearLineLeftCbImpl = (w, cb) => readline.clearLine(w, -1, cb); export const clearLineRightImpl = (w) => readline.clearLine(w, 1); -export const clearLineRightCbImpl = (w) => readline.clearLine(w, 1, cb); +export const clearLineRightCbImpl = (w, cb) => readline.clearLine(w, 1, cb); export const clearEntireLineImpl = (w) => readline.clearLine(w, 0); -export const clearEntireLineCbImpl = (w) => readline.clearLine(w, 0, cb); +export const clearEntireLineCbImpl = (w, cb) => readline.clearLine(w, 0, cb); export const clearScreenDownImpl = (w) => readline.clearScreenDown(w); export const clearScreenDownCbImpl = (w, cb) => readline.clearScreenDown(w, cb); export const cursorToXImpl = (w, x) => readline.cursorTo(w, x);