Initial version

master
Sven Slootweg 8 years ago
parent c61801d0ce
commit fd74f06dc1

4
.gitignore vendored

@ -2,4 +2,6 @@
# https://help.github.com/articles/ignoring-files
# Example .gitignore files: https://github.com/github/gitignore
/bower_components/
/node_modules/
/node_modules/
/notes/
/lib/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="233"
height="91"
id="svg2"
version="1.1"
inkscape:version="0.48.1 r9760"
sodipodi:docname="npm.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="128.62279"
inkscape:cy="127.36086"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1440"
inkscape:window-height="827"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Camada 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-109.57142,-485.2193)">
<path
style="fill:#cb3837;fill-opacity:1"
d="m 174.4429,576.2193 0,-13.01931 -64.87148,0 1.9e-4,-77.98069 233.35644,0 -1.9e-4,77.98069 -116.67804,0 0,13.01931 z"
id="path4951" />
<path
style="fill:#ffffff;fill-opacity:1"
d="m 213.63597,563.19999 0,-13.1094 25.67822,0 0,-51.76189 -51.71684,0 0,64.87129 z"
id="path4949" />
<path
style="fill:#cb3837;fill-opacity:1"
d="m 213.63597,511.34801 12.61385,0 0,25.67822 -12.61385,0 z"
id="path4947" />
<path
style="fill:#ffffff;fill-opacity:1"
d="m 148.31419,550.09059 0,-38.74258 13.06435,0 0,38.74258 13.06436,0 0,-51.76189 -51.71226,0 0,51.76189 z"
id="path4945" />
<path
style="fill:#ffffff;fill-opacity:1"
d="m 278.05676,550.09059 0,-38.74258 13.06435,0 0,38.74258 13.06436,0 0,-38.74258 13.06435,0 0,38.74258 13.06437,0 0,-51.76189 -77.93404,0 0,51.76189 z"
id="path2998" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,112 @@
var gulp = require("gulp");
var fileUrl = require("file-url");
var path = require("path");
var xtend = require("xtend");
var stream = require("stream");
var rfr = require("rfr");
var livereload = require("gulp-livereload");
var rename = require("gulp-rename");
var presetES2015 = require("@joepie91/gulp-preset-es2015");
var presetJade = require("@joepie91/gulp-preset-jade");
var presetSCSS = require("@joepie91/gulp-preset-scss");
var presetRiot = require("@joepie91/gulp-preset-riot");
var patchLivereloadLogger = require("@joepie91/gulp-partial-patch-livereload-logger");
var runElectron = require("@joepie91/gulp-partial-electron");
patchLivereloadLogger(livereload);
var sources = {
"babel-main": "app.js",
"babel-lib": "src/**/*.js",
"jade-views": "src/views/**/*.jade",
"sass-main": "src/stylesheets/**/*.scss",
"riot-components": "src/components/**/*.tag",
"electron": ["lib/**/*.js", "lib/views/**/*.html"]
}
function waitStream(duration) {
var dummyStream = new stream.Readable();
dummyStream._read = function() {
setTimeout(function() {
dummyStream.push(null);
}, duration);
}
return dummyStream;
}
var electronProcess;
gulp.task("electron", ["electron-kill", 'babel-lib', 'babel-main', 'jade-views', 'sass-main', 'riot-components'], function() {
electronProcess = runElectron();
});
gulp.task("electron-kill", function() {
if (electronProcess != null) {
console.log("Killing old Electron process...")
electronProcess.kill("SIGINT");
/* To ensure that the process really has exited... */
return waitStream(200);
}
})
gulp.task('babel-main', function() {
return gulp.src(sources["babel-main"])
.pipe(presetES2015({
livereload: livereload,
basePath: __dirname
}))
.pipe(rename("app.es5.js"))
.pipe(gulp.dest("./"));
});
gulp.task('babel-lib', function() {
return gulp.src(sources["babel-lib"])
.pipe(presetES2015({
livereload: livereload,
basePath: __dirname
}))
.pipe(gulp.dest("./lib/"));
});
gulp.task("jade-views", function() {
return gulp.src(sources["jade-views"])
.pipe(presetJade({
livereload: livereload,
basePath: __dirname
}))
.pipe(gulp.dest("lib/views/"));
});
gulp.task("sass-main", function() {
return gulp.src(sources["sass-main"])
.pipe(presetSCSS({
livereload: livereload,
basePath: __dirname
}))
.pipe(gulp.dest("lib/stylesheets/"));
});
gulp.task("riot-components", function() {
return gulp.src(sources["riot-components"])
.pipe(presetRiot({
livereload: livereload,
basePath: __dirname
}))
.pipe(gulp.dest("lib/components/"));
});
gulp.task('watch', ['babel-lib', 'babel-main', 'jade-views', 'sass-main', 'riot-components'], function () {
livereload.listen();
Object.keys(sources).forEach(function(source) {
gulp.watch(sources[source], [source]);
});
});
gulp.task('default', ['watch', 'electron']);

