From d9305e28f980befa43f9b248cc07ac9c98bb6ca7 Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Thu, 30 Jun 2022 20:37:55 -0700 Subject: [PATCH 1/6] add some command linking and (bad) frecency links --- server.js | 126 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 33 deletions(-) diff --git a/server.js b/server.js index 06490cc..9c18b4c 100644 --- a/server.js +++ b/server.js @@ -2,12 +2,16 @@ import { execSync } from 'child_process' import path from 'path' import { fileURLToPath } from 'url' +import flatfile from 'flat-file-db' // docs: https://github.com/mafintosh/flat-file-db#api + import server from 'server' const { get, post } = server.router const __dirname = path.dirname(fileURLToPath(import.meta.url)) let nb = path.resolve(__dirname, 'nb.js') +const db = flatfile.sync(path.join(__dirname, 'server-config.db')) + // this function is only used by the web code // do not add any node-specific stuff in it // do not run it outside of the rendered output @@ -18,31 +22,91 @@ async function doCommand(event) { outputElement.innerHTML += text } -function getCommandArgs(ctx, args = [], extra = '') { - return `${args.map(arg => ctx.params[arg]).join(' ')} ${extra}`.replace(/\s{2,}/g,' ').trim() +function makeCommandsLinks(ctx, input) { + let isInCommandSection = false + let commandRegExp = /(nb\.js)\s+((?:(?!&|<|\[| ).)+)\s/ + + let lines = input.split('\n') + .map(line => { + + if (line.startsWith('nb.js')) { + let parsedLine = '' + line.split(' ').reduce((itemsSoFar, item) => { + let isAnArg = item.startsWith('&') || item.startsWith('[') + if (item !== 'nb.js' && !isAnArg) { + itemsSoFar.push(item) + } + if (!isAnArg) { + parsedLine += `${item} ` + } else { + parsedLine += `${item} ` + } + return itemsSoFar + }, []) + return parsedLine + } + + if (line.startsWith('Commands:')) { + isInCommandSection = true + } + + else if (isInCommandSection && line.startsWith(' ')) { + return line.replace(commandRegExp, `$1 $2 `) + } + + else if (isInCommandSection && !line.startsWith(' ')) { + isInCommandSection = false + } + + return line + }) + return lines.join("\n") } -function runAndRenderPage(ctx, args = [], extra = '') { +function sanitizeHtml(input) { + return input.replace(//g, '>') +} + +function runAndRenderPage(ctx, commandString) { return `
\n` +
-    runAndRender(ctx, args, `${extra} --help`) +
+    makeCommandsLinks(ctx, runAndRender(ctx, `${commandString} --help`)) +
     `\n\n` +
-    `run nb.js ${getCommandArgs(ctx, args, extra)}` +
+    `run nb.js ${commandString}` +
     `\n
