From 1254372229cd7435f0b201fdd991f8c30449c9e2 Mon Sep 17 00:00:00 2001 From: piotrruss Date: Sun, 8 May 2022 23:45:01 +0100 Subject: compact to shell script --- notes_cli | 392 +++++++++++++++++++++++++++++++++++++ notes_cli.js | 627 ----------------------------------------------------------- 2 files changed, 392 insertions(+), 627 deletions(-) create mode 100755 notes_cli delete mode 100755 notes_cli.js diff --git a/notes_cli b/notes_cli new file mode 100755 index 0000000..5db00cb --- /dev/null +++ b/notes_cli @@ -0,0 +1,392 @@ +#!/usr/bin/env node +const fs = require('fs') +const { homedir } = require('os') +const https = require('https') +const readline = require("readline") + +let conf, notesList = [], active = 0; scroll = 0; +let draw = { t: 'p', v: () => ['Loading NOTES CLI...'] }; menu = 'Press [h] for help'; sort = 3 +const cl = (m, c) => console.log(c ? `\x1b[${c}m${m}\x1b[0m` : m) +const cls = () => console.clear() +const filesPath = `${homedir}/.local/share/notes_cli` +const fixedStr = (s, l) => s.length < l ? s.padEnd(l) : s.substring(0, l) +const formatDate = d => d.replace('T',' ').replace(/\..*Z/, '') + +const cursor = { + hide: () => process.stdout.write('\u001B[?25l'), + show: () => process.stdout.write('\u001B[?25h') +} + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +rl._writeToOutput = function _writeToOutput(stringToWrite) { + if (rl.stdoutMuted) + rl.output.write("\x1B[2K\x1B[200D Password: "+('*'.repeat(rl.line.length))) + else + rl.output.write(stringToWrite); +}; + +const exitWithError = (e) => { + draw = { t: 'p', v: () => e.map(m => `\x1b[31m${m}\x1b[0m`)}; drawApp() + setTimeout(() => process.exit(1), 2000) +} + +const formatNote = note => { + const columns = process.stdout.columns + return columns > 72 + ? `${fixedStr(note.title, columns - 45)} ${formatDate(note.created_at)} ${formatDate(note.updated_at)}` + : fixedStr(note.title, columns - note.title.length-2) +} + +const showHelp = () => { + draw = { t: 'h', prev: draw.v, v: () => ['[q] - Quit','[h] - Help','','[up/k] - Previous note','[down/j] - Next note', + '[enter/o] - Open note','','[c] - Create note','[d] - Delete note',"[t] - Change note's title",'','[1] - Sort by title', + '[2] - Sort by creation date','[3] - Sort by modification date']}; + menu = 'Press any key to exit help'; drawApp() +} + +const setSort = (s) => { + rl.clearLine(-1) + sort = s === Math.abs(sort) ? sort * (-1) : parseInt(s) + draw = { t: 'l', v: () => notesList.sort(sortFn).map(formatNote) }; drawApp() +} + +const sortFn = (a, b) => { + const d = sort > 0 ? 1 : -1 + switch (Math.abs(sort)) { + case 1: return d * a.title.localeCompare(b.title) + case 2: return d * (new Date(b.created_at) - new Date(a.created_at)) + case 3: return d * (new Date(b.updated_at) - new Date(a.updated_at)) + } +} + +const showList = (notes, status) => { + if (status !== 200) { cls(); return getEmail('Session error') } + notesList = notes + draw = { t: 'l', v: () => notes.sort(sortFn).map(formatNote) }; drawApp() +} + +const changeTitle = (title) => { + draw = {t: 'p', prev: draw.prev, v: () => ['Changing title...']}; drawApp() + putTitle(notesList[active], title) +} + +const confirmRemoval = () => { + menu = '[y]es [n]o'; draw = {t: 'c', prev: draw.v, v: () => ['Do you want to remove note?']}; drawApp() +} + +const closePopup = () => { + menu = 'Press [h] for help'; draw = {t: 'l', v: draw.prev}; drawApp() +} + +const getNewNoteTitle = (existing = false) => { + rl.clearLine(-1) + draw = {t: 'i', prev: draw.v, v: () => ['Please type the new note title', 'or leave empty to cancel'], noMenu: true}; drawApp() + cursor.show() + rl.question(' Title: ', t => { + cursor.hide() + if (t === '') { + draw = {t: 'p', prev: draw.v, v: () => ['Canceled']}; drawApp() + setTimeout(() => { draw = {t: 'l', v}; drawApp() }, 1000) + } else { + existing ? changeTitle(t) : createTmpFile(t) + } + }) +} + +const saveTmpFile = (note, { content }) => { + const file = `${filesPath}/${note._id}.tmp` + fs.writeFile(file, content, function(err) { + if(err) { exitWithError(['Error creating tmp file']) } + return editTmpFile(note) + }) +} + +const readTmpFile = (note, title) => { + const file = `${filesPath}/${title ? 'new' : note._id}.tmp` + fs.readFile(file, function(err, f){ + if (err) { return getEmail() } + try { + const content = f.toString() + fs.unlink(file, () => {}) + if (!content) { return getEmail('Error saving note') } + title ? createNote(title, content) : putNote(note, content) + } catch (e) { + return getEmail('Session error') + } + }) +} + +const noteSaved = (list) => { + draw = {t: 'p', v: () => list ? ['Note saved'] : ['Could not save note']}; drawApp(); fetchList() +} + +const noteRemoved = (list) => { + menu = 'Press [h] for help'; draw = {t: 'p', v: () => list ? ['Note removed'] : ['Could not remove note']} + if (list) { active-- }; + drawApp(); fetchList() +} + +const createTmpFile = (title) => { + const file = `${filesPath}/new.tmp` + const child_process = require('child_process') + const editor = process.env.EDITOR || 'vi'; + cursor.show() + const child = child_process.spawn(editor, [file], { stdio: 'inherit' }); + rl.pause() + child.on('exit', function (e) { + rl.resume() + cursor.hide() + if (e === 0) { + draw = { t: 'p', v: () => ['Saving new note...'] }; drawApp(); readTmpFile(null, title) + } else { + draw = { t: 'p', v: () => ['Canceled'] }; fs.unlink(file, () => {}); drawApp(); fetchList() + } + }); +} + +const editTmpFile = (note) => { + const file = `${filesPath}/${note._id}.tmp` + const child_process = require('child_process') + const editor = process.env.EDITOR || 'vi'; + cursor.show() + const child = child_process.spawn(editor, [file], { stdio: 'inherit' }); + rl.pause() + child.on('exit', function (e) { + rl.resume() + cursor.hide() + if (e === 0) { + draw = { t: 'p', v: () => ['Saving changes...'] }; readTmpFile(note) + } else { + draw = { t: 'p', v: () => ['Changes not saved'] }; fs.unlink(file, () => {}); drawApp(); fetchList() + } + }); +} + +const sortIco = c => c === Math.abs(sort) ? sort > 0 ? '▼' : '▲' : ' ' + +const drawApp = () => { + const lines = process.stdout.rows + const columns = process.stdout.columns + const headers = columns > 72 + ? `${fixedStr(' Title '+sortIco(1), columns - 44)} Created at ${sortIco(2)}${' '.repeat(8)} Updated at ${sortIco(3)}${' '.repeat(8)}` + : ` Title ${' '.repeat(columns - 9)}` + + cls() + for (var i = 0; i < lines - 1; i++) { + const dist = t => columns > t + 2 ? [Math.floor((columns - t) / 2), Math.ceil((columns - t) / 2) - 2] : [0, 0] + switch(i){ + case 0: cl('╔'+'═'.repeat(columns - 2)+'╗'); break + case 1: conf?.email + ? cl('║'+' '.repeat(dist(conf.email.length+12)[0])+`NOTES CLI (${conf.email})`.substring(0, columns-2)+' '.repeat(dist(conf.email.length+12)[1])+'║') + : cl('║'+' '.repeat(dist(9)[0])+`NOTES CLI`.substring(0, columns-2)+' '.repeat(dist(9)[1])+'║'); break + case 2: cl('╠'+'═'.repeat(columns - 2) +'╣'); break + case 3: cl('║'+(headers || ' '.repeat(columns-2))+'║'); break + case 4: cl('╟'+'─'.repeat(columns - 2) +'╢'); break + case lines - 2: cl('╚'+'═'.repeat(columns - 2) +'╝'); break + default: + const max = Math.max(...(draw.v().map(el => el.length))) + if (['p','h','c','i'].includes(draw.t)) { + if (i === (Math.floor((lines - draw.v().length) / 2) - 2)){ + cl('║'+' '.repeat(dist(max+4)[0])+'┌'+'─'.repeat(max+2)+'┐'+' '.repeat(dist(max+4)[1])+'║') + } else if (i === (Math.floor((lines + draw.v().length) / 2) + 1)) { + cl('║'+' '.repeat(dist(max+4)[0])+'└'+'─'.repeat(max+2)+'┘'+' '.repeat(dist(max+4)[1])+'║') + } else if (i === (Math.floor((lines - draw.v().length) / 2) - 1) || i === (Math.floor((lines + draw.v().length) / 2))) { + cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '.repeat(max+2)+'│'+' '.repeat(dist(max+4)[1])+'║') + } else if (i < (Math.floor((lines - draw.v().length) / 2) - 1) || (i > (Math.floor((lines + draw.v().length) / 2)))) { + cl('║'+' '.repeat(columns - 2)+'║') + } else { + const n = i - (Math.floor((lines - draw.v().length) / 2)) + const t = draw.v()[n] + const tLength = t.indexOf('\x1b') > -1 ? t.length - 9 : t.length + const s = [Math.floor((max+2-tLength) / 2), Math.ceil((max+2-tLength) / 2)] + draw.t === 'h' + ? cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '+t+' '.repeat(s[0]+s[1]-1)+'│'+' '.repeat(dist(max+4)[1])+'║') + : cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '.repeat(s[0])+t+' '.repeat(s[1])+'│'+' '.repeat(dist(max+4)[1])+'║') + } + } else if (draw.t === 'l') { + const l = draw.v()[i-5+scroll] && draw.v()[i-5+scroll].substring(0,columns-4) + cl(l + ? ('║'+(active===i-5+scroll?'\x1b[7m ':' ')+l+' '.repeat(columns-l.length-3)+'\x1b[0m║') + : ('║'+' '.repeat(columns - 2)+'║') + ) + } + } + } + + if (!draw.noMenu) { + const activeNr = `${active+1}`, notesNr = `${notesList.length}` + process.stdout.write(' '+menu+' '.repeat(columns-5-menu.length-activeNr.length-notesNr.length)+`[${activeNr}/${notesNr}]`) + } +} + +const getKey = () => { + readline.emitKeypressEvents(process.stdin); + process.stdin.setRawMode(true); + process.stdin.on('keypress', (_, key) => { + const lines = process.stdout.rows + const run = (mode, fn) => { + if (draw.t === mode) { + fn() + } else if (draw.t === 'h') { + draw = { t: 'l', v: draw.prev }; menu = 'Press [h] for help'; drawApp() + } else if (draw.t === 'i') { + } else { rl.clearLine(-1); drawApp() } + } + switch(key.name) { + case 'up': case 'k': run('l', () => { active > 0 && active--; active < scroll && scroll--; drawApp() }); break + case 'down': case 'j': run('l', () => { active < draw.v().length - 1 && active++; active - scroll > lines - 7 && scroll++; drawApp(); }); break + case 'return': case 'o': run('l', () => { fetchNote(notesList[active]) }); break + case 'h': run('l', () => { showHelp() }); break + case 'c': run('l', () => { getNewNoteTitle() }); break + case 't': run('l', () => { getNewNoteTitle(true) }); break + case 'd': run('l', () => { confirmRemoval() }); break + case 'y': run('c', () => { removeNote(notesList[active]) }); break + case 'n': run('c', () => { closePopup() }); break + case '1': case '2': case '3': run('l', () => { setSort(parseInt(key.name)) }); break + case 'q': run('l', () => { process.exit() }); break + default: + if (draw.t === 'h') { + const v = draw.prev; draw = { t: 'l', v }; menu = 'Press [h] for help'; drawApp() + } if (draw.t === 'i') { + } else { rl.clearLine(-1); drawApp() } + } + }) +} + +const setSession = (session) => { + if (!fs.existsSync(filesPath)) fs.mkdirSync(filesPath, { recursive: true }); + fs.writeFile(`${filesPath}/session`, JSON.stringify(session), function(err) { + if (err) { exitWithError(['Error writting session file']) } + getSession() + }) +} + +const getSession = () => { + fs.readFile(`${filesPath}/session`, function(err, f){ + if (err) return getEmail() + try { + conf = JSON.parse(f.toString()) + if (!conf.session || !conf.userId || !conf.email || !conf.list) { return getEmail('Session error') } + getKey() + fetchList() + } catch (e) { return getEmail('Session error') } + }) +} + +const loginMsg = (text, err) => ['Login to apps.pruss.it', '', `\x1b[31m${err}\x1b[0m`, '', ...text] +const handleSession = (o, s, h) => { + if (!o || s !== 201) return getEmail('Could not log in, try again') + const {_id, email, isVerified, noteList} = o, session = h['set-cookie'] + if (!_id || !email || !noteList) { + exitWithError(['Could not log in']) + } else if (!isVerified) { + exitWithError(['User not verified.', 'Please first verify the user using apps.pruss.it']) + } + setSession({userId: _id, email, list: noteList, session}) +} + +const getPass = (e) => { + cls() + draw = { t: 'p', v: () => loginMsg(['Please type password for', `your apps.pruss.it account`, 'or press [q] to quit'], ''), noMenu: true}; drawApp() + rl.stdoutMuted = true + rl.write() + rl.question('', p => { + rl.pause(); rl.stdoutMuted = false; drawApp() + if (p === 'q') process.exit() + post('/api/login', {"email": e, "password": p}, handleSession) + }) + rl.history = rl.history.slice(1) +} + +const getEmail = (error = '') => { + cls(); draw = { t: 'p', v: () => loginMsg(['Please input email for your','apps.pruss.it account or' , 'press [q] to quit'], error), noMenu: true}; + drawApp(); cursor.show() + rl.question(' Email: ', e => { + cursor.hide(); rl.pause() + if (e === 'q') { + process.exit() + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) { + cls(); return getEmail('Not a valid email address, try again.') + } + getPass(e) + }) +} + +const fetchList = () => { get('/api/notes', showList, conf.session) } + +const fetchNote = note => { + draw = { t: 'p', v: () => ['Loading...'] }; drawApp() + get(`/api/notes/${note.noteId}`, (content) => saveTmpFile(note, content), conf.session) +} + +const removeNote = note => { + draw = { t: 'p', v: () => ['Removing note...'] }; drawApp() + remove(`/api/notes/${note._id}`, noteRemoved, conf.session) +} + +const putNote = ({ _id, noteId }, content) => { + put(`/api/notes/${_id}`, { noteId, content }, noteSaved, conf.session) +} + +const putTitle = ({ _id, noteId }, title) => { + put(`/api/notes/${_id}`, { noteId, title }, noteSaved, conf.session) +} + +const createNote = (title, content) => { + post('/api/notes', { title, content }, noteSaved, conf.session) +} + +const get = (path, callback, cookie = '') => { + const options = { hostname: 'apps.pruss.it', port: 443, path, method: 'GET', headers: { 'Cookie': cookie } }; + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => { data += chunk }); + res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); + }); + req.on('error', () => { callback(null) }); + req.end(); +} + +const post = (path, data, callback, cookie = '', put = false) => { + const dataString = JSON.stringify(data) + const options = { + hostname: 'apps.pruss.it', + port: 443, + path, + method: put ? 'PUT' : 'POST', + headers: put + ? { 'Cookie': cookie, } + : { 'Content-Type': 'text/plain', 'Content-Length': dataString.length, 'Cookie': cookie, }, + timeout: 1000, + }; + const req = https.request(options, (res) => { + let data = ''; + res.on('data', chunk => { data += chunk }); + res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); + }).on("error", () => { callback(null) }); + req.write(dataString); + req.end(); +} + +const put = (...arg) => post(...arg, true) + +const remove = (path, callback, cookie = '') => { + const options = { hostname: 'apps.pruss.it', port: 443, path, method: 'DELETE', headers: { 'Cookie': cookie } }; + const req = https.request(options, res => { + let data = ''; + res.on('data', chunk => { data += chunk }); + res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); + }); + req.on('error', () => { callback(null) }); + req.end(); +} + +process.stdout.on('resize', drawApp); +process.on('exit',() => { rl.close(); cursor.show(); cls(); cl('Bye!\n\n') }) +cls() +getSession() +cursor.hide() diff --git a/notes_cli.js b/notes_cli.js deleted file mode 100755 index d40df6f..0000000 --- a/notes_cli.js +++ /dev/null @@ -1,627 +0,0 @@ -const fs = require('fs') -const { homedir } = require('os') -const https = require('https') -const readline = require("readline") - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}) - -rl._writeToOutput = function _writeToOutput(stringToWrite) { - if (rl.stdoutMuted) - rl.output.write("\x1B[2K\x1B[200D Password: "+('*'.repeat(rl.line.length))) - else - rl.output.write(stringToWrite); -}; - -const cursor = { - hide: () => process.stdout.write('\u001B[?25l'), - show: () => process.stdout.write('\u001B[?25h') -} - -process.on('exit',() => { - rl.close() - cursor.show() - cls() - cl('Bye!\n\n') -}) - -const exitWithError = (e) => { - cls() - draw = { t: 'p', v: () => e.map(m => `\x1b[31m${m}\x1b[0m`)} - drawApp() - setTimeout(() => process.exit(1), 2000) -} - -let conf, notesList = [], active = 0; scroll = 0; draw = { t: 'p', v: () => ['Loading NOTES CLI...'] }; menu = 'Press [h] for help'; sort = 3 -const cl = (m, c) => console.log(c ? `\x1b[${c}m${m}\x1b[0m` : m) -const cls = () => console.clear() -const filesPath = `${homedir}/.local/share/notes_cli` - -const fixedStr = (s, l) => s.length < l ? s.padEnd(l) : s.substring(0, l) -const formatDate = d => d.replace('T',' ').replace(/\..*Z/, '') - -/* Views */ -const formatNote = note => { - const columns = process.stdout.columns - return columns > 72 - ? `${fixedStr(note.title, columns - 45)} ${formatDate(note.created_at)} ${formatDate(note.updated_at)}` - : fixedStr(note.title, columns - note.title.length-2) -} - -const showHelp = () => { - const prev = draw.v - draw = { t: 'h', prev, v: () => ['[q] - Quit','[h] - Help','','[up/k] - Previous note','[down/j] - Next note', - '[enter/o] - Open note','','[c] - Create note','[d] - Delete note',"[t] - Change note's title",'','[1] - Sort by title','[2] - Sort by creation date','[3] - Sort by modification date']}; - menu = 'Press any key to exit help' - drawApp() -} - -const setSort = (s) => { - rl.clearLine(-1) - sort = s === Math.abs(sort) ? sort * (-1) : parseInt(s) - draw = { t: 'l', v: () => notesList.sort(sortFn).map(formatNote) } - drawApp() -} - -const sortFn = (a, b) => { - const d = sort > 0 ? 1 : -1 - switch (Math.abs(sort)) { - case 1: return d * a.title.localeCompare(b.title) - case 2: return d * (new Date(b.created_at) - new Date(a.created_at)) - case 3: return d * (new Date(b.updated_at) - new Date(a.updated_at)) - } -} - -const showList = (notes, status) => { - if (status !== 200) { - cls() - return getEmail('Session error') - } - - notesList = notes - draw = { t: 'l', v: () => notes.sort(sortFn).map(formatNote) } - drawApp() -} - -const changeTitle = (title) => { - const prev = draw.prev - draw = {t: 'p', prev, v: () => ['Changing title...']} - drawApp() - putTitle(notesList[active], title) -} - -const confirmRemoval = () => { - const v = draw.v - menu = '[y]es [n]o' - draw = {t: 'c', prev: v, v: () => ['Do you want to remove note?']} - drawApp() -} - -const closePopup = () => { - const v = draw.prev - menu = 'Press [h] for help' - draw = {t: 'l', v} - drawApp() -} - -const getNewNoteTitle = (existing = false) => { - const v = draw.v - rl.clearLine(-1) - cls() - draw = {t: 'i', prev: v, v: () => ['Please type the new note title', 'or leave empty to cancel'], noMenu: true} - drawApp() - cursor.show() - rl.question(' Title: ', t => { - cursor.hide() - if (t === '') { - draw = {t: 'p', prev: v, v: () => ['Canceled']} - drawApp() - setTimeout(() => { - draw = {t: 'l', v} - drawApp() - }, 1000) - } else { - existing ? changeTitle(t) : createTmpFile(t) - } - }) -} - -const saveTmpFile = (note, { content }) => { - const file = `${filesPath}/${note._id}.tmp` - fs.writeFile(file, content, function(err) { - if(err) { - exitWithError(['Error creating tmp file']) - } - - return editTmpFile(note) - }) -} - -const readTmpFile = (note, title) => { - const file = `${filesPath}/${title ? 'new' : note._id}.tmp` - fs.readFile(file, function(err, f){ - if (err) { - return getEmail() - } - - try { - const content = f.toString() - - fs.unlink(file, () => {}) - - if (!content) { - return getEmail('Error saving note') - } - - title ? createNote(title, content) : putNote(note, content) - } catch (e) { - return getEmail('Session error') - } - }) -} - -const noteSaved = (list) => { - draw = {t: 'p', v: () => list ? ['Note saved'] : ['Could not save note']} - drawApp() - fetchList() -} - -const noteRemoved = (list) => { - menu = 'Press [h] for help' - draw = {t: 'p', v: () => list ? ['Note removed'] : ['Could not remove note']} - if (list) { active-- } - drawApp() - fetchList() -} - -const createTmpFile = (title) => { - const file = `${filesPath}/new.tmp` - const child_process = require('child_process') - const editor = process.env.EDITOR || 'vi'; - cursor.show() - const child = child_process.spawn(editor, [file], { stdio: 'inherit' }); - rl.pause() - child.on('exit', function (e) { - rl.resume() - cursor.hide() - if (e === 0) { - draw = { t: 'p', v: () => ['Saving new note...'] } - drawApp() - readTmpFile(null, title) - } else { - draw = { t: 'p', v: () => ['Canceled'] } - fs.unlink(file, () => {}) - drawApp() - fetchList() - } - }); -} - -const editTmpFile = (note) => { - const file = `${filesPath}/${note._id}.tmp` - const child_process = require('child_process') - const editor = process.env.EDITOR || 'vi'; - cursor.show() - const child = child_process.spawn(editor, [file], { stdio: 'inherit' }); - rl.pause() - child.on('exit', function (e) { - rl.resume() - cursor.hide() - if (e === 0) { - draw = { t: 'p', v: () => ['Saving changes...'] } - readTmpFile(note) - } else { - draw = { t: 'p', v: () => ['Changes not saved'] } - fs.unlink(file, () => {}) - drawApp() - fetchList() - } - }); -} - -// =========== DRAW APP ============= - -const sortIco = c => { - if (c === Math.abs(sort)){ - return sort > 0 ? '▼' : '▲' - } else { - return ' ' - } -} - -const drawApp = () => { - const lines = process.stdout.rows - const columns = process.stdout.columns - const headers = columns > 72 - ? `${fixedStr(' Title '+sortIco(1), columns - 44)} Created at ${sortIco(2)}${' '.repeat(8)} Updated at ${sortIco(3)}${' '.repeat(8)}` - : ` Title ${' '.repeat(columns - 9)}` - - cls() - for (var i = 0; i < lines - 1; i++) { - const dist = t => columns > t + 2 ? [Math.floor((columns - t) / 2), Math.ceil((columns - t) / 2) - 2] : [0, 0] - switch(i){ - case 0: - cl('╔'+'═'.repeat(columns - 2)+'╗') - break - case 1: - conf?.email - ? cl('║'+' '.repeat(dist(conf.email.length+12)[0])+`NOTES CLI (${conf.email})`.substring(0, columns-2)+' '.repeat(dist(conf.email.length+12)[1])+'║') - : cl('║'+' '.repeat(dist(9)[0])+`NOTES CLI`.substring(0, columns-2)+' '.repeat(dist(9)[1])+'║') - break - case 2: - cl('╠'+'═'.repeat(columns - 2) +'╣') - break - case 3: - cl('║'+(headers || ' '.repeat(columns-2))+'║') - break - case 4: - cl('╟'+'─'.repeat(columns - 2) +'╢') - break - case lines - 2: - cl('╚'+'═'.repeat(columns - 2) +'╝') - break - default: - const max = Math.max(...(draw.v().map(el => el.length))) - if (['p','h','c','i'].includes(draw.t)) { - if (i === (Math.floor((lines - draw.v().length) / 2) - 2)){ - cl('║'+' '.repeat(dist(max+4)[0])+'┌'+'─'.repeat(max+2)+'┐'+' '.repeat(dist(max+4)[1])+'║') - } else if (i === (Math.floor((lines + draw.v().length) / 2) + 1)) { - cl('║'+' '.repeat(dist(max+4)[0])+'└'+'─'.repeat(max+2)+'┘'+' '.repeat(dist(max+4)[1])+'║') - } else if (i === (Math.floor((lines - draw.v().length) / 2) - 1) || i === (Math.floor((lines + draw.v().length) / 2))) { - cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '.repeat(max+2)+'│'+' '.repeat(dist(max+4)[1])+'║') - } else if (i < (Math.floor((lines - draw.v().length) / 2) - 1) || (i > (Math.floor((lines + draw.v().length) / 2)))) { - cl('║'+' '.repeat(columns - 2)+'║') - } else { - const n = i - (Math.floor((lines - draw.v().length) / 2)) - const t = draw.v()[n] - const tLength = t.indexOf('\x1b') > -1 ? t.length - 9 : t.length - const s = [Math.floor((max+2-tLength) / 2), Math.ceil((max+2-tLength) / 2)] - draw.t === 'h' - ? cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '+t+' '.repeat(s[0]+s[1]-1)+'│'+' '.repeat(dist(max+4)[1])+'║') - : cl('║'+' '.repeat(dist(max+4)[0])+'│'+' '.repeat(s[0])+t+' '.repeat(s[1])+'│'+' '.repeat(dist(max+4)[1])+'║') - } - } else if (draw.t === 'l') { - const l = draw.v()[i-5+scroll] && draw.v()[i-5+scroll].substring(0,columns-4) - - cl(l - ? ('║'+(active===i-5+scroll?'\x1b[7m ':' ')+l+' '.repeat(columns-l.length-3)+'\x1b[0m║') - : ('║'+' '.repeat(columns - 2)+'║') - ) - } - } - } - - if (!draw.noMenu) { - const activeNr = `${active+1}` - const notesNr = `${notesList.length}` - process.stdout.write(' '+menu+' '.repeat(columns-5-menu.length-activeNr.length-notesNr.length)+`[${activeNr}/${notesNr}]`) - } -} - -// ============ GET KEY ================= - -const getKey = () => { - readline.emitKeypressEvents(process.stdin); - process.stdin.setRawMode(true); - process.stdin.on('keypress', (_, key) => { - const lines = process.stdout.rows - const run = (mode, fn) => { - if (draw.t === mode) { - fn() - } else if (draw.t === 'h') { - const v = draw.prev - draw = { t: 'l', v } - menu = 'Press [h] for help' - drawApp() - } else if (draw.t === 'i') { - } else { - rl.clearLine(-1) - drawApp() - } - } - switch(key.name) { - case 'up': - case 'k': - run('l', () => { - active > 0 && active-- - active < scroll && scroll-- - drawApp() - }) - break - case 'down': - case 'j': - run('l', () => { - active < draw.v().length - 1 && active++ - active - scroll > lines - 7 && scroll++ - drawApp() - }) - break - case 'return': - case 'o': - run('l', () => { - fetchNote(notesList[active]) - }) - break - case 'h': - run('l', () => { - showHelp() - }) - break - case 'c': - run('l', () => { - getNewNoteTitle() - }) - break - case 't': - run('l', () => { - getNewNoteTitle(true) - }) - break - case 'd': - run('l', () => { - confirmRemoval() - }) - break - case 'y': - run('c', () => { - removeNote(notesList[active]) - }) - break - case 'n': - run('c', () => { - closePopup() - }) - break - case '1': - case '2': - case '3': - run('l', () => { - setSort(parseInt(key.name)) - }) - break - case 'q': - run('l', () => { - process.exit() - }) - break - default: - if (draw.t === 'h') { - const v = draw.prev - draw = { t: 'l', v } - menu = 'Press [h] for help' - drawApp() - } if (draw.t === 'i') { - } else { - rl.clearLine(-1) - drawApp() - } - } - }) -} - -/* Session */ - -const setSession = (session) => { - if (!fs.existsSync(filesPath)){ - fs.mkdirSync(filesPath, { recursive: true }); - } - - fs.writeFile(`${filesPath}/session`, JSON.stringify(session), function(err) { - if(err) { - exitWithError(['Error writting session file']) - } - - getSession() - }) -} - -const getSession = () => { - fs.readFile(`${filesPath}/session`, function(err, f){ - if (err) { - return getEmail() - } - - try { - conf = JSON.parse(f.toString()) - - if (!conf.session || !conf.userId || !conf.email || !conf.list) { - return getEmail('Session error') - } - - getKey() - fetchList() - } catch (e) { - return getEmail('Session error') - } - }) -} - -/* Login */ -const loginMsg = (text, err) => ['Login to apps.pruss.it', '', `\x1b[31m${err}\x1b[0m`, '', ...text] - -const getPass = (e) => { - cls() - draw = { t: 'p', v: () => loginMsg(['Please type password for', `your apps.pruss.it account`, 'or press [q] to quit'], ''), noMenu: true} - drawApp() - rl.stdoutMuted = true - rl.write() - rl.question('', p => { - rl.pause() - rl.stdoutMuted = false - drawApp() - if (p === 'q') { - process.exit() - } - - post( - '/api/login', - {"email": e, "password": p}, - (o, s, h) => { - if (!o || s !== 201) { - return getEmail('Could not log in, try again') - } - - const {_id, email, isVerified, noteList} = o - const session = h['set-cookie'] - - if (!_id || !email || !noteList) { - exitWithError(['Could not log in']) - } else if (!isVerified) { - exitWithError(['User not verified.', 'Please first verify the user using apps.pruss.it']) - } - - setSession({userId: _id, email, list: noteList, session}) - }) - }) - - rl.history = rl.history.slice(1) -} - -const getEmail = (error = '') => { - cls() - draw = { t: 'p', v: () => loginMsg(['Please input email for your','apps.pruss.it account or' , 'press [q] to quit'], error), noMenu: true} - drawApp() - cursor.show() - rl.question(' Email: ', e => { - cursor.hide() - rl.pause() - if (e === 'q') { - process.exit() - } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) { - cls() - return getEmail('Not a valid email address, try again.') - } - - getPass(e) - }) -} - -/* API */ - -const fetchList = () => { - get('/api/notes', showList, conf.session) -} - -const fetchNote = note => { - draw = { t: 'p', v: () => ['Loading...'] } - drawApp() - get(`/api/notes/${note.noteId}`, (content) => saveTmpFile(note, content), conf.session) -} - -const removeNote = note => { - draw = { t: 'p', v: () => ['Removing note...'] } - drawApp() - remove(`/api/notes/${note._id}`, noteRemoved, conf.session) -} - -const putNote = ({ _id, noteId }, content) => { - put(`/api/notes/${_id}`, { noteId, content }, noteSaved, conf.session) -} - -const putTitle = ({ _id, noteId }, title) => { - put(`/api/notes/${_id}`, { noteId, title }, noteSaved, conf.session) -} - -const createNote = (title, content) => { - post('/api/notes', { title, content }, noteSaved, conf.session) -} - -/* HTTPS METHODS */ - -const get = (path, callback, cookie = '') => { - const options = { - hostname: 'apps.pruss.it', - port: 443, - path, - method: 'GET', - headers: { - 'Cookie': cookie, - }, - }; - - const req = https.request(options, res => { - let data = ''; - - res.on('data', chunk => { - data += chunk - }); - - res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); - }); - - req.on('error', () => { - callback(null) - }); - - req.end(); -} - -const post = (path, data, callback, cookie = '', put = false) => { - const dataString = JSON.stringify(data) - const options = { - hostname: 'apps.pruss.it', - port: 443, - path, - method: put ? 'PUT' : 'POST', - headers: put ? { - 'Cookie': cookie, - } : { - 'Content-Type': 'text/plain', - 'Content-Length': dataString.length, - 'Cookie': cookie, - }, - timeout: 1000, - }; - - const req = https.request(options, (res) => { - let data = ''; - - res.on('data', chunk => { - data += chunk; - }); - - res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); - }).on("error", () => { - callback(null) - }); - - req.write(dataString); - req.end(); -} - -const put = (...arg) => post(...arg, true) - -const remove = (path, callback, cookie = '') => { - const options = { - hostname: 'apps.pruss.it', - port: 443, - path, - method: 'DELETE', - headers: { - 'Cookie': cookie, - }, - }; - - const req = https.request(options, res => { - let data = ''; - - res.on('data', chunk => { - data += chunk - }); - - res.on('end', () => callback(JSON.parse(data), res.statusCode, res.headers)); - }); - - req.on('error', () => { - callback(null) - }); - - req.end(); -} - -cls() -getSession() -process.stdout.on('resize', drawApp); -cursor.hide() -- cgit v1.2.3