@ -0,0 +1,3 @@
'use strict';
module.exports = require("./lib/app");

@ -0,0 +1,52 @@
{
"name": "npmbar",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.cryto.net:joepie91/npmbar.git"
},
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"appdirectory": "^0.1.0",
"bhttp": "^1.2.1",
"bluebird": "^3.3.5",
"create-error": "^0.3.1",
"default-value": "0.0.3",
"document-offset": "^1.0.4",
"document-ready-promise": "^3.0.1",
"electron-prebuilt": "^1.0.1",
"file-url": "^1.1.0",
"in-array": "^0.1.2",
"is-function": "^1.0.1",
"lokijs": "^1.3.16",
"match-object": "0.0.2",
"menubar": "^4.1.1",
"mkdirp": "^0.5.1",
"object-pick": "^0.1.2",
"ps-tree": "^1.0.1",
"rfr": "^1.2.3",
"riot": "^2.4.0",
"riot-query": "0.0.3",
"sanitize-filename": "^1.6.0",
"xtend": "^4.0.1"
},
"devDependencies": {
"@joepie91/gulp-partial-electron": "^1.0.1",
"@joepie91/gulp-partial-patch-livereload-logger": "^1.0.1",
"@joepie91/gulp-preset-es2015": "^1.0.1",
"@joepie91/gulp-preset-jade": "^1.0.1",
"@joepie91/gulp-preset-riot": "^1.0.1",
"@joepie91/gulp-preset-scss": "^1.0.1",
"babel-preset-es2015": "^6.6.0",
"file-url": "^1.1.0",
"gulp": "^3.9.1",
"gulp-livereload": "^3.8.1",
"gulp-rename": "^1.2.2"
}
}