` + `
`
 }
 
-function runAndRender(ctx, args = [], extra = '') {
-  let command = `${nb} ${getCommandArgs(ctx, args, extra)}`
+function runAndRender(ctx, commandString) {
+  let command = `${nb} ${commandString}`
   let output
   try {
     output = execSync(command, { encoding: 'utf8' })
   } catch (e) {
     output = e.stdout + e.stderr
   }
+  
   console.log(`> ${command}`)
   console.log(output)
-  return `> nb.js ${getCommandArgs(ctx, args, extra)}\n\n`
-    + output.replace(//g, '>')
+
+  return `> nb.js ${sanitizeHtml(commandString)}\n\n`
+    + sanitizeHtml(output)
+}
+
+function getCommand(ctx) {
+  let commandString = decodeURIComponent(ctx.params.command).trim()
+  
+  let routes = db.get('recent') || {}
+  if (!routes[commandString]) routes[commandString] = 0
+  Object.keys(routes).forEach(key => {
+    if (key !== commandString) {
+      routes[key]--;
+      if (routes[key] < 0) delete routes[key]
+    }
+  })
+  routes[commandString] += 4;
+  db.put('recent', routes)
+
+  return commandString
 }
 
 const port = 8080
@@ -50,32 +114,28 @@ const port = 8080
 // because we're trying to be shape-agnostic, we don't care what the args are called so long as they exist
 // this functions makes handler routes for both the --help and the regular commands for a certain length
 
-// if we have 2 args, that means we need `/nb`, `/nb/arg1`, and `/nb/arg1/arg2`.
-// we need to create two methods like this for each of them:
-// get('/nb/:arg1/:arg2', ctx => runAndRenderPage(ctx, ["arg1", "arg2"]))
-// post('/nb/:arg1/:arg2', ctx => runAndRender(ctx, ["arg1", "arg2"]))
-
-function makeRoutes(root, totalNumberOfArgs = 0) {
-  // for each sub-length in the totalNumberOfArgs, make a new set of get/post
-  // (e.g. if totalNumberOfArgs === 2, we need to make routes with 0, 1, and 2 args)
-  return new Array(totalNumberOfArgs + 1).fill('arg').map((arg, index) => index)
-    .map(numberOfArgs => {
-      let args = new Array(numberOfArgs).fill('arg').map((arg, index) => arg + index)
-      let path = args.map(arg => `:${arg}`).join('/') || ''
-      return [
-        get(root + (path ? "/" : "") + path, async ctx => runAndRenderPage(ctx, args)),
-        post(root + (path ? "/" : "") + path, async ctx => runAndRender(ctx, args))
-      ]
-    })
-    .flat(2)
-}
-
 server({ port, security: { csrf: false }}, [
-  get('/', ctx => `nb`),
+  get('/', ctx => {
+
+    let recents = db.get('recent') 
+    let recentsText = ''
+    if (recents) {
+      recentsText = Object.entries(recents)
+        .sort((a, b) => b[1] - a[1]).map(([key]) => {
+          return `- ${key}`
+        })
+        .join('
') + } + console.log() + + return `nb
${recentsText}` + }), - // handle a given number of args: - // this is total depth -- right now, this works - makeRoutes('/nb', 10) + get('/nb', ctx => runAndRenderPage(ctx, '')), + post('/nb', ctx => runAndRender(ctx, '')), + + get('/nb/:command', ctx => runAndRenderPage(ctx, getCommand(ctx))), + post('/nb/:command', ctx => runAndRender(ctx, getCommand(ctx))) ]); console.log(`Listening on port ${port}...`) \ No newline at end of file From 85c4fcfbb15a8268b5ec222f6248180f09e0b14a Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Fri, 16 Sep 2022 00:53:50 -0700 Subject: [PATCH 2/6] make command more easily editable --- server.js | 54 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/server.js b/server.js index 9c18b4c..a30a22a 100644 --- a/server.js +++ b/server.js @@ -12,14 +12,8 @@ let nb = path.resolve(__dirname, 'nb.js') const db = flatfile.sync(path.join(__dirname, 'server-config.db')) -// this function is only used by the web code -// do not add any node-specific stuff in it -// do not run it outside of the rendered output -async function doCommand(event) { - event.preventDefault() - let text = await (await fetch(window.location.pathname, { method: 'POST' })).text() - let outputElement = document.querySelector('#output') - outputElement.innerHTML += text +function changeCommand(event) { + } function makeCommandsLinks(ctx, input) { @@ -68,16 +62,52 @@ function sanitizeHtml(input) { } function runAndRenderPage(ctx, commandString) { + // this function is only used by the web code + // do not add any node-specific stuff in it + // do not run it outside of the rendered output + async function doCommand(event) { + event.preventDefault() + let text = await (await fetch(window.location.pathname, { method: 'POST' })).text() + let outputElement = document.querySelector('#output') + outputElement.innerHTML += text + } + return `
\n` +
-    makeCommandsLinks(ctx, runAndRender(ctx, `${commandString} --help`)) +
+    makeCommandsLinks(ctx, runAndRender(ctx, commandString, '--help')) +
     `\n\n` +
     `run nb.js ${commandString}` +
     `\n
