diff options
author | 2022-05-05 20:43:13 +0100 | |
---|---|---|
committer | 2022-05-05 20:43:13 +0100 | |
commit | 6b2c9151f1e687025c6793b47ffda600d2fc089e (patch) | |
tree | 0933e94796b219ab00d07464d7442fa62adbe0af /notes_cli.js | |
parent | 094349e061cb9b094ddba330322c7eace55a6393 (diff) | |
download | notes_cli-6b2c9151f1e687025c6793b47ffda600d2fc089e.tar.gz notes_cli-6b2c9151f1e687025c6793b47ffda600d2fc089e.tar.bz2 notes_cli-6b2c9151f1e687025c6793b47ffda600d2fc089e.zip |
Login, config file, notes list
Diffstat (limited to 'notes_cli.js')
-rwxr-xr-x | notes_cli.js | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/notes_cli.js b/notes_cli.js new file mode 100755 index 0000000..be427d8 --- /dev/null +++ b/notes_cli.js @@ -0,0 +1,341 @@ +const fs = require('fs') +const { homedir } = require('os') + +let conf + +const https = require('https') +const readline = require("readline") +// const spawn = require('child_process').spawn + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +const cl = (m, c) => console.log(c ? `\x1b[${c}m${m}\x1b[0m` : m) +const cls = () => console.clear() + +const configPath = `${homedir}/.local/share/notes_cli` + +// app view +let active = 0; scroll = 0; draw = { t: 'p', v: () => ['Loading NOTES CLI...'] }, headers = () => '' +const pr = t => process.stdout.write(t) +const cursor = { + hide: () => pr('\u001B[?25l'), + show: () => pr('\u001B[?25h') +} + +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 showList = (notes, status) => { + if (status !== 200) { + cls() + return login('Session error') + } + headers = () => { + const columns = process.stdout.columns + return columns > 72 + ? `${fixedStr(' Title', columns - 44)} Created at${' '.repeat(10)} Updated at ${' '.repeat(9)}` + : ` Title ${' '.repeat(columns - 9)}` + } + cursor.hide() + process.stdout.on('resize', drawApp); + draw = { t: 'l', v: () => notes.map(formatNote) } + + drawApp() + getKey() +} + + +// =========== DRAW APP ============= + +const drawApp = () => { + const lines = process.stdout.rows + const columns = process.stdout.columns + // process.stdout.write('\x1Bc') + 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: + pr('╔'+'═'.repeat(columns - 2)+'╗') + break + case 1: + pr('║'+' '.repeat(dist(conf.email.length+12)[0])+`NOTES CLI (${conf.email})`.substring(0, columns-2)+' '.repeat(dist(conf.email.length+12)[1])+'║') + break + case 2: + pr('╠'+'═'.repeat(columns - 2) +'╣') + break + case 3: + const h = headers() + pr('║'+(h || ' '.repeat(columns-2))+'║') + break + case 4: + pr('╟'+'─'.repeat(columns - 2) +'╢') + break + case lines - 3: + pr('╠'+'═'.repeat(columns - 2) +'╣') + break + case lines - 2: + const menu = '[Q]uit [↓/j] Down [↑/k] Up [Enter/o] Open' + pr('║'+' '+menu.substring(0,columns-4)+' '.repeat(columns-menu.length-3 > 0 ? columns-menu.length-3 : 1)+'║') + break + default: + const max = Math.max(...(draw.v().map(el => el.length))) + if (draw.t === 'p') { + if (i === (Math.floor((lines - draw.v().length) / 2) - 2)){ + pr('║'+' '.repeat(dist(max+4)[0])+'┌'+'─'.repeat(max+2)+'┐'+' '.repeat(dist(max+4)[1])+'║') + } else if (i === (Math.floor((lines + draw.v().length) / 2) + 1)) { + pr('║'+' '.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))) { + pr('║'+' '.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)))) { + pr('║'+' '.repeat(columns - 2)+'║') + } else { + const n = i - (Math.floor((lines - draw.v().length) / 2)) + const t = draw.v()[n] + const s = [Math.floor((max+2-t.length) / 2), Math.ceil((max+2-t.length) / 2)] + pr('║'+' '.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) + + pr(l + ? ('║'+(active===i-5+scroll?'\x1b[7m ':' ')+l+' '.repeat(columns-l.length-3)+'\x1b[0m║') + : ('║'+' '.repeat(columns - 2)+'║') + ) + } + } + } + + pr('╚'+'═'.repeat(columns - 2)+'╝') +} + +// ============ GET KEY ================= + +const getKey = () => { + readline.emitKeypressEvents(process.stdin); + process.stdin.setRawMode(true); + process.stdin.on('keypress', (_, key) => { + const lines = process.stdout.rows + switch(key.name) { + case 'up': + case 'k': + active > 0 && active-- + active < scroll && scroll-- + drawApp() + break + case 'down': + case 'j': + if (draw.t === 'l') { + active < draw.v().length - 1 && active++ + active - scroll > lines - 7 && scroll++ + drawApp() + } + break + case 'return': + case 'o': + draw = { t: 'p', v: () => ['popup test', '', `This will display note nr ${active + 1}.`] } + drawApp() + // process.stdout.write(draw.v[active]); + // openNote(active) + break + case 'q': + cursor.show() + console.clear() + pr('Bye!\n\n') + process.exit() + default: + } + }) +} + + +/* Configuration */ + +const setConf = (c, callback) => { + fs.writeFile(configPath, JSON.stringify(c), function(err) { + if(err) { + cl(err, 32) + cl('Error writting configuration file', 32) + return null + } + + conf = c + return callback() + }) +} + +const getConf = () => { + fs.readFile(configPath, function(err, f){ + if (err) { + return login() + } + + try { + const r = JSON.parse(f.toString()) + + if (!r.session || !r.userId || !r.email || !r.list) { + return login('Error parsing configuration') + } + + fetchList(r) + } catch (e) { + return login('Error reading configuration') + } + }) +} + +/* Login */ + +const login = (error = '') => { + cl('\n Login to apps.pruss.it\n\n') + cl(' ┌'+'─'.repeat(50)+'┐\n'+' │'+' '.repeat(50)+'│\n'+' │ Account can be created on '+ + 'apps.pruss.it website. │\n'+' │ To quit type "q" and press enter.'+' '.repeat(16)+'│\n'+ + ' │'+' '.repeat(50)+'│\n'+' └'+'─'.repeat(50)+'┘\n', 32) + cl(' '+error+'\n', 31) + rl.resume() + rl.question(' Email: ', e => { + if (e === 'q') { + cl('\nExiting My Apps', 31) + rl.close() + process.exit(1) + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)) { + cls() + rl.pause() + return login('Not a valid email address, try again.') + } + + rl.stdoutMuted = true + + rl.question(' Password: ', p => { + if (p === 'q') { + cl('\nExiting My Apps', 31) + rl.close() + process.exit(1) + } + + rl.pause() + + post( + '/api/login', + {"email": e, "password": p}, + (o, s, h) => { + if (!o || s !== 201) { + rl.pause() + cls() + rl.stdoutMuted = false + return login('Could not log in, try again') + } + + const {_id, email, isVerified, noteList} = o + const session = h['set-cookie'] + + if (!_id || !email || !noteList) { + cl('Could not log in', 31) + process.exit(1) + } else if (!isVerified) { + cl('User not verified.\nPlease first verify the user using apps.pruss.it', 32) + return 1 + } + + const c = {userId: _id, email, list: noteList, session} + + setConf(c, () => { + cl('Successfully saved configuration', 32) + cl('saved config: ', conf) + }) + }) + }) + + rl._writeToOutput = function _writeToOutput(stringToWrite) { + if (rl.stdoutMuted) + rl.output.write("*") + else + rl.output.write(stringToWrite) + }; + + rl.history = rl.history.slice(1) + }) +} + +/* API */ + +const fetchList = config => { + conf = config + drawApp() + get('/api/notes', showList, 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 = '') => { + const dataString = JSON.stringify(data) + const options = { + hostname: 'apps.pruss.it', + port: 443, + path, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + '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(); +} + +cls() +getConf() |