@ -0,0 +1,88 @@
'use strict';
const Promise = require("bluebird");
const path = require("path");
const menubar = require("menubar");
const appDirectory = require("appdirectory");
const xtend = require("xtend");
const mkdirpAsync = Promise.promisify(require("mkdirp"));
const fileUrl = require("file-url");
const rfr = require("rfr");
const {app, globalShortcut, ipcMain} = require("electron");
const errors = rfr("lib/util/errors");
const executeFunction = rfr("lib/electron/execute-function");
let appDirectories = new appDirectory("npmbar");
let cacheExpiry = 300;
let baseHeight = 58;
console.log("Creating menubar...")
let bar = menubar({
index: fileUrl(path.join(__dirname, "views", "index.html")),
icon: path.join(__dirname, "..", "assets", "icon.png"),
"preload-window": true,
"always-on-top": true,
width: 700,
height: baseHeight
});
bar.on("ready", () => {
//bar.window.toggleDevTools();
globalShortcut.register("F8", () => {
if (bar.window.isVisible()) {
bar.hideWindow();
} else {
bar.showWindow();
}
});
bar.on("after-show", () => {
bar.window.setAlwaysOnTop(true);
bar.window.focusOnWebView();
bar.window.webContents.send("focusSearch");
})
//bar.showWindow();
});
ipcMain.on("resize", (event, newSize) => {
if (newSize.width != null) {
bar.setOption("width", newSize.width);
}
if (newSize.height != null) {
bar.setOption("height", newSize.height);
}
/* Temporary workaround until maxogden/menubar#125 is resolved */
bar.window.setSize(bar.getOption("width"), bar.getOption("height"));
})
Promise.try(() => {
return mkdirpAsync(appDirectories.userData());
}).then(() => {
return rfr("lib/db/json-db")(path.join(appDirectories.userData(), "db.json"));
}).then((db) => {
let metadataCache = db.collection("metadataCache");
const getPackageMetadata = rfr("lib/package/fetch-metadata")({
CacheError: errors.CacheError,
get: function(packageName) {
try {
return metadataCache.findOne({name: packageName});
} catch (err) { // FIXME
throw new errors.CacheError("Not in cache");
}
},
set: function(packageName, metadata) {
return metadataCache.upsertBy("name", metadata);
}
});
});

@ -0,0 +1,74 @@
app
.logo-section
img(src="../../assets/npm.svg", height=40)
.bar-section
search-box
search-results(results="{results}")
.window-height-marker
script.
const Promise = require("bluebird");
const rfr = require("rfr");
const search = rfr("lib/search/constructor-io")("CD06z4gVeqSXRiDL2ZNK");
this.mixin(require("riot-query").mixin);
let lastQuery;
this.results = []
this.on("mount", () => {
let searchBox = this.queryOne("search-box");
let searchResults = this.queryOne("search-results");
searchBox.on("selectionUp", () => {
searchResults.moveSelectionUp();
});
searchBox.on("selectionDown", () => {
searchResults.moveSelectionDown();
});
searchBox.on("queryChanged", (query) => {
Promise.try(() => {
lastQuery = query;
return search(query);
}).then((results) => {
/* Sometimes, search results may come back out of order. Here we check whether
* the lastQuery is still the same as when we initially made the request. */
if (lastQuery === query) {
this.results = results.packages.map((pkg) => {
return {
name: pkg.value,
description: pkg.data.description
}
});
this.update();
}
});
})
});
this.on("updated", () => {
global.triggerWindowResize();
});
style(scoped, type="scss").
.logo-section {
position: absolute;
left: 0px;
top: 0px;
bottom: 0px;
width: 120px;
text-align: center;
background-color: #2a333c;
padding: 9px;
}
.bar-section {
position: absolute;
left: 138px;
top: 0px;
bottom: 0px;
right: 0px;
}

@ -0,0 +1,6 @@
'use strict';
require("../search-box");
require("../search-results");
require("./component");

@ -0,0 +1,48 @@
search-box
input.search(placeholder="Search...", onkeydown="{_handleKeyDown}")
script.
let lastKnownQuery;
Object.assign(this, {
_handleKeyDown: (event) => {
switch (event.code) {
case "ArrowDown":
this.trigger("selectionDown");
break;
case "ArrowUp":
this.trigger("selectionUp");
break;
case "Enter":
this.trigger("confirm");
break;
case "Escape":
this.trigger("cancel");
break;
}
let searchInput = this.root.querySelector(".search");
if (searchInput.value !== lastKnownQuery) {
lastKnownQuery = searchInput.value;
this.trigger("queryChanged", searchInput.value);
}
return true;
}
})
style(scoped, type="scss").
.search {
border: 0px;
padding: 17px 9px;
font-size: 21px;
background-color: transparent;
width: 100%;
color: white;
font-weight: bold;
&::-webkit-input-placeholder {
color: #d99d9d;
}
}

@ -0,0 +1,3 @@
'use strict';
require("./component");