` + `
`
 }
 
-function runAndRender(ctx, commandString) {
-  let command = `${nb} ${commandString}`
+function form(prefix, defaultValue, suffix) {
+  function onInput(event) {
+    document.querySelector('#spacer').innerHTML = ` ${event.target.value} `
+  }
+
+  function focusInput() {
+    document.querySelector('#command-input').focus()
+  }
+
+  function goToCommand(event) {
+    event.preventDefault()
+    window.location.pathname = `/nb/${encodeURIComponent(event.target[0].value)}`
+  }
+
+  let formStyle = `style="white-space: normal; display: inline-block;" `
+  let spanStyle = `style="white-space: pre;" `
+  let wrapperStyle = `style="position: relative; display: inline-block;" `
+  let inputStyle = `style="position: absolute; width: 100%; left: 0; border: 0; padding: 0; margin: 0; font-family: inherit; font-size: inherit; text-align: center;" `
+  return `
+ ${prefix.trim()} + ${defaultValue} + + ${suffix} +
` +} + +function runAndRender(ctx, commandString, nonEditableFlags) { + let command = `${nb} ${commandString} ${nonEditableFlags}` let output try { output = execSync(command, { encoding: 'utf8' }) @@ -88,7 +118,7 @@ function runAndRender(ctx, commandString) { console.log(`> ${command}`) console.log(output) - return `> nb.js ${sanitizeHtml(commandString)}\n\n` + return `> ${form(`nb.js `, sanitizeHtml(commandString), nonEditableFlags)}\n\n` + sanitizeHtml(output) } From 5330f9f78adb93b61b43089b062140e83e9f175b Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Fri, 23 Sep 2022 01:25:45 -0700 Subject: [PATCH 3/6] make interacting via server more fluid (by displaying prior commands and keeping input focus) --- server.js | 82 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/server.js b/server.js index a30a22a..90d795c 100644 --- a/server.js +++ b/server.js @@ -12,14 +12,14 @@ let nb = path.resolve(__dirname, 'nb.js') const db = flatfile.sync(path.join(__dirname, 'server-config.db')) -function changeCommand(event) { - -} - function makeCommandsLinks(ctx, input) { let isInCommandSection = false let commandRegExp = /(nb\.js)\s+((?:(?!&|<|\[| ).)+)\s/ + function makeLink(link, text) { + return `${text}` + } + let lines = input.split('\n') .map(line => { @@ -31,7 +31,7 @@ function makeCommandsLinks(ctx, input) { itemsSoFar.push(item) } if (!isAnArg) { - parsedLine += `${item} ` + parsedLine += `${makeLink(itemsSoFar.join(' '), item)} ` } else { parsedLine += `${item} ` } @@ -45,7 +45,7 @@ function makeCommandsLinks(ctx, input) { } else if (isInCommandSection && line.startsWith(' ')) { - return line.replace(commandRegExp, `$1 $2 `) + return line.replace(commandRegExp, `$1 ${makeLink('$2', '$2')} `) } else if (isInCommandSection && !line.startsWith(' ')) { @@ -62,52 +62,58 @@ function sanitizeHtml(input) { } function runAndRenderPage(ctx, commandString) { - // this function is only used by the web code - // do not add any node-specific stuff in it - // do not run it outside of the rendered output - async function doCommand(event) { + return form(`> nb.js `, sanitizeHtml(commandString)) + + '
' + + `
${makeCommandsLinks(ctx, runAndRender(ctx, commandString))}
` +} + +function form(prefix, defaultValue) { + function onInput(value) { + document.querySelector('#spacer').innerHTML = ` ${value} ` + } + + async function doCommand(event, link) { event.preventDefault() - let text = await (await fetch(window.location.pathname, { method: 'POST' })).text() + let command = link || event.target[0].value let outputElement = document.querySelector('#output') - outputElement.innerHTML += text - } + window.history.pushState({}, '', encodeURIComponent(command)) - return `
\n` +
-    makeCommandsLinks(ctx, runAndRender(ctx, commandString, '--help')) +
-    `\n\n` +
-    `run nb.js ${commandString}` +
-    `\n
` + - `
`
-}
+    document.querySelector('#command-input').value = command
+    onInput(command)
 
-function form(prefix, defaultValue, suffix) {
-  function onInput(event) {
-    document.querySelector('#spacer').innerHTML = ` ${event.target.value} `
+    let text = await (await fetch(`/nb/${encodeURIComponent(command)}`, { method: 'POST' })).text()
+    outputElement.innerHTML = makeCommandsLinks({}, text) + '\n\n' + outputElement.innerHTML
   }
 
-  function focusInput() {
-    document.querySelector('#command-input').focus()
+  // this hack makes the cursor move to the end of the text on focus
+  function onFocus(event) {
+    let val = event.target.value
+    event.target.value = ''
+    event.target.value = val
   }
 
-  function goToCommand(event) {
-    event.preventDefault()
-    window.location.pathname = `/nb/${encodeURIComponent(event.target[0].value)}`
+  function addListeners() {
+    window.addEventListener('keydown', () => {
+      document.querySelector('#command-input').focus()
+    })
   }
 
-  let formStyle = `style="white-space: normal; display: inline-block;" `
+  let formStyle = `style="white-space: normal; font-family: monospace;" `
   let spanStyle = `style="white-space: pre;" `
   let wrapperStyle = `style="position: relative; display: inline-block;" `
   let inputStyle = `style="position: absolute; width: 100%; left: 0; border: 0; padding: 0; margin: 0; font-family: inherit; font-size: inherit; text-align: center;" `
-  return `
+ + return ` + ${prefix.trim()} ${defaultValue} - - ${suffix} + +
` } -function runAndRender(ctx, commandString, nonEditableFlags) { - let command = `${nb} ${commandString} ${nonEditableFlags}` +function runAndRender(ctx, commandString) { + let command = `${nb} ${commandString}` let output try { output = execSync(command, { encoding: 'utf8' }) @@ -118,8 +124,7 @@ function runAndRender(ctx, commandString, nonEditableFlags) { console.log(`> ${command}`) console.log(output) - return `> ${form(`nb.js `, sanitizeHtml(commandString), nonEditableFlags)}\n\n` - + sanitizeHtml(output) + return sanitizeHtml(`> nb.js ${sanitizeHtml(commandString)}\n\n` + output) } function getCommand(ctx) { @@ -152,13 +157,12 @@ server({ port, security: { csrf: false }}, [ if (recents) { recentsText = Object.entries(recents) .sort((a, b) => b[1] - a[1]).map(([key]) => { - return `- ${key}` + return `- ${key}` }) .join('
') } - console.log() - return `nb
${recentsText}` + return `nb
${recentsText}` }), get('/nb', ctx => runAndRenderPage(ctx, '')), From bbda7cdab7cf66762ead9d7f7b5c9814d372c3f9 Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Fri, 23 Sep 2022 15:07:34 -0700 Subject: [PATCH 4/6] update server to separate posts from puts more, add repeat links - repeat links are automatically attached to every run command -- clicking it will trigger the same command again - runAndRender doesn't do anything except sanitize html (so angle brackets don't get consumed) - runAndRenderPage now does all the html stuff - make the focuser only focus on alphanumeric keys and only if a modifier key isn't pressed - make the commands much safer (though nothing is perfect) --- server.js | 152 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 60 deletions(-) diff --git a/server.js b/server.js index 90d795c..5ff4941 100644 --- a/server.js +++ b/server.js @@ -12,49 +12,11 @@ let nb = path.resolve(__dirname, 'nb.js') const db = flatfile.sync(path.join(__dirname, 'server-config.db')) -function makeCommandsLinks(ctx, input) { - let isInCommandSection = false - let commandRegExp = /(nb\.js)\s+((?:(?!&|<|\[| ).)+)\s/ - - function makeLink(link, text) { - return `${text}` - } - - let lines = input.split('\n') - .map(line => { - - if (line.startsWith('nb.js')) { - let parsedLine = '' - line.split(' ').reduce((itemsSoFar, item) => { - let isAnArg = item.startsWith('&') || item.startsWith('[') - if (item !== 'nb.js' && !isAnArg) { - itemsSoFar.push(item) - } - if (!isAnArg) { - parsedLine += `${makeLink(itemsSoFar.join(' '), item)} ` - } else { - parsedLine += `${item} ` - } - return itemsSoFar - }, []) - return parsedLine - } - - if (line.startsWith('Commands:')) { - isInCommandSection = true - } - - else if (isInCommandSection && line.startsWith(' ')) { - return line.replace(commandRegExp, `$1 ${makeLink('$2', '$2')} `) - } - - else if (isInCommandSection && !line.startsWith(' ')) { - isInCommandSection = false - } - - return line - }) - return lines.join("\n") +function injectScripts(...fns) { + return `` } function sanitizeHtml(input) { @@ -62,9 +24,13 @@ function sanitizeHtml(input) { } function runAndRenderPage(ctx, commandString) { - return form(`> nb.js `, sanitizeHtml(commandString)) - + '
' - + `
${makeCommandsLinks(ctx, runAndRender(ctx, commandString))}
` + return `` + + '
' + + form(`> nb.js `, sanitizeHtml(commandString)) + + '
' + + `
` + + `
${formatCommandOutput(ctx, commandString, sanitizeHtml(runCommand(ctx, commandString)))}
` + + `` } function form(prefix, defaultValue) { @@ -82,7 +48,7 @@ function form(prefix, defaultValue) { onInput(command) let text = await (await fetch(`/nb/${encodeURIComponent(command)}`, { method: 'POST' })).text() - outputElement.innerHTML = makeCommandsLinks({}, text) + '\n\n' + outputElement.innerHTML + outputElement.innerHTML = formatCommandOutput({}, command, text) + '\n\n' + outputElement.innerHTML } // this hack makes the cursor move to the end of the text on focus @@ -93,8 +59,17 @@ function form(prefix, defaultValue) { } function addListeners() { - window.addEventListener('keydown', () => { - document.querySelector('#command-input').focus() + window.addEventListener('keydown', (event) => { + console.log(event) + if ( + event.key.match(/^[a-zA-Z0-9]$/) + && event.altKey === false + && event.metaKey === false + && event.ctrlKey === false + && event.shiftKey === false + ) { + document.querySelector('#command-input').focus() + } }) } @@ -103,7 +78,7 @@ function form(prefix, defaultValue) { let wrapperStyle = `style="position: relative; display: inline-block;" ` let inputStyle = `style="position: absolute; width: 100%; left: 0; border: 0; padding: 0; margin: 0; font-family: inherit; font-size: inherit; text-align: center;" ` - return ` + return `${injectScripts(onInput, doCommand, onFocus, `(${addListeners.toString()}())`, formatCommandOutput, sanitizeHtml)}
${prefix.trim()} ${defaultValue} @@ -112,8 +87,55 @@ function form(prefix, defaultValue) { ` } -function runAndRender(ctx, commandString) { - let command = `${nb} ${commandString}` +function formatCommandOutput(ctx, command, input) { + let isInCommandSection = false + let commandRegExp = /(nb\.js)\s+((?:(?!&|<|\[| ).)+)\s/ + + function makeLink(link, text, help = true) { + return `${text}` + } + + let lines = input.split('\n') + .map(line => { + + if (line.startsWith('nb.js')) { + let parsedLine = '' + line.split(' ').reduce((itemsSoFar, item) => { + let isAnArg = item.startsWith('&') || item.startsWith('[') + if (item !== 'nb.js' && !isAnArg) { + itemsSoFar.push(item) + } + if (!isAnArg) { + parsedLine += `${makeLink(itemsSoFar.join(' '), item)} ` + } else { + parsedLine += `${item} ` + } + return itemsSoFar + }, []) + return parsedLine + } + + if (line.startsWith('Commands:')) { + isInCommandSection = true + } + + else if (isInCommandSection && line.startsWith(' ')) { + return line.replace(commandRegExp, `$1 ${makeLink('$2', '$2')} `) + } + + else if (isInCommandSection && !line.startsWith(' ')) { + isInCommandSection = false + } + + return line + }) + return `> nb.js ${sanitizeHtml(command)} ${makeLink(command, '(repeat)', false)}\n\n` + lines.join("\n") +} + +function runCommand(ctx, commandString) { + let command = `${nb} ${commandString.split(/\s/) + // best effort safety -- wrap all args in single quotes + .map(part => `'${part.replace(/'/g, "'\\''")}'`).join(' ')}` let output try { output = execSync(command, { encoding: 'utf8' }) @@ -124,12 +146,12 @@ function runAndRender(ctx, commandString) { console.log(`> ${command}`) console.log(output) - return sanitizeHtml(`> nb.js ${sanitizeHtml(commandString)}\n\n` + output) + return output } function getCommand(ctx) { - let commandString = decodeURIComponent(ctx.params.command).trim() - + let commandString = ctx.params.command + let routes = db.get('recent') || {} if (!routes[commandString]) routes[commandString] = 0 Object.keys(routes).forEach(key => { @@ -151,7 +173,6 @@ const port = 8080 server({ port, security: { csrf: false }}, [ get('/', ctx => { - let recents = db.get('recent') let recentsText = '' if (recents) { @@ -166,10 +187,21 @@ server({ port, security: { csrf: false }}, [ }), get('/nb', ctx => runAndRenderPage(ctx, '')), - post('/nb', ctx => runAndRender(ctx, '')), + post('/nb', ctx => server.reply + .type("text/plain") + .send(runCommand(ctx, ''))), get('/nb/:command', ctx => runAndRenderPage(ctx, getCommand(ctx))), - post('/nb/:command', ctx => runAndRender(ctx, getCommand(ctx))) -]); + post('/nb/:command', ctx => { + return server.reply + .type("text/plain") + .send(runCommand(ctx, getCommand(ctx)))} + ) +], + server.router.error(ctx => { + console.log(ctx.error) + return server.reply.status(500).send(ctx.error.message) + }) +); -console.log(`Listening on port ${port}...`) \ No newline at end of file +console.log(`Listening on http://localhost:${port}`) \ No newline at end of file From 7f4902c1e9303089b3b5589781d053db8f28c05f Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Fri, 23 Sep 2022 15:11:16 -0700 Subject: [PATCH 5/6] make note have tags; add --reverse and --limit to show command - api is now `note [value] [tags...]` - value is defaulted to 1 if first property after isn't a number - api for correct is now `correct [value] [tags...]` to match - --reverse flag on `show` now flips the stream - --limit flag allows you to limit the amount of data shown --- helpers.js | 53 +++++++++++++++++++++++++++++++++++++++++ nb.js | 70 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/helpers.js b/helpers.js index ce70d07..96678c0 100644 --- a/helpers.js +++ b/helpers.js @@ -76,3 +76,56 @@ export function generateDimension(argDimension, dimensionName) { return dimension } +// This function is a little nuts, and probably not worth it. Use it as a +// tagged template literal (e.g. printIfExists`foo ${var} bar`) to print +// the string if `var` "exists" where "exists" is some pseudo definition +// that I made up. So yeah. There's that. +export function printIfExists(strings, input, ...otherArgs) { + if (otherArgs.length > 0) { + throw new Error(`Unexpected usage! Use like ${printIfExists.name}\`before ${variable} after\`.`) + } + + function wrap(str) { + return strings[0] + str + strings[1] + } + + if (input !== undefined) { + if (input && Array.isArray(input) && input.length > 0) { + return wrap(input.join(', ')) + } + + else if (input && typeof input === 'object' && Object.keys(input).length > 0) { + try { + return wrap(JSON.stringify(input)) + } catch (e) { + return wrap(input) + } + } + + else if ( + (typeof input === 'string' && input !== '') || + (typeof input === 'number') || + (typeof input === 'boolean') + ) { + return wrap(input) + } + } + + // if we've fallen all the way through without returning, display nothing + return '' +} + +export function handleValueAndTags(value, tags) { + // for a dumb edge-case where Number('') returns 0 + let valueAsNumber = value === '' ? NaN : Number(value) + let valueIsNotNumber = Number.isNaN(valueAsNumber) + // prevents us from making an "undefined" tag when value is not provided + let valueIsTag = valueIsNotNumber && value !== undefined && value !== '' + + let finalValue = valueIsNotNumber ? 1 : valueAsNumber + let finalTags = (valueIsTag ? [value, ...tags] : tags) + .map(tag => tag.trim()) // trim out spacing for the case where someone does `note ' tag'` + .filter(tag => tag !== '') + + return { value: finalValue, tags: finalTags } +} \ No newline at end of file diff --git a/nb.js b/nb.js index abeeef8..69a27d8 100755 --- a/nb.js +++ b/nb.js @@ -30,19 +30,22 @@ import { formatTime, indexOrNot, parseTimestamp, - generateDimension + generateDimension, + printIfExists, + handleValueAndTags } from './helpers.js' updateNotifier({ pkg }).notify() yargs(hideBin(process.argv)) .command({ - command: 'note ', + command: 'note [value] [tags...]', description: 'record something worth remembering', builder: yargs => { return yargs .positional('stream', { describe: 'the id of the stream to write to', type: 'string' }) - .positional('value', { describe: 'the value to remember' }) + .positional('value', { describe: 'the value to remember', type: "string" }) + .positional('tags', { describe: 'tags to categorize this value', type: "array" }) .option('timestamp', { alias: 'ts' }) }, handler: args => { @@ -52,14 +55,16 @@ yargs(hideBin(process.argv)) console.log(`stream ${args.stream} does not exist; creating it now.`) } + const { value, tags } = handleValueAndTags(args.value, args.tags) + if (args.timestamp) { const parsedTimestamp = parseTimestamp(args.timestamp) - let values = [...stream.values, [ parsedTimestamp, args.value ]].sort((a, b) => a[0] - b[0]) + let values = [...stream.values, [parsedTimestamp, value, ...tags]].sort((a, b) => a[0] - b[0]) db.put(args.stream, { ...stream, values }) - console.log(`noted ${args.value} in stream ${formatStreamName(stream)} at time ${args.timestamp}.`) + console.log(`noted ${value}${printIfExists` (${tags})`} in stream ${formatStreamName(stream)} at time ${args.timestamp}.`) } else { - db.put(args.stream, { ...stream, values: [...stream.values, [ Date.now(), args.value ]] }) - console.log(`noted ${args.value} in stream ${formatStreamName(stream)}.`) + db.put(args.stream, { ...stream, values: [...stream.values, [Date.now(), value, ...tags]] }) + console.log(`noted ${value}${printIfExists` (${tags})`} in stream ${formatStreamName(stream)}.`) } } }) @@ -94,15 +99,17 @@ yargs(hideBin(process.argv)) } }) .command({ - command: 'correct [index] [note]', + command: 'correct [index] [value] [tags...]', description: 'correct a memory', builder: yargs => { return yargs .positional('stream', { describe: 'the stream containing the memory to replace' }) .positional('index', { describe: 'the memory to replace; reference it by its timestamp or index. if not specified, the newest note will be corrected' }) - .option('note', { describe: 'the new note value' }) + .positional('value', { describe: 'the new note value' }) + .positional('tags', { describe: 'the new note\'s tags', type: "array" }) }, handler: args => { + let stream = db.get(args.stream) if (stream === undefined) throw Error(`stream ${args.stream} does not exist.`) @@ -124,13 +131,15 @@ yargs(hideBin(process.argv)) throw Error(`stream does not contain memory with index ${index}.`) const newValues = values - newValues[correctionIndex] = [values[correctionIndex][0], args.note] + + const { value, tags } = handleValueAndTags(args.value, args.tags) + newValues[correctionIndex] = [values[correctionIndex][0], value, ...tags] db.put(args.stream, { ...stream, values: newValues }) if (args.index === undefined) { - console.log(`corrected latest note ${values.length} in stream ${formatStreamName(stream)}.`) + console.log(`corrected latest note ${values.length} (${value}${printIfExists`, [${tags}]`}) in stream ${formatStreamName(stream)}.`) } else { - console.log(`corrected note ${args.index} in stream ${formatStreamName(stream)}.`) + console.log(`corrected note ${args.index} (${value}${printIfExists`, [${tags}]`}) in stream ${formatStreamName(stream)}.`) } } }) @@ -194,30 +203,45 @@ yargs(hideBin(process.argv)) // TODO: Implement timeline format .option('format', { describe: 'the format of the output', choices: ['csv', 'table', 'chart', 'graph', 'json', 'timeline'], default: 'csv' }) .option('time-format', { describe: 'the format of timestamps in the output', choices: ['unix', 'relative', 'date'], default: 'relative' }) + .option('reverse', { describe: "reverse the order of the returned data", type: "boolean", default: false }) + .option('limit', { describe: "how many entries to display", type: "number"}) }, handler: args => { const stream = db.get(args.stream) if (stream === undefined) throw Error(`stream ${args.stream} does not exist.`) + // attach the index to the note before (potentially) reversing it and trimming it + let notes = stream.values.map((value, index) => [index, ...value]) + + if (args.reverse) notes = notes.reverse() + + if (typeof args.limit === 'number' && args.limit > 0) { + notes = notes.slice(0, args.limit) + } + if (args.format === 'csv') { - for(let i = 0; i < stream.values.length; i++) { - console.log(`${i}, ${formatTime(stream.values[i][0], args.timeFormat)}, ${stream.values[i][1]}`) - } + notes.forEach((note) => { + let [index, time, value, ...otherValues] = note + console.log(`${index}, ${formatTime(time, args.timeFormat)}, ${value}${printIfExists`, (${otherValues})`}`) + }) } else if (args.format === 'chart') { - console.log(chart(stream.values.map((value) => value[1]), { + console.log(chart(notes.map((value) => value[2]), { width: generateDimension(args.width, 'width'), height: generateDimension(args.height, 'height'), dense: true })) } else if (args.format === 'graph') { - console.log(sparkly(stream.values.map((value) => value[1]), { minimum: 0 })) + console.log(sparkly(notes.map((value) => value[2]), { minimum: 0 })) } else if (args.format === 'table') { const table = new AsciiTable(formatStreamName(stream)) - table.setHeading('index', 'time', 'value') - table.addRowMatrix(stream.values.map((value, index) => [index, formatTime(value[0], args.timeFormat), value[1]])) + table.setHeading('index', 'time', 'value', 'tags') + table.addRowMatrix(notes.map(note => { + let [index, time, value, ...otherValues] = note + return [index, formatTime(time, args.timeFormat), value, printIfExists`${otherValues}`] + })) console.log(table.toString()) } else if (args.format === 'json') { - console.log(JSON.stringify({ ...stream, values: stream.values.map((value, index) => [index, ...value]) }, null, 2)) + console.log(JSON.stringify({ ...stream, values: notes }, null, 2)) } else { throw Error(`format ${args.format} not implemented yet.`) } @@ -233,7 +257,7 @@ yargs(hideBin(process.argv)) handler: args => { const stream = db.get(args.stream) if (stream === undefined) throw Error(`stream ${args.stream} does not exist.`) - if (args.name) { db.put(args.stream, {... stream, name: args.name }) } + if (args.name) { db.put(args.stream, { ...stream, name: args.name }) } console.log(`updated stream ${formatStreamName(stream)}.`) } }) @@ -276,7 +300,7 @@ yargs(hideBin(process.argv)) for (let metaRow = 0; metaRow < rowCount; metaRow++) { for (let literalRow = 0; literalRow < height - 2; literalRow++) { let row = '' - for(let column = 0; column < chartsPerRow; column++) { + for (let column = 0; column < chartsPerRow; column++) { if (metaRow * chartsPerRow + column < streams.length) { row += charts[metaRow * chartsPerRow + column][literalRow] } @@ -322,7 +346,7 @@ yargs(hideBin(process.argv)) let fromStreams = from.keys() - for(let i = 0; i < fromStreams.length; i++) { + for (let i = 0; i < fromStreams.length; i++) { if (!to.has(fromStreams[i])) { to.put(fromStreams[i], { id: fromStreams[i], ...defaultStream }) console.log(`stream ${fromStreams[i]} does not exist; creating it now.`) From 9dbe0e0bf84d2da4ba6fefe03f5cb9f1593d0ff0 Mon Sep 17 00:00:00 2001 From: Than Hutchins Date: Fri, 23 Sep 2022 15:17:31 -0700 Subject: [PATCH 6/6] make sure nb throws an error on unknown commands --- nb.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nb.js b/nb.js index 69a27d8..cf04b9e 100755 --- a/nb.js +++ b/nb.js @@ -311,6 +311,7 @@ yargs(hideBin(process.argv)) } }) .demandCommand() + .strictCommands() .help() }, handler: args => { @@ -376,11 +377,13 @@ yargs(hideBin(process.argv)) } }) .demandCommand() + .strictCommands() .help() }, handler: args => { } }) .demandCommand() + .strictCommands() .help() .argv