@ -0,0 +1,52 @@
search-results
.result(each="{result, i in opts.results}", class="{active: i === currentSelection}")
h2 {result.name}
.description {result.description}
script.
Object.assign(this, {
currentSelection: 0,
moveSelectionDown: function() {
if (this.currentSelection + 1 < opts.results.length) {
this.currentSelection += 1;
this.update();
}
},
moveSelectionUp: function() {
if (this.currentSelection >= 1) {
this.currentSelection -= 1;
this.update();
}
}
})
this.on("update", () => {
/* Reset the cursor/selection state... */
this.currentSelection = 0;
});
style(scoped, type="scss").
.result {
background-color: white;
color: #676767;
padding: 9px;
border-top: 1px solid #2a333c;
&:first-child {
border-top: 0px;
}
&.active {
background-color: #e2e2e2;
}
h2 {
color: black;
font-size: 21px;
margin: 0px;
}
.description {
font-size: 13px;
}
}

@ -0,0 +1,3 @@
'use strict';
require("./component");

@ -0,0 +1,11 @@
'use strict';
const crypto = require("crypto");
const sanitizeFilename = require("sanitize-filename");
module.exports = function escapeCollectionName(name) {
let hash = crypto.createHash("sha256").update(name, "utf8").digest("hex");
/* We 'sanitize' the entire filename with hash included, to ensure that the result has the right maximum length. */
return sanitizeFilename(`${name}_${hash}.db`);
}

@ -0,0 +1,202 @@
'use strict';
/* Rolling your own database is a bad idea. In this particular
* case, I am doing it anyway, because there are very clear
* constraints, no extreme performance requirements, and most
* importantly, all the other in-memory databases seem to
* -suck- from an API/usability point of view.
*
* Limitations:
* - Only one process can use the DB at the same time. Yes,
* it's a reading lock.
* - Persistence is not guaranteed. If the process crashes,
* writes may be lost.
* - No performance guarantees, whatsoever.
* - No schemas.
*/
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
const matchObject = require("match-object");
const uuid = require("uuid");
const pick = require("object-pick");
const rfr = require("rfr");
const promiseDebounce = rfr("lib/util/promise-debounce");
const escapeCollectionName = rfr("lib/db/escape-collection-name"); // FIXME: Actually split this up into collections...
function getLockPath(path) {
return `${path}.lock`;
}
function obtainLock(path) {
return Promise.try(() => {
return fs.writeFileAsync(getLockPath(path), "", {
flag: "wx"
});
});
}
function releaseLock(path) {
return Promise.try(() => {
return fs.unlinkAsync(getLockPath(path));
});
}
function loadDatabase(path) {
return Promise.try(() => {
return obtainLock(path);
}).then(() => {
return fs.readFileAsync(path);
}).then((data) => {
return JSON.parse(data);
}).catch({code: "ENOENT"}, (err) => {
/* Initialize a blank database */
return {};
});
}
function saveDatabase(path, collections) {
return Promise.try(() => {
return fs.writeFileAsync(path, JSON.stringify(collections));
});
}
module.exports = function(path) {
return Promise.try(() => {
return loadDatabase(path);
}).then((collections) => {
let queueWrite = promiseDebounce(function() {
if (db.opened === true) {
saveDatabase(path, collections);
}
});
function getCollection(name) {
let indexes; // TODO
function findIdIndex(id) {
if (id == null) {
return null;
}
return collections[name].findIndex((item) => item.$id === id);
}
return {
find: function(query) {
return collections[name].filter((item) => matchObject(query, item));
},
findOne: function(query) {
let result = collections[name].find((item) => matchObject(query, item));
if (result == null) {
throw new Error("No results found");
} else {
return result;
}
},
insert: function(object, options = {}) {
/* Intentional mutation. */
Object.assign(object, {$id: uuid.v4()});
collections[name].push(object);
queueWrite();
},
update: function(object, options = {}) {
let index;
if (index = findIdIndex(object.$id)) {
if (options.patch === true) {
Object.assign(collections[name][index], object);
} else {
collections[name][index] = object;
}
queueWrite();
} else {
throw new Error("No such object exists");
}
},
upsert: function(object, options = {}) {
try {
this.update(object, options);
} catch (err) {
this.insert(object, options);
}
},
upsertBy: function(keys, object, options = {}) {
let query = pick(object, keys);
try {
let result = this.findOne(query);
// FIXME: The following shouldn't be in the try block...
object.$id = result.$id;
return this.update(object, options);
} catch (err) {
return this.insert(object, options);
}
},
delete: function(object) {
let index;
if (index = findIdIndex(object.$id)) {
collections[name].splice(index, 1);
queueWrite();
} else {
throw new Error("No such object exists");
}
},
deleteBy: function(query) {
collections[name] = collections[name].filter((item) => !matchObject(query, item));
queueWrite();
},
ensureIndex: function(property) {
// TODO
}
}
}
// TODO: Cache/TTL collections
// TODO: .bak file
let db = {
opened: true,
close: function() {
return Promise.try(() => {
return queueWrite();
}).then(() => {
return releaseLock(path);
}).then(() => {
this.opened = false;
});
},
collection: function(name) {
if (collections[name] == null) {
collections[name] = [];
}
return getCollection(name);
}
}
function cleanup() {
console.log("Doing cleanup...");
if (db.opened === true) {
db.close();
}
}
if (process.versions.electron != null) {
/* Running in Electron */
require("electron").app.on("will-quit", () => {
cleanup();
})
} else {
/* Running in a different Node-y environment */
process.on("beforeExit", () => {
cleanup();
});
}
return db;
});
}

@ -0,0 +1,10 @@
'use strict';
function packFunction(func) {
return `;${func.toString()};${func.name}();`;
}
module.exports = function(window, func) {
let stringifiedFunc = packFunction(func);
window.webContents.executeJavaScript(stringifiedFunc);
}

@ -0,0 +1,11 @@
'use strict';
let regexes = [
/^(?:event:)?'[a-z:\$%-]+'$/i, // events
/[a-z0-9_]+\(/, // function signatures
/^[a-z0-9_]+(\.[a-z0-9_]+)+/ // attributes
]
module.exports = function(string) {
return regexes.some((regex) => regex.test(string));
}

@ -0,0 +1,14 @@
'use strict';
let readmeFilenames = [
"readme.md",
"readme.rst",
"readme",
"read me"
]
module.exports = function(filenames) {
/* We invert the search here, so that we prioritize by the order of entries
* in readmeFilenames, rather than the filenames passed in. */
return readmeFilenames.find((filename) => inArray(filenames, filename));
}

@ -0,0 +1,20 @@
'use strict';
module.exports = function(header, contents) {
if (/^(?:module )?api$/i.test(header)) {
return "api";
} else if (/^events$/i.test(header)) {
return "events";
} else if (/^options$/i.test(header)) {
return "options";
} else if (/^(?:tips|notes)$/i.test(header)) {
return "notes";
} else if (/(?:usage|examples?)/i.test(header)) {
// FIXME: Detect API methods under 'usage'
return "example";
} else if (/(?:caution|^warning$)/i.test(header)) {
return "warning";
} else {
return "other";
}
}

@ -0,0 +1,42 @@
'use strict';
const Promise = require("bluebird");
const bhttp = require("bhttp");
module.exports = function(cache) {
function getFromRemote(packageName) {
return Promise.try(() => {
let encodedPackageName = encodeURIComponent(packageName).replace(/%40/g, "@");
return bhttp.get(`https://registry.npmjs.org/${encodedPackageName}`);
}).then((response) => {
if (response.statusCode !== 200) {
// FIXME: Proper error types
throw new Error(`Got non-200 status code from NPM registry: ${response.statusCode}`);
} else {
return response.body;
}
}).tap((metadata) => {
cache.set(packageName, metadata);
});
}
function getFromCache(packageName) {
return Promise.try(() => {
return cache.get(packageName);
}).then((metadata) => {
if (metadata._cacheExpiry < Date.now()) {
throw new cache.CacheError("Cached data expired");
} else {
return metadata;
}
});
}
return function(packageName) {
return Promise.try(() => {
return getFromCache(packageName);
}).catch(cache.CacheError, (err) => {
return getFromRemote(packageName);
});
}
}

@ -0,0 +1,33 @@
'use strict';
const Promise = require("bluebird");
const bhttp = require("bhttp");
const querystring = require("querystring");
const defaultValue = require("default-value");
module.exports = function(autocompleteKey) {
function createQueryString(query, options) {
return querystring.stringify({
autocomplete_key: autocompleteKey,
_: Date.now(),
query: query,
num_results: defaultValue(options.resultCount, 20)
});
}
function createUrl(query, options) {
return `https://ac.cnstrc.com/autocomplete/${encodeURIComponent(query)}?${createQueryString(query, options)}`;
}
return function doSearch(query, options) {
return Promise.try(() => {
return bhttp.get(createUrl(query, options));
}).then((response) => {
if (response.statusCode === 200) {
return response.body;
} else {
throw new Error(`Non-200 status code encountered: ${response.statusCode}`)
}
});
}
}

@ -0,0 +1,7 @@
'use strict';
const createError = require("create-error");
module.exports = {
CacheError: createError("CacheError")
}

@ -0,0 +1,41 @@
'use strict';
const Promise = require("bluebird");
module.exports = function(func) {
let operationQueued = false;
let queuedHandlers = [];
let currentPromise;
function runFunc() {
return Promise.try(() => {
return func();
}).tap(() => {
if (operationQueued === true) {
currentPromise = runFunc();
queuedHandlers.forEach((handler) => {
handler(currentPromise);
});
operationQueued = false;
queuedHandlers = [];
} else {
currentPromise = null;
}
});
}
return function() {
return new Promise((resolve, reject) => {
if (currentPromise == null) {
currentPromise = runFunc();
resolve(currentPromise);
} else {
operationQueued = true;
queuedHandlers.push(resolve);
}
})
}
}

@ -0,0 +1,15 @@
'use strict';
const isFunction = require("is-function");
module.exports = function(object, additions) {
let wrappedObject = {};
Object.getOwnPropertyNames(object).forEach((key) => {
if (isFunction(object[key])) {
wrappedObject[key] = object[key].bind(object);
}
});
return Object.assign(wrappedObject, additions);
}

@ -0,0 +1,14 @@
html
head
meta(charset="UTF-8")
title Hello World!
style.
body {
background-color: #C12127;
color: white;
font-family: sans-serif;
overflow: hidden;
}
script(src="index.js")
body
app

@ -0,0 +1,40 @@
'use strict'
const Promise = require("bluebird");
const documentReady = require("document-ready-promise");
const documentOffset = require("document-offset");
const util = require("util");
const riot = require("riot");
const {ipcRenderer} = require("electron");
require("../components/app");
global.triggerWindowResize = function() {
let marker = document.querySelector(".window-height-marker");
let pageHeight;
if (marker != null) {
pageHeight = documentOffset(marker).top;
} else {
let body = document.querySelector("body");
let rootElement = document.documentElement;
pageHeight = Math.max(body.scrollHeight, body.offsetHeight, rootElement.scrollHeight, rootElement.offsetHeight, rootElement.clientHeight);
}
ipcRenderer.send("resize", {
height: pageHeight
});
}
Promise.try(() => {
return documentReady();
}).then(() => {
riot.mount("app");
global.triggerWindowResize();
ipcRenderer.on("focusSearch", () => {
document.querySelector("input.search").focus();
});
})
Loading…
Cancel
Save