text:
    // ==UserScript==
    // @name         betterTradingeconomics
    // @namespace    https://github.com/amit0rana/betterTradingeconomics
    // @version      0.04
    // @description  Introduces small features on top of betterTradingeconomics site
    // @author       Amit
    // @match        https://tradingeconomics.com/*
    // @match        https://in.investing.com/*
    // @grant        GM_addStyle
    // @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
    // @require      https://raw.githubusercontent.com/amit0rana/MonkeyConfig/master/monkeyconfig.js
    // @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
    // @require      https://raw.githubusercontent.com/amit0rana/betterOptionsTrading/master/betterCommon.js
    // @downloadURL  https://github.com/amit0rana/betterOptionsTrading/raw/master/betterTradingeconomics.user.js
    // @updateURL    https://github.com/amit0rana/betterOptionsTrading/raw/master/betterTradingeconomics.meta.js
    // ==/UserScript==

    const D_LEVEL = D_LEVEL_NONE;
    var currentUrl = window.location.pathname;
    console.log(currentUrl);

//tradingeconomics.com
    function stockFilter() {
        if (currentUrl.includes('stocks')) {
            const domForAdPanel = '[data-ad=ad-below-menu]';
            //$('div#ctl00_ctl07_AdPanel').hide();
            $(domForAdPanel).hide();

            //jQ(allDOMPaths.PathForPositions).filter(':has(:checkbox:checked)');

            var statuses = [];
            var selectBox = document.createElement("SELECT");
            selectBox.id = "toggleSelectboxID";
            selectBox.classList.add("randomClassToHelpHide");
            //selectBox.style="margin: 15px 0;margin-top: 15px;margin-right: 0px;margin-bottom: 15px;margin-left: 0px;background-color: var(--color-bg-default)"

            var option = document.createElement("option");
            option.text = "Show All";
            option.value= "All";
            selectBox.add(option);

            var statusesRows = $('td#session > span');
            console.log(statusesRows.length);
            statusesRows.each(function(rowIndex) {
                var title = $(this).attr('title');
                if (!statuses.includes(title)) {
                    statuses.push(title);

                    var option = document.createElement("option");
                    option.text = title;
                    option.value = title;
                    selectBox.add(option);
                }

            });
            console.log(statuses);

            selectBox.addEventListener("change", function() {
                var selectedItem = this.value;

                var allRows = $('tr');

                allRows.each(function(rowIndex) {
                    if ($(this).find('th').length > 0) {
                        return;
                    }
                    var session = $(this).find(`[title='${selectedItem}']`);
                    if (selectedItem === 'All' ) {
                        $(this).show();
                    } else if (session.length > 0) {
                        $(this).show();
                    } else {
                        $(this).hide();
                    }

                });

            });

            $('#toggleSelectboxID').remove();
            //$(domForAdPanel).after(selectBox);
            $(".col-lg-10 > div:nth-child(5)").before(selectBox);

            var refreshBtn = document.createElement("button");
            refreshBtn.id = "refreshID";
            refreshBtn.type = "button";
            refreshBtn.innerHTML = "Refresh";
            refreshBtn.addEventListener("click", function() {
                stockFilter();
            });
            $('#refreshID').remove();
            $(domForAdPanel).after(refreshBtn);
        }
    }

    stockFilter();

//investing.in
function hideAds() {
    $('div.ads').hide();
}

hideAds();

repo_language: JavaScript

mime_type: text/plain


text:
// ==UserScript==
// @name         betterSensibull
// @namespace    https://github.com/amit0rana/betterSensibull
// @version      0.14
// @description  Introduces small features on top of sesibull
// @author       Amit
// @match        https://web.sensibull.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require      https://github.com/amit0rana/betterOptionsTrading/raw/master/common.js
// @require      https://raw.githubusercontent.com/amit0rana/MonkeyConfig/master/monkeyconfig.js
// @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
// @downloadURL  https://github.com/amit0rana/betterOptionsTrading/raw/master/betterSensibull.user.js
// @updateURL    https://github.com/amit0rana/betterOptionsTrading/raw/master/betterSensibull.meta.js
// ==/UserScript==


text:
// This is free and unencumbered software released into the public domain.

// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.

// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

// For more information, please refer to <https://unlicense.org>

const D_LEVEL_INFO = 2;
const D_LEVEL_DEBUG = 1;
const D_LEVEL_NONE = 100;

const log = function (level, logInfo) {
    switch (level) {
        case D_LEVEL_DEBUG:
            console.debug(logInfo);
            break;
        case D_LEVEL_INFO:
            console.info(logInfo);
            break;
        default:
            console.log(logInfo);
    }
}
const debug = function (logInfo) {
    if (D_LEVEL <= D_LEVEL_DEBUG) {
        log(D_LEVEL_DEBUG, logInfo);
    }
}
const info = function (logInfo) {
    if (D_LEVEL <= D_LEVEL_INFO) {
        log(D_LEVEL_INFO, logInfo);
    }
}
const formatter = Intl.NumberFormat('en-IN', {
    style: 'currency', currency: 'INR'
});

const reloadPage = function (values) {
    window.location.reload();
}

const getCookie = function (name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

const getFunctionName = function () {
    // var me = arguments.callee.toString();
    // me = me.substr('function '.length);
    // me = me.substr(0, me.indexOf('('));
    return getFunctionName.caller.name;
}


text:
// ==UserScript==
// @name         betterSensibull
// @namespace    https://github.com/amit0rana/betterSensibull
// @version      0.14
// @description  Introduces small features on top of sensibull
// @author       Amit
// @match        https://web.sensibull.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require      https://raw.githubusercontent.com/amit0rana/betterOptionsTrading/master/betterCommon.js
// @require      https://raw.githubusercontent.com/amit0rana/MonkeyConfig/master/monkeyconfig.js
// @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
// @downloadURL  https://github.com/amit0rana/betterOptionsTrading/raw/master/betterSensibull.user.js
// @updateURL    https://github.com/amit0rana/betterOptionsTrading/raw/master/betterSensibull.meta.js
// ==/UserScript==

console.log("bs: script load");
var context = window, options = "{    anonymizeIp: true,    colorDepth: true,    characterSet: true,    screenSize: true,    language: true}"; const hhistory = context.history, doc = document, nav = navigator || {}, storage = localStorage, encode = encodeURIComponent, pushState = hhistory.pushState, typeException = "exception", generateId = () => Math.random().toString(36), getId = () => (storage.cid || (storage.cid = generateId()), storage.cid), serialize = e => { var t = []; for (var o in e) e.hasOwnProperty(o) && void 0 !== e[o] && t.push(encode(o) + "=" + encode(e[o])); return t.join("&") }, track = (e, t, o, n, i, a, r) => { const c = "https://www.google-analytics.com/collect", s = serialize({ v: "1", ds: "web", aip: options.anonymizeIp ? 1 : void 0, tid: "UA-176741575-1", cid: getId(), t: e || "pageview", sd: options.colorDepth && screen.colorDepth ? `${screen.colorDepth}-bits` : void 0, dr: doc.referrer || void 0, dt: doc.title, dl: doc.location.origin + doc.location.pathname + doc.location.search, ul: options.language ? (nav.language || "").toLowerCase() : void 0, de: options.characterSet ? doc.characterSet : void 0, sr: options.screenSize ? `${(context.screen || {}).width}x${(context.screen || {}).height}` : void 0, vp: options.screenSize && context.visualViewport ? `${(context.visualViewport || {}).width}x${(context.visualViewport || {}).height}` : void 0, ec: t || void 0, ea: o || void 0, el: n || void 0, ev: i || void 0, exd: a || void 0, exf: void 0 !== r && !1 == !!r ? 0 : void 0 }); if (nav.sendBeacon) nav.sendBeacon(c, s); else { var d = new XMLHttpRequest; d.open("POST", c, !0), d.send(s) } }, tEv = (e, t, o, n) => track("event", e, t, o, n), tEx = (e, t) => track(typeException, null, null, null, null, e, t); hhistory.pushState = function (e) { return "function" == typeof history.onpushstate && hhistory.onpushstate({ state: e }), setTimeout(track, options.delay || 10), pushState.apply(hhistory, arguments) }, track(), context.ma = { tEv: tEv, tEx: tEx };

//window.jQ=jQuery.noConflict(true);
const VERSION = "v0.14";
const PRO_MODE = false;

const g_config = new MonkeyConfig({
    title: 'betterSensibull Settings',
    menuCommand: true,
    onSave: reloadPage,
    params: {
        logging: {
            type: 'select',
            choices: ['Info', 'Debug'],
            values: [D_LEVEL_INFO, D_LEVEL_DEBUG],
            default: D_LEVEL_INFO
        }
    }
});
const D_LEVEL = g_config.get('logging');

const SENSIBULL_DOM_PATHS = {
    domForPlacingToggleSelectBox: "button:contains('Clear Positions')",
    domForPositionsRows: "div.style__TradeTableRow-sc-szonbw-7",
    //domForPositionExpiry: 'div:nth-child(1) > div:nth-child(2) > div > div:nth-child(2)',
    domForPositionExpiry: 'div.tradesItemViewDate',
    // domForStrategySuggestions: '#app > div > div.page-sidebar-is-open.sn-page--builder.style__AppWrapper-djPJnZ.gvrWYn > div.sn-l__app-content.style__AppContent-haAgYm.korEfl > div.style__ContainerSpacing-kZpkBx.kJeLXd > div > div.style__BuilderWrapper-hHFjHn.nAQhs > div.style__BuilderColRight-jAAkJD.hbmLzB > div.style__BuilderPresetStrategiesWrapper-iJakRq.jDHsHZ',
    domForCheckbox: 'span > span:nth-child(1) > input'
};

waitForKeyElements("button:contains('Add New Trade')", function () {
    console.log('onwait');
    main();
});
$(document).on('click', "#app > div > div.style__AppWrapper-gfb86b-0.kMpPMs.page-sidebar-is-open.sn-page--builder > div.style__NavBarWrapper-gfb86b-4.Jdjhy.sn-l__navBar-wrapper > div > div:nth-child(2) > div > div:nth-child(1) > div", function () {
    console.log('onclick');

    main();
});

// all behavior related actions go here.
function main() {
    console.log("bs: main started");
    // $(SENSIBULL_DOM_PATHS.domForStrategySuggestions).remove();
    $("#toggleSelectboxID").remove();

    var rows = $(SENSIBULL_DOM_PATHS.domForPositionsRows);
    console.log('bs: rows: ' + rows.length);

    var selectBox = document.createElement("SELECT");
    selectBox.id = "toggleSelectboxID";
    selectBox.classList.add("randomClassToHelpHide");
    //selectBox.style="margin: 15px 0;margin-top: 15px;margin-right: 0px;margin-bottom: 15px;margin-left: 0px;background-color: var(--color-bg-default)"

    var option = document.createElement("option");
    option.text = "Toggle";
    option.value = "All";
    selectBox.add(option);

    var expiryArray = [];
    rows.each(function (rowIndex) {
        var t = getExpiryText($(this).find(SENSIBULL_DOM_PATHS.domForPositionExpiry).text());
        console.log('bs: t: ' + t);
        if (t && !expiryArray.includes(t)) {
            expiryArray.push(t);

            var option = document.createElement("option");
            option.text = t;
            option.value = t;
            selectBox.add(option);
        }
    });

    //$('.jss1557').click();
    var t = $(SENSIBULL_DOM_PATHS.domForPlacingToggleSelectBox);
    $(t[t.length - 1]).before(selectBox);

    selectBox.addEventListener("click", function () {
        console.log(this.value);
        var selectedItem = this.value;

        var rows = $(SENSIBULL_DOM_PATHS.domForPositionsRows);
        rows.each(function (rowIndex) {
            var t = getExpiryText($(this).find(SENSIBULL_DOM_PATHS.domForPositionExpiry).text());

            console.log('bs: text to cmp' + t);
            if (selectedItem == 'All' || (t && t == selectedItem)) {
                console.log('match found ' + t);
                var c = $(this).find(SENSIBULL_DOM_PATHS.domForCheckbox)[0].click();
                console.log(c);
                //console.log($(c[0]).attr('class'));
                //document.getElementsByClassName($(c[0]).attr('class'))[0].click();
            }
        });

        //selectBox.value = 'All';
    });

}

function getExpiryText(fullText) {
    //var expiry = fullText.split(' ');
    //var t = expiry[0] + ' ' + expiry[1];

    return fullText;
}

tEv("sensibull", "visit", "main", "");


text:
// ==UserScript==
// @name         betterOipulse
// @namespace    https://github.com/amit0rana/betterOipulse
// @version      0.01
// @description
// @author       Amit
// @match        https://oipulse.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require      https://raw.githubusercontent.com/amit0rana/MonkeyConfig/master/monkeyconfig.js
// @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
// @downloadURL  https://github.com/amit0rana/betterOptionsTrading/raw/master/betterOipulse.user.js
// @updateURL    https://github.com/amit0rana/betterOptionsTrading/raw/master/betterOipulse.meta.js
// ==/UserScript==

const formatter = Intl.NumberFormat('en-IN');

var i = document.createElement("INPUT");
i.type = 'button';
i.name='addLot';
i.value='Multiply 75';
i.classList.add("randomClassToHelpHide");
i.id='addLot';

function main() {
    $('#main_navbar').append(i);

    $(document).on('click',"#addLot",  function() {
        var allRows = $('#optionChainTable-indices > tbody > tr');
        allRows.each(function(rowIndex) {
            //2 , 3, 21, 22
            var col = $(this).find("td:nth-child(2)");
            var txt = parseInt($(col).text().split(",").join(""));

            if (!isNaN(txt)) {
                $(col).text(getNumber(txt));
            }

            col = $(this).find("td:nth-child(3)");
            txt = parseInt($(col).text().split(",").join(""));

            if (!isNaN(txt)) {
                $(col).text(getNumber(txt));
            }
            col = $(this).find("td:nth-child(21)");
            txt = parseInt($(col).text().split(",").join(""));

            if (!isNaN(txt)) {
                $(col).text(getNumber(txt));
            }
            col = $(this).find("td:nth-child(22)");
            txt = parseInt($(col).text().split(",").join(""));

            if (!isNaN(txt)) {
                $(col).text(getNumber(txt));
            }
        });
        changeButtonName();
    });

    $(document).on('change',"#expirySelect",  function() {
        isMultiply = false;
        changeButtonName();
    });
    $(document).on('click',".fullViewBtn",  function() {
        $(".randomClassToHelpHide").remove();
        $('#goBackSite').before(i);
    });
    $(document).on('click',"#goBackSite",  function() {
        $(".randomClassToHelpHide").remove();
        $('#main_navbar').append(i);
    });
}

main();


text:
// ==UserScript==
// @name         betterKite
// @namespace    https://github.com/amit0rana/betterKite
// @version      3.8
// @description  Introduces small features on top of kite app
// @author       Amit
// @match        https://kite.zerodha.com/*
// @match        https://console.zerodha.com/*
// @match        https://insights.sensibull.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_registerMenuCommand
// @require      https://raw.githubusercontent.com/amit0rana/betterOptionsTrading/master/betterCommon.js
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require      https://raw.githubusercontent.com/amit0rana/MonkeyConfig/master/monkeyconfig.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js
// @require      https://raw.githubusercontent.com/kawanet/qs-lite/master/dist/qs-lite.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js
// @require      https://unpkg.com/@popperjs/core@2
// @require      https://unpkg.com/tippy.js@6
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @require      https://cdn.jsdelivr.net/npm/toastify-js
// @resource     TOASTIFY_CSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @downloadURL  https://github.com/amit0rana/betterOptionsTrading/raw/master/betterKite.user.js
// @updateURL    https://github.com/amit0rana/betterOptionsTrading/raw/master/betterKite.meta.js
// ==/UserScript==

// This is free and unencumbered software released into the public domain.

// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.

// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

// For more information, please refer to <https://unlicense.org>

// SEBI MANDATORY DISCLAIMER AS REQUIRED BY SEBI

// KIND ATTENTION TO ALL USERS

// Disclaimer from Developers as per SEBI norms: Equity Investment are subject to 100%
// market risks. Refer your financial consultant advice before Investing. This code is
//  only for Educational and Learning, Knowledge Purposes. Developers have no
// responsibility for your intended decision & financial losses. Keep calculated & always
// analyzed your cash osition and risk bearing capacity before following any advise.
//  Stock market investments and using this script are VERY RISKY and using ths code, you
// agree that you understand the Market risks involved. Profits and Losses are part of Share market.
//  Kindly understand and act wisely.

// Disclaimer/ disclosure

// 👉This script does not provide any tips/recommendations/advice
// 👉All features are only for education and learning purpose.
// 👉Do Consult your financial advisor before taking trades or investment decisions
// 👉Developers are not responsible for any financial losses
// 👉Disclaimer/disclosure/terms and conditions applicable to all users of this script

// All the features are for education and learning purpose only👍


text:
var context = window;
var options = "{    anonymizeIp: true,    colorDepth: true,    characterSet: true,    screenSize: true,    language: true}";

const hhistory = context.history;
const doc = document;
const nav = navigator || {};
const storage = localStorage;
const encode = encodeURIComponent;
const pushState = hhistory.pushState;
const typeException = 'exception';
const generateId = () => Math.random().toString(36);
const getId = () => {
    if (!storage.cid) {
        storage.cid = generateId()
    }
    return storage.cid;
};
const serialize = (obj) => {
    var str = [];
    for (var p in obj) {
        if (obj.hasOwnProperty(p)) {
            if(obj[p] !== undefined) {
                str.push(encode(p) + "=" + encode(obj[p]));
            }
        }
    }
    return str.join("&");
};
const track = (
    type,
    eventCategory,
    eventAction,
    eventLabel,
    eventValue,
    exceptionDescription,
    exceptionFatal
) => {
    const url = 'https://www.google-analytics.com/collect';
    const data = serialize({
        v: '1',
        ds: 'web',
        aip: options.anonymizeIp ? 1 : undefined,
        tid: "UA-176741575-1",
        cid: getId(),
        t: type || 'pageview',
        sd: options.colorDepth && screen.colorDepth ? `${screen.colorDepth}-bits` : undefined,
        dr: doc.referrer || undefined,
        dt: doc.title,
        dl: doc.location.origin + doc.location.pathname + doc.location.search,
        ul: options.language ? (nav.language || "").toLowerCase() : undefined,
        de: options.characterSet ? doc.characterSet : undefined,
        sr: options.screenSize ? `${(context.screen || {}).width}x${(context.screen || {}).height}` : undefined,
        vp: options.screenSize && context.visualViewport ? `${(context.visualViewport || {}).width}x${(context.visualViewport || {}).height}` : undefined,
        ec: eventCategory || undefined,
        ea: eventAction || undefined,
        el: eventLabel || undefined,
        ev: eventValue || undefined,
        exd: exceptionDescription || undefined,
        exf: typeof exceptionFatal !== 'undefined' && !!exceptionFatal === false ? 0 : undefined,
    });

    if(nav.sendBeacon) {
        nav.sendBeacon(url, data)
    } else {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", url, true);
        xhr.send(data);
    }
};
const tEv = (category, action, label, value) => track('event', category, action, label, value);
const tEx = (description, fatal) => track(typeException, null, null, null, null, description, fatal);
hhistory.pushState = function (state) {
    if (typeof history.onpushstate == "function") {
        hhistory.onpushstate({ state: state });
    }
    setTimeout(track, options.delay || 10);
    return pushState.apply(hhistory, arguments);
}
track();
context.ma = {
    tEv,
    tEx
}

//https://support.google.com/analytics/answer/1033068?hl=en


text:
Disclaimer: I have made this tool for my personal use, it may have bugs.

This repository has 2 userscripts
* [For Zerodha](https://github.com/amit0rana/betterOptionsTrading/blob/master/betterKite.md)
* [For Upstox](https://github.com/amit0rana/betterOptionsTrading/blob/master/betterUpstox.md)
* [For Sensibull](https://github.com/amit0rana/betterOptionsTrading/blob/master/betterSensibull.md)
* [For NSEIndia site] (https://github.com/amit0rana/betterOptionsTrading/blob/master/betterNSE.md)


text:
// ==UserScript==
// @name         betterStocksDeveloper
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Small improvemnts to the betterStocks website
// @author       Amit
// @match        https://web.stocksdeveloper.in/*
// @icon         https://www.google.com/s2/favicons?domain=stocksdeveloper.in
// @grant        none
// @require      https://gist.github.com/raw/2625891/waitForKeyElements.js
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// ==/UserScript==

//(function() {
    'use strict';

    window.jQ = jQuery.noConflict(true);

    waitForKeyElements("#quantityTable", addBtn);
    console.log(jQ("div.pcoded-inner-navbar > ul"));
    jQ("div.pcoded-inner-navbar > ul").after('
Trade
');

    function addBtn() {
        console.log('addBtn');

        $("#updateQty").remove();
        var i = document.createElement("BUTTON");
        i.type = 'button';
        i.name = 'updateQty';
        i.value = 'Update Quantity';
        i.innerHTML = 'Update Quantity';
        i.id = 'updateQty';
        i.classList.add('btn');
        i.style = 'margin: 0px 2px;';
        i.classList.add('btn-secondary');

        i.onclick = function () {

            var selectedGroups = $("tr.selected");
            var accountSizes = [];
            var quantities = [];
            selectedGroups.each(function (rowIndex) {
                var accountSize = jQ(jQ(this).find('td')[1]).text();

                accountSizes.push(accountSize);
                var input = jQ(this).find('input')[0];
                quantities[accountSize] = jQ(input).val();
            });

            var minAccountSize = Math.min(...accountSizes);
            console.log(quantities);

            selectedGroups.each(function (rowIndex) {
                var accountSize = jQ(jQ(this).find('td')[1]).text();

                if (accountSize >= minAccountSize) {
                    var input = jQ(this).find('input')[0];
                    jQ(input).val((accountSize / minAccountSize) * quantities[minAccountSize]);
                }
            });
            //console.log(min);

        };

        $("#placeResetButton").before(i);

    }
    jQ(document).on('click', "#clearFilter", function (event) {
        jQ('#positionsTable_filter > label:nth-child(1) > input:nth-child(1)').val('').trigger('change').click().focus().keydown().keypress().keyup();
    });
    jQ(document).on('dblclick', "#positions-body", function (event) {
        jQ("#clearFilter").remove();
        jQ('#positionsTable_filter > label:nth-child(1) > input:nth-child(1)').after("r");
        jQ('#positionsTable_filter > label:nth-child(1) > input:nth-child(1)').val(jQ(event.target).text()).trigger('change').click().focus().keydown().keypress().keyup();
        addFilters();
    });
    //})();

    waitForKeyElements("#positions-body", addFilters);

    function addFilters() {
        var allVisibleRows = jQ("#positions-body > tr");
        var symbols = [];
        allVisibleRows.each(function (rowIndex) {
            var symbol = jQ(this).find('td:nth-child(1)');
            if (!symbols.contains(symbol)) {
                symbols.push(symbol);
            }
        });

        console.log(symbols);
    }

    jQ.fn.exists = function () {
        return this.length !== 0;
    }


text:
        <canvas id='render-canvas'></canvas>

✖

        Controls

Left mouse button	Place voxel

Shift + left mouse button	Remove voxel

Control + left mouse button	Copy voxel

Right mouse button + drag	Rotate camera

Mouse wheel	Zoom

H key	Hide/show controls

        More information

help

repo_name: wwwtyro/vixel-editor

mime_type: text/x-c


text:
# Vixel

A javascript & webgl voxel path tracer. See it live [here](https://wwwtyro.github.io/vixel-editor).

![vixel screenshot](media/screenshot-000.png)

![vixel screenshot](media/screenshot-001.png)

## Materials

- **Color** is the material's base color.

- **Roughness** describes how randomly specular light is reflected from the surface.

- **Metalness** describes how much of the light it reflects is diffusive. A purely metallic surface reflects zero light diffusively.

- **Emission** is how much light the material emits. If this value is greater than zero, only the **color** component of the material is used.

| Roughness | Metalness | Real world analogue | Rendered example                 |
| --------- | --------- | ------------------- | -------------------------------- |
| 0.0       | 0.0       | Smooth plastic      | ![thing](media/material-000.png) |
| 1.0       | 0.0       | Chalk               | ![thing](media/material-001.png) |
| 0.0       | 1.0       | Mirror              | ![thing](media/material-002.png) |
| 1.0       | 0.05      | Unpolished metal    | ![thing](media/material-003.png) |

## Ground

The **color**, **roughness**, and **metalness** properties can also be set for the ground plane, and are identical in meaning.

## Sky

- **Time** is simply the time of day on a 24-hour clock. The sun rises at 6:00 and sets at 18:00.

- **Azimuth** is the direction of the sun _around_ the up/down axis.

## Rendering

- **Width** and **Height** define the resolution of your rendered image.

- **DOF Distance** is how far into your scene the focus plane lies.

- **DOF Magnitude** is how strong the DOF effect is.

- **Samples/Frame** describes how many samples are taken per frame. `1` is one sample per pixel, per frame. If the interactivity of the editor is slow or
  choppy, you can reduce this to improve your framerate. Similarly, if you want to converge the scene faster, you can increase it (though increasing it is only
  effective until you're GPU bound).

- **Take Screenshot** will download a screenshot.

## Scene

- **Copy URL** copies the current scene to the clipboard and updates the URL. For now, this is the only way to save and share your scene. Feel free to paste it
  into a text file to save it longer term. Yes, there are absolutely plans to improve this.

- **Clear Scene** clears the scene of all voxels save one.


text:
"use strict";

const { vec3 } = require("gl-matrix");

const VoxelIndex = require("./voxel-index");

module.exports = class Stage {
  constructor(regl) {
    this.regl = regl;
    this.data = {};
    this.vIndex = new VoxelIndex();
    this.tIndex = regl.texture();
    this.tRGB = regl.texture();
    this.tRMET = regl.texture();
    this.tRi = regl.texture();
  }

  key(x, y, z) {
    return `${x} ${y} ${z}`;
  }

  set(x, y, z, red, green, blue, rough, metal, emit, transparent, ri) {
    this.data[this.key(x, y, z)] = {
      x,
      y,
      z,
      red: Math.round(red * 255),
      green: Math.round(green * 255),
      blue: Math.round(blue * 255),
      rough,
      metal,
      emit,
      transparent,
      ri
    };
  }

  unset(x, y, z) {
    if (Object.keys(this.data).length === 1) return;
    delete this.data[this.key(x, y, z)];
  }

  get(x, y, z) {
    return this.data[this.key(x, y, z)];
  }

  clear() {
    this.vIndex.clear();
    this.data = {};
  }

  updateBounds() {
    const b = {
      min: {
        x: Infinity,
        y: Infinity,
        z: Infinity
      },
      max: {
        x: -Infinity,
        y: -Infinity,
        z: -Infinity
      }
    };
    for (let [_, v] of Object.entries(this.data)) {
      b.min.x = Math.min(b.min.x, v.x);
      b.min.y = Math.min(b.min.y, v.y);
      b.min.z = Math.min(b.min.z, v.z);
      b.max.x = Math.max(b.max.x, v.x);
      b.max.y = Math.max(b.max.y, v.y);
      b.max.z = Math.max(b.max.z, v.z);
    }
    b.width = 1 + b.max.x - b.min.x;
    b.height = 1 + b.max.y - b.min.y;
    b.depth = 1 + b.max.z - b.min.z;
    this.bounds = b;
  }

  update() {
    this.updateBounds();
    let size = 1;
    while (
      size * size <
      this.bounds.width * this.bounds.height * this.bounds.depth
    ) {
      size *= 2;
    }
    const shiftX = -this.bounds.min.x;
    const shiftY = -this.bounds.min.y;
    const shiftZ = -this.bounds.min.z;
    const aIndex = new Uint8Array(size * size * 2);
    aIndex.fill(0);
    for (let [_, v] of Object.entries(this.data)) {
      const vi = this.vIndex.get(v);
      const ai =
        (shiftY + v.y) * this.bounds.width * this.bounds.depth +
        (shiftZ + v.z) * this.bounds.width +
        (shiftX + v.x);
      aIndex[ai * 2 + 0] = vi[0];
      aIndex[ai * 2 + 1] = vi[1];
    }
    this.tIndex({
      width: size,
      height: size,
      format: "luminance alpha",
      data: aIndex
    });
    this.tRGB({
      width: 256,
      height: 256,
      format: "rgb",
      data: this.vIndex.aRGB
    });
    this.tRMET({
      width: 256,
      height: 256,
      format: "rgba",
      type: "float",
      data: this.vIndex.aRMET
    });
    this.tRi({
      width: 256,
      height: 256,
      format: "rgba",
      type: "float",
      data: this.vIndex.aRi
    });
  }

  serialize() {
    const out = {
      version: 0
    };
    out.xyz = [];
    out.rgb = [];
    out.rough = [];
    out.metal = [];
    out.emit = [];
    out.transparent = [];
    out.ri = [];
    for (let [_, v] of Object.entries(this.data)) {
      out.xyz.push(v.x, v.y, v.z);
      out.rgb.push(v.red, v.green, v.blue);
      out.rough.push(+v.rough.toFixed(3));
      out.metal.push(+v.metal.toFixed(3));
      out.emit.push(+v.emit.toFixed(3));
      out.transparent.push(+v.transparent.toFixed(3));
      out.ri.push(+v.ri.toFixed(3));
    }
    return out;
  }

  deserialize(d) {
    this.clear();
    for (let i = 0; i < d.xyz.length / 3; i++) {
      this.set(
        d.xyz[i * 3 + 0],
        d.xyz[i * 3 + 1],
        d.xyz[i * 3 + 2],
        d.rgb[i * 3 + 0] / 255,
        d.rgb[i * 3 + 1] / 255,
        d.rgb[i * 3 + 2] / 255,
        d.rough[i],
        d.metal[i],
        d.emit[i],
        d.transparent[i],
        d.ri[i]
      );
    }
  }

  rayAABB(r0, r, v) {
    const bMin = v.slice();
    const bMax = vec3.add([], v, [1, 1, 1]);
    const invr = r.map(e => 1 / e);
    const omax = vec3.mul([], vec3.sub([], bMin, r0), invr);
    const omin = vec3.mul([], vec3.sub([], bMax, r0), invr);
    const imax = vec3.max([], omax, omin);
    const imin = vec3.min([], omax, omin);
    const t1 = Math.min(imax[0], Math.min(imax[1], imax[2]));
    const t0 = Math.max(0, Math.max(imin[0], Math.max(imin[1], imin[2])));
    if (t1 > t0) {
      return t0;
    }
    return false;
  }

  intersect(r0, r) {
    const v = r0.map(Math.floor);
    const stp = r.map(Math.sign);
    const tDelta = r.map(e => 1.0 / Math.abs(e));
    const tMax = [
      r[0] < 0 ? r0[0] - Math.floor(r0[0]) : Math.ceil(r0[0]) - r0[0],
      r[1] < 0 ? r0[1] - Math.floor(r0[1]) : Math.ceil(r0[1]) - r0[1],
      r[2] < 0 ? r0[2] - Math.floor(r0[2]) : Math.ceil(r0[2]) - r0[2]
    ];
    tMax[0] /= Math.abs(r[0]);
    tMax[1] /= Math.abs(r[1]);
    tMax[2] /= Math.abs(r[2]);
    for (let i = 0; i < 8192; i++) {
      if (tMax[0] < tMax[1]) {
        if (tMax[0] < tMax[2]) {
          v[0] += stp[0];
          tMax[0] += tDelta[0];
        } else {
          v[2] += stp[2];
          tMax[2] += tDelta[2];
        }
      } else {
        if (tMax[1] <= tMax[2]) {
          v[1] += stp[1];
          tMax[1] += tDelta[1];
        } else {
          v[2] += stp[2];
          tMax[2] += tDelta[2];
        }
      }
      const gv = this.get(v[0], v[1], v[2]);
      if (gv) {
        return {
          voxel: v,
          t: this.rayAABB(r0, r, v)
        };
      }
    }
    return undefined;
  }
};


text:
"use strict";

const { mat4, vec3 } = require("gl-matrix");

module.exports = class TrackballCamera {
  constructor(domElement) {
    this._domElement = domElement;
    this._rotation = mat4.create();
    this.fov = Math.PI / 6;
    this.center = [0, 0, 0];
    this.radius = 10.0;
    this.near = 0.1;
    this.far = 1000;
  }

  rotate(dx, dy) {
    mat4.rotateY(this._rotation, this._rotation, -dx);
    mat4.rotateX(this._rotation, this._rotation, -dy);
  }

  up() {
    const u = [0, 1, 0];
    vec3.transformMat4(u, u, this._rotation);
    return u;
  }

  right() {
    const r = [1, 0, 0];
    vec3.transformMat4(r, r, this._rotation);
    return r;
  }

  eye() {
    const e = [0, 0, this.radius];
    vec3.transformMat4(e, e, this._rotation);
    vec3.add(e, e, this.center);
    return e;
  }

  view() {
    const up = [0, 1, 0];
    vec3.transformMat4(up, up, this._rotation);
    const e = this.eye();
    return mat4.lookAt([], e, this.center, up);
  }

  projection() {
    return mat4.perspective(
      [],
      this.fov,
      this._domElement.clientWidth / this._domElement.clientHeight,
      this.near,
      this.far
    );
  }

  invpv() {
    const v = this.view();
    const p = this.projection();
    const pv = mat4.multiply([], p, v);
    return mat4.invert([], pv);
  }

  serialize() {
    return {
      version: 0,
      rotation: this._rotation,
      fov: this.fov,
      center: this.center,
      radius: this.radius,
      near: this.near,
      far: this.far
    };
  }

  deserialize(data) {
    this._rotation = data.rotation;
    this.fov = data.fov;
    this.center = data.center;
    this.radius = data.radius;
    this.near = data.near;
    this.far = data.far;
  }
};


text:
"use strict";

const { mat4, vec3, vec2 } = require("gl-matrix");
const dat = require("dat.gui");
const pick = require("camera-picking-ray");
const downloadCanvas = require("download-canvas").downloadCanvas;
const clip = require("copy-to-clipboard");
const jcb64 = require("jcb64");

const Renderer = require("./render");
const Stage = require("./stage");
const Camera = require("./camera");

const canvas = document.getElementById("render-canvas");
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

const renderer = Renderer(canvas);
const stage = new Stage(renderer.context);
const camera = new Camera(canvas);

camera.rotate(1, 0.2);

const mouse = {
  left: false,
  right: false,
  x: null,
  y: null
};

function toggleHelp() {
  const h = document.getElementById("help");
  const hb = document.getElementById("help-button");
  if (hb.style.display === "none") {
    hb.style.display = "inline";
    h.style.display = "none";
  } else {
    hb.style.display = "none";
    h.style.display = "inline";
  }
}

toggleHelp();

document.getElementById("help").addEventListener("click", toggleHelp);
document.getElementById("help-button").addEventListener("click", toggleHelp);

window.addEventListener("contextmenu", e => {
  e.preventDefault();
  return false;
});

canvas.addEventListener("mousedown", e => {
  if (e.button === 0) {
    mouse.left = true;
    mouse.x = e.clientX;
    mouse.y = e.clientY;
    const b = stage.bounds;
    camera.center = [
      b.width / 2 + b.min.x,
      b.height / 2 + b.min.y,
      b.depth / 2 + b.min.z
    ];
    camera.radius = vec3.length([b.width, b.height, b.depth]) * 1.5;
    const r = [];
    const r0 = [];
    pick(
      r0,
      r,
      [
        (canvas.width * e.offsetX) / canvas.clientWidth,
        (canvas.height * e.offsetY) / canvas.clientHeight
      ],
      [0, 0, canvas.width, canvas.height],
      camera.invpv()
    );
    const v = stage.intersect(r0, r);
    if (v === undefined && r[1] < 0 && r0[1] > b.min.y) {
      if (!e.shiftKey && !e.ctrlKey) {
        const p = vec3.add(
          [],
          r0,
          vec3.scale([], r, -(r0[1] - b.min.y) / r[1] - 0.001)
        );
        let n = p.map(Math.floor);
        stage.set(
          n[0],
          n[1],
          n[2],
          controls.color[0] / 255,
          controls.color[1] / 255,
          controls.color[2] / 255,
          controls.roughness,
          controls.metalness,
          controls.emission,
          controls.transparent,
          controls.ri
        );
        stage.update();
        renderer.reset();
      }
    } else if (v !== undefined) {
      if (e.shiftKey) {
        stage.unset(v.voxel[0], v.voxel[1], v.voxel[2]);
        stage.update();
        renderer.reset();
      } else if (e.ctrlKey) {
        const vd = stage.get(v.voxel[0], v.voxel[1], v.voxel[2]);
        controls.roughness = vd.rough;
        controls.metalness = vd.metal;
        controls.emission = vd.emit;
        controls.color = [vd.red, vd.green, vd.blue];
        controls.transparent = vd.transparent;
        controls.ri = vd.ri;
        gui.updateDisplay();
      } else {
        const p = vec3.add([], r0, vec3.scale([], r, v.t - 0.001));
        let n = p.map(Math.floor);
        stage.set(
          n[0],
          n[1],
          n[2],
          controls.color[0] / 255,
          controls.color[1] / 255,
          controls.color[2] / 255,
          controls.roughness,
          controls.metalness,
          controls.emission,
          controls.transparent,
          controls.ri
        );
        stage.update();
        renderer.reset();
      }
    }
  }
  if (e.button === 2) {
    mouse.right = true;
    mouse.x = e.clientX;
    mouse.y = e.clientY;
  }
});

window.addEventListener("mouseup", e => {
  if (e.button === 0) {
    mouse.left = false;
  }
  if (e.button === 2) {
    mouse.right = false;
  }
});

window.addEventListener("mousemove", e => {
  if (!mouse.right) return;
  const dx = e.clientX - mouse.x;
  const dy = e.clientY - mouse.y;
  mouse.x = e.clientX;
  mouse.y = e.clientY;
  camera.rotate(dx * 0.003, dy * 0.003);
  renderer.reset();
});

window.addEventListener("wheel", e => {
  camera.fov *= 1 + Math.sign(e.deltaY) * 0.1;
  camera.fov = Math.max(Math.PI / 32, Math.min(Math.PI / 1.1, camera.fov));
  renderer.reset();
});

const controls = new function() {
  this.color = [255, 255, 255];
  this.roughness = 0.0;
  this.metalness = 0.0;
  this.emission = 0.0;
  this.transparent = 0.0;
  this.ri = 1.0;
  this.groundColor = [80, 80, 80];
  this.groundRoughness = 1;
  this.groundMetalness = 0.0;
  this.time = 6.1;
  this.azimuth = 0.0;
  this.lightRadius = 8.0;
  this.lightIntensity = 1.0;
  this.width = 1280;
  this.height = 720;
  this.dofDist = 0.5;
  this.dofMag = 0.0;
  this.autoSample = true;
  this.samplesPerFrame = 1;
  this.screenshot = function() {
    downloadCanvas("render-canvas", {
      name: "voxel",
      type: "png",
      quality: 1
    });
  };
  this.save = function() {
    const hash = `#${pack()}`;
    clip(location.href + hash);
    location.hash = hash;
  };
  this.clear = function() {
    stage.clear();
    stage.set(0, 0, 0, 0.5, 0.5, 0.5, 0, 0, 0, 0, 1);
    stage.update();
    renderer.reset();
  };
}();

const gui = new dat.GUI();

gui.fMaterial = gui.addFolder("Material");
gui.fGround = gui.addFolder("Ground");
gui.fSky = gui.addFolder("Sky");
gui.fRender = gui.addFolder("Rendering");
gui.fScene = gui.addFolder("Scene");

gui.fMaterial.open();
gui.fSky.open();
gui.fGround.open();
gui.fRender.open();
gui.fScene.open();

gui.fMaterial.addColor(controls, "color").name("Color");
gui.fMaterial
  .add(controls, "roughness")
  .name("Roughness")
  .min(0.0)
  .max(1.0)
  .step(0.01);
gui.fMaterial
  .add(controls, "metalness")
  .name("Metalness")
  .min(0.0)
  .max(1.0)
  .step(0.01);
gui.fMaterial
  .add(controls, "transparent")
  .name("Transparency")
  .min(0.0)
  .max(1.0)
  .step(0.01);
gui.fMaterial
  .add(controls, "ri")
  .name("Refractive Index")
  .min(1.0)
  .max(3.0)
  .step(0.01);
gui.fMaterial
  .add(controls, "emission")
  .name("Emission")
  .min(0.0)
  .step(0.1);

gui.fGround
  .addColor(controls, "groundColor")
  .name("Color")
  .onChange(renderer.reset);
gui.fGround
  .add(controls, "groundRoughness")
  .name("Roughness")
  .min(0.0)
  .max(1.0)
  .step(0.01)
  .onChange(renderer.reset);
gui.fGround
  .add(controls, "groundMetalness")
  .name("Metalness")
  .min(0.0)
  .max(1.0)
  .step(0.01)
  .onChange(renderer.reset);

gui.fSky
  .add(controls, "time")
  .name("Time")
  .min(0.0)
  .max(24.0)
  .step(0.01)
  .onChange(function() {
    renderer.reset();
  });
gui.fSky
  .add(controls, "azimuth")
  .name("Azimuth")
  .min(0.0)
  .max(2 * Math.PI)
  .step(0.01)
  .onChange(function() {
    renderer.reset();
  });
gui.fSky
  .add(controls, "lightRadius")
  .name("Sun Radius")
  .min(0.0)
  .onChange(function() {
    renderer.reset();
  });
gui.fSky
  .add(controls, "lightIntensity")
  .name("Sun Intensity")
  .min(0.0)
  .onChange(function() {
    renderer.reset();
  });

gui.fRender
  .add(controls, "width")
  .name("Width")
  .min(1.0)
  .step(1)
  .onFinishChange(reflow);
gui.fRender
  .add(controls, "height")
  .name("Height")
  .min(1.0)
  .step(1)
  .onFinishChange(reflow);
gui.fRender
  .add(controls, "dofDist")
  .name("DOF Distance")
  .min(0.0)
  .max(1.0)
  .step(0.001)
  .onChange(renderer.reset);
gui.fRender
  .add(controls, "dofMag")
  .name("DOF Magnitude")
  .min(0.0)
  .step(0.01)
  .onChange(renderer.reset);
gui.fRender.add(controls, "autoSample");
gui.fRender
  .add(controls, "samplesPerFrame")
  .name("Samples/Frame")
  .min(1)
  .listen();
gui.fRender.add(controls, "screenshot").name("Take Screenshot");

gui.fScene.add(controls, "save").name("Copy URL");
gui.fScene.add(controls, "clear").name("Clear Scene");

const dg = document.getElementsByClassName("dg");
Array.prototype.forEach.call(dg, function(el, i) {
  el.style.userSelect = "none";
  el.style.webkitUserSelect = "none";
  el.style.webkitTouchCallout = "none";
  el.style.msUserSelect = "none";
  el.style.mozUserSelect = "none";
  el.style.oUserSelect = "none";
});

function reflow() {
  if (canvas.width !== controls.width || canvas.height !== controls.height) {
    canvas.width = controls.width;
    canvas.height = controls.height;
    renderer.reset();
  }
  const aspect0 = canvas.width / canvas.height;
  const aspect1 = window.innerWidth / window.innerHeight;
  if (aspect0 > aspect1) {
    canvas.style.width = `${window.innerWidth}px`;
    canvas.style.height = `${Math.floor(window.innerWidth / aspect0)}px`;
  } else {
    canvas.style.height = `${window.innerHeight}px`;
    canvas.style.width = `${Math.floor(aspect0 * window.innerHeight)}px`;
  }
}

function pack() {
  const data = {
    model: stage.serialize(),
    camera: camera.serialize(),
    ground: {
      version: 0,
      color: controls.groundColor,
      roughness: controls.groundRoughness,
      metalness: controls.groundMetalness
    },
    sky: {
      version: 0,
      time: controls.time,
      azimuth: controls.azimuth,
      radius: controls.lightRadius,
      intensity: controls.lightIntensity
    },
    dof: {
      dist: controls.dofDist,
      mag: controls.dofMag
    }
  };
  return jcb64.pack(data);
}

function unpack(d) {
  const data = jcb64.unpack(d);
  stage.deserialize(data.model);
  camera.deserialize(data.camera);
  controls.time = data.sky.time;
  controls.azimuth = data.sky.azimuth;
  controls.lightRadius = data.sky.radius;
  controls.lightIntensity = data.sky.intensity;
  controls.groundColor = data.ground.color;
  controls.groundRoughness = data.ground.roughness;
  controls.groundMetalness = data.ground.metalness;
  controls.dofDist = data.dof.dist;
  controls.dofMag = data.dof.mag;
  gui.updateDisplay();
}

reflow();

window.addEventListener("resize", reflow);

if (location.hash) {
  unpack(location.hash.slice(1));
} else {
  let x = 0;
  let y = 0;
  let z = 0;
  stage.set(x, y, z, 1, 1, 1, 1, 0, 0, 0, 1);
  let transparent = 1;
  let ri = 1.5;
  let rgb = [1, 1, 1];
  let rough = 0.1;
  for (let i = 0; i < 200; i++) {
    const n = [[1, 0], [-1, 0], [0, 1], [0, -1]][Math.floor(Math.random() * 4)];
    const x1 = x + n[0];
    const z1 = z + n[1];
    while (stage.get(x1, y, z1)) y++;
    x = x1;
    z = z1;
    let emit = Math.random() < 0.1 && !transparent ? 2 : 0;
    if (Math.random() < 0.1) {
      if (transparent) {
        transparent = 0;
        ri = 1;
        rgb = [1, 1, 1];
        rough = 1;
      } else {
        emit = 0;
        transparent = 1.0;
        ri = Math.random() + 1;
        rough = Math.random() * 0.1;
        rgb = [Math.random(), Math.random(), Math.random()];
      }
    }
    stage.set(x, y, z, ...rgb, 1, 0, emit, transparent, ri);
    while (stage.get(x, y - 1, z) === undefined && y > 0) {
      y--;
      stage.set(x, y, z, ...rgb, 1, 0, 0, 0, 1);
    }
  }
}

renderer.reset();

stage.update();

let tLast = 0;

function loop() {
  if (controls.autoSample) {
    const dt = performance.now() - tLast;
    tLast = performance.now();

    if (dt > 1000 / 30) {
      controls.samplesPerFrame = Math.max(1, controls.samplesPerFrame - 1);
    } else if (dt < 1000 / 60) {
      if (renderer.sampleCount() > 1) controls.samplesPerFrame++;
    }
    // gui.updateDisplay();
  }
  const b = stage.bounds;
  camera.center = vec3.scale([], [b.width, b.height, b.depth], 0.5);
  camera.radius = vec3.length([b.width, b.height, b.depth]) * 1.5;
  renderer.sample(stage, camera, controls);
  renderer.display();

  document.getElementById(
    "stats"
  ).innerText = `${renderer.sampleCount().toFixed(2)} samples`;

  requestAnimationFrame(loop);
}

loop();


text:
"use strict";

module.exports = function PingPong(regl, opts) {
  const fbos = [regl.framebuffer(opts), regl.framebuffer(opts)];

  let index = 0;

  function ping() {
    return fbos[index];
  }

  function pong() {
    return fbos[1 - index];
  }

  function swap() {
    index = 1 - index;
  }

  function resize(width, height) {
    opts.width = width;
    opts.height = height;
    ping()(opts);
    pong()(opts);
  }

  return {
    ping: ping,
    pong: pong,
    swap: swap,
    resize: resize
  };
};


text:
"use strict";

module.exports = class VoxelIndex {
  constructor() {
    this.aRGB = new Uint8Array(256 * 256 * 3);
    this.aRMET = new Float32Array(256 * 256 * 4);
    this.aRi = new Float32Array(256 * 256 * 4);
    this.clear();
  }

  clear() {
    this.aRGB.fill(0);
    this.aRMET.fill(0);
    this.aRi.fill(0);
    this.x = 1;
    this.y = 0;
    this.keys = {};
  }

  get(v) {
    const h = `${v.red} ${v.green} ${v.blue} ${v.rough} ${v.metal} ${v.emit} ${
      v.transparent
    } ${v.ri}`;
    if (this.keys[h] === undefined) {
      // It's cool that we're skipping the first two indices, because those will be a shortcut for air and ground.
      this.x++;
      if (this.x > 255) {
        this.x = 0;
        this.y++;
        if (this.y > 255) {
          throw new Error("Exceeded voxel type limit of 65536");
        }
      }
      this.keys[h] = [this.x, this.y];
      const i = this.y * 256 + this.x;
      this.aRGB[i * 3 + 0] = v.red;
      this.aRGB[i * 3 + 1] = v.green;
      this.aRGB[i * 3 + 2] = v.blue;
      this.aRMET[i * 4 + 0] = v.rough;
      this.aRMET[i * 4 + 1] = v.metal;
      this.aRMET[i * 4 + 2] = v.emit;
      this.aRMET[i * 4 + 3] = v.transparent;
      this.aRi[i * 4 + 0] = v.ri;
    }
    return this.keys[h];
  }
};


text:
"use strict";

const { mat4, vec3, vec2 } = require("gl-matrix");
const glsl = require("glslify");
const createAtmosphereRenderer = require("regl-atmosphere-envmap");
const PingPong = require("./pingpong");

module.exports = function Renderer(canvas) {
  const regl = require("regl")({
    canvas: canvas,
    extensions: ["OES_texture_float"],
    attributes: {
      antialias: false,
      preserveDrawingBuffer: true
    }
  });

  const sunDistance = 149600000000;
  let sunPosition = vec3.scale(
    [],
    vec3.normalize([], [1.11, -0.0, 0.25]),
    sunDistance
  );

  const renderAtmosphere = createAtmosphereRenderer(regl);
  const skyMap = renderAtmosphere({
    sunDirection: vec3.normalize([], sunPosition),
    resolution: 1024
  });

  const pingpong = PingPong(regl, {
    width: canvas.width,
    height: canvas.height,
    colorType: "float"
  });

  const fboPreview = regl.framebuffer({
    width: canvas.width,
    height: canvas.height,
    colorType: "float"
  });

  const ndcBox = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];

  const tRandSize = 1024;

  const t2Sphere = (function() {
    const data = new Float32Array(tRandSize * tRandSize * 3);
    for (let i = 0; i < tRandSize * tRandSize; i++) {
      const r = vec3.random([]);
      data[i * 3 + 0] = r[0];
      data[i * 3 + 1] = r[1];
      data[i * 3 + 2] = r[2];
    }
    return regl.texture({
      width: tRandSize,
      height: tRandSize,
      format: "rgb",
      type: "float",
      data: data,
      wrap: "repeat"
    });
  })();

  const t3Sphere = (function() {
    const data = new Float32Array(tRandSize * tRandSize * 3);
    for (let i = 0; i < tRandSize * tRandSize; i++) {
      const r = vec3.random([], Math.random());
      data[i * 3 + 0] = r[0];
      data[i * 3 + 1] = r[1];
      data[i * 3 + 2] = r[2];
    }
    return regl.texture({
      width: tRandSize,
      height: tRandSize,
      format: "rgb",
      type: "float",
      data: data,
      wrap: "repeat"
    });
  })();

  const tUniform2 = (function() {
    const data = new Float32Array(tRandSize * tRandSize * 2);
    for (let i = 0; i < tRandSize * tRandSize; i++) {
      data[i * 2 + 0] = Math.random();
      data[i * 2 + 1] = Math.random();
    }
    return regl.texture({
      width: tRandSize,
      height: tRandSize,
      format: "luminance alpha",
      type: "float",
      data: data,
      wrap: "repeat"
    });
  })();

  const tUniform1 = (function() {
    const data = new Float32Array(tRandSize * tRandSize * 1);
    for (let i = 0; i < tRandSize * tRandSize; i++) {
      data[i] = Math.random();
    }
    return regl.texture({
      width: tRandSize,
      height: tRandSize,
      format: "luminance",
      type: "float",
      data: data,
      wrap: "repeat"
    });
  })();

  const cmdSample = regl({
    vert: glsl.file("./glsl/sample.vert"),
    frag: glsl.file("./glsl/sample.frag"),
    attributes: {
      position: ndcBox
    },
    uniforms: {
      source: regl.prop("source"),
      invpv: regl.prop("invpv"),
      eye: regl.prop("eye"),
      res: regl.prop("res"),
      resFrag: regl.prop("resFrag"),
      tSky: skyMap,
      tUniform1: tUniform1,
      tUniform2: tUniform2,
      t2Sphere: t2Sphere,
      t3Sphere: t3Sphere,
      tOffset: regl.prop("tOffset"),
      tRGB: regl.prop("tRGB"),
      tRMET: regl.prop("tRMET"),
      tRi: regl.prop("tRi"),
      tIndex: regl.prop("tIndex"),
      dofDist: regl.prop("dofDist"),
      dofMag: regl.prop("dofMag"),
      resStage: regl.prop("resStage"),
      invResRand: [1 / tRandSize, 1 / tRandSize],
      lightPosition: regl.prop("lightPosition"),
      lightIntensity: regl.prop("lightIntensity"),
      lightRadius: regl.prop("lightRadius"),
      groundColor: regl.prop("groundColor"),
      groundRoughness: regl.prop("groundRoughness"),
      groundMetalness: regl.prop("groundMetalness"),
      bounds: regl.prop("bounds"),
      renderPreview: regl.prop("renderPreview")
    },
    depth: {
      enable: false,
      mask: false
    },
    viewport: regl.prop("viewport"),
    framebuffer: regl.prop("destination"),
    count: 6
  });

  const cmdDisplay = regl({
    vert: glsl.file("./glsl/display.vert"),
    frag: glsl.file("./glsl/display.frag"),
    attributes: {
      position: ndcBox
    },
    uniforms: {
      source: regl.prop("source"),
      preview: regl.prop("preview"),
      fraction: regl.prop("fraction"),
      tUniform1: tUniform1,
      tUniform1Res: [tUniform1.width, tUniform1.height]
    },
    depth: {
      enable: false,
      mask: false
    },
    viewport: regl.prop("viewport"),
    count: 6
  });

  function calculateSunPosition(time, azimuth) {
    const theta = (2 * Math.PI * (time - 6)) / 24;
    return [
      sunDistance * Math.cos(azimuth) * Math.cos(theta),
      sunDistance * Math.sin(theta),
      sunDistance * Math.sin(azimuth) * Math.cos(theta)
    ];
  }

  let sampleCount = 0;

  function sample(stage, camera, controls) {
    const sp = calculateSunPosition(controls.time, controls.azimuth);
    if (vec3.distance(sp, sunPosition) > 0.001) {
      sunPosition = sp;
      renderAtmosphere({
        sunDirection: vec3.normalize([], sunPosition),
        cubeFBO: skyMap
      });
    }
    const b = stage.bounds;
    for (let i = 0; i < controls.samplesPerFrame; i++) {
      cmdSample({
        eye: camera.eye(),
        invpv: camera.invpv(),
        res: [canvas.width, canvas.height],
        tOffset: [Math.random(), Math.random()],
        tRGB: stage.tRGB,
        tRMET: stage.tRMET,
        tRi: stage.tRi,
        tIndex: stage.tIndex,
        resStage: stage.tIndex.width,
        bounds: [b.width, b.height, b.depth],
        lightPosition: sunPosition,
        lightIntensity: controls.lightIntensity,
        lightRadius: 695508000 * controls.lightRadius,
        groundRoughness: controls.groundRoughness,
        groundColor: controls.groundColor.map(c => c / 255),
        groundMetalness: controls.groundMetalness,
        dofDist: controls.dofDist,
        dofMag: controls.dofMag,
        renderPreview: sampleCount === 0,
        source: pingpong.ping(),
        destination: sampleCount === 0 ? fboPreview : pingpong.pong(),
        viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height }
      });
      if (sampleCount > 0) {
        pingpong.swap();
      }
      sampleCount++;
      if (sampleCount === 1) break;
    }
  }

  function display() {
    cmdDisplay({
      source: pingpong.ping(),
      preview: fboPreview,
      fraction: Math.min(1.0, sampleCount / 128),
      viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height }
    });
  }

  function reset() {
    if (
      pingpong.ping().width !== canvas.width ||
      pingpong.ping().height !== canvas.height
    ) {
      pingpong.ping()({
        width: canvas.width,
        height: canvas.height,
        colorType: "float"
      });
      pingpong.pong()({
        width: canvas.width,
        height: canvas.height,
        colorType: "float"
      });
      fboPreview({
        width: canvas.width,
        height: canvas.height,
        colorType: "float"
      });
    }
    regl.clear({ color: [0, 0, 0, 0], framebuffer: pingpong.ping() });
    regl.clear({ color: [0, 0, 0, 0], framebuffer: pingpong.pong() });
    regl.clear({ color: [0, 0, 0, 0], framebuffer: fboPreview });
    sampleCount = 0;
  }

  return {
    context: regl,
    sample: sample,
    display: display,
    reset: reset,
    sampleCount: function() {
      return sampleCount;
    }
  };
};


text:
precision highp float;

uniform sampler2D tUniform2;
uniform vec2 resFrag, resTarget, randOffset;
uniform float resRand;

void main() {
  vec2 p0 = floor(gl_FragCoord.xy)/resFrag;
  vec2 r = texture2D(tUniform2, randOffset + gl_FragCoord.xy/resRand).ra;
  vec2 pr = p0 + r/resFrag;
  vec2 fc = floor(pr * resTarget);
  gl_FragColor = vec4(fc + 0.5, 1, 1);
}


text:
precision highp float;

attribute vec2 position;

void main() {
  gl_Position = vec4(position, 0, 1);
}


text:
precision highp float;

uniform sampler2D source, preview, tUniform1;
uniform vec2 tUniform1Res;
uniform float fraction;

varying vec2 vPos;

void main() {
  vec4 src = texture2D(source, vPos);
  vec4 prv = texture2D(preview, vPos);
  vec3 color = mix(prv.rgb, src.rgb/max(src.a, 1.0), fraction);
  color = pow(color, vec3(1.0/2.2));
  float r = texture2D(tUniform1, gl_FragCoord.xy/tUniform1Res).r;
  color += mix(-0.5/255.0, 0.5/255.0, r);
  gl_FragColor = vec4(color, 1);
}


text:
precision highp float;
uniform sampler2D tRGB, tRMET, tRi, tIndex, t2Sphere, t3Sphere, tUniform2, tUniform1, source;
uniform samplerCube tSky;
uniform mat4 invpv;
uniform vec3 eye, bounds, lightPosition, groundColor;
uniform vec2 res, tOffset, invResRand;
uniform float resStage, lightRadius, groundRoughness, groundMetalness, dofDist, dofMag, lightIntensity;
uniform bool renderPreview;

const float epsilon = 0.0001;
const int nBounces = 5;

float randUniform1(inout vec2 randOffset) {
  float r = texture2D(tUniform1, randOffset + tOffset).r;
  randOffset += r;
  return r;
}

vec2 randUniform2(inout vec2 randOffset) {
  vec2 r = texture2D(tUniform2, randOffset + tOffset).ra;
  randOffset += r;
  return r;
}

vec3 rand2Sphere(inout vec2 randOffset) {
  vec3 r = texture2D(t2Sphere, randOffset + tOffset).xyz;
  randOffset += r.xy;
  return r;
}

vec3 rand3Sphere(inout vec2 randOffset) {
  vec3 r = texture2D(t3Sphere, randOffset + tOffset).xyz;
  randOffset += r.xy;
  return r;
}

bool inBounds(vec3 p) {
  return all(greaterThanEqual(p, vec3(0.0))) && all(lessThan(p, bounds));
}

bool rayAABB(vec3 origin, vec3 direction, vec3 bMin, vec3 bMax, out float t0) {
  vec3 invDir = 1.0 / direction;
  vec3 omin = (bMin - origin) * invDir;
  vec3 omax = (bMax - origin) * invDir;
  vec3 imax = max(omax, omin);
  vec3 imin = min(omax, omin);
  float t1 = min(imax.x, min(imax.y, imax.z));
  t0 = max(imin.x, max(imin.y, imin.z));
  t0 = max(t0, 0.0);
  return t1 > t0;
}

vec3 rayAABBNorm(vec3 p, vec3 v) {
  vec3 d = p - (v + 0.5);
  vec3 dabs = abs(d);
  if (dabs.x > dabs.y) {
    if (dabs.x > dabs.z) {
      return vec3(sign(d.x), 0.0, 0.0);
    } else {
      return vec3(0, 0, sign(d.z));
    }
  } else {
    if (dabs.y > dabs.z) {
      return vec3(0.0, sign(d.y), 0.0);
    } else {
      return vec3(0.0, 0.0, sign(d.z));
    }
  }
}

vec2 samplePoint(vec3 v) {
  float invResStage = 1.0 / resStage;
  float i = v.y * bounds.x * bounds.z + v.z * bounds.x + v.x;
  i = i * invResStage;
  float y = floor(i);
  float x = fract(i) * resStage;
  x = (x + 0.5) * invResStage;
  y = (y + 0.5) * invResStage;
  return vec2(x, y);
}

struct VoxelData {
  vec3 xyz;
  vec3 rgb;
  vec2 index;
  float roughness;
  float metalness;
  float emission;
  float transparent;
  float ri;
};

VoxelData floorData(vec3 v) {
  return VoxelData(v, groundColor, vec2(1.0/255.0, 0.0), groundRoughness, groundMetalness, 0.0, 0.0, 1.0);
}

VoxelData airData(vec3 v) {
  return VoxelData(v, vec3(1.0), vec2(0.0), 0.0, 0.0, 0.0, 1.0, 1.0);
}

VoxelData voxelData(vec3 v) {
  VoxelData vd;
  vd.xyz = v;
  if (v.y == -1.0) {
    return floorData(v);
  }
  if (!inBounds(v)) {
    return airData(v);
  }
  vec2 s = samplePoint(v);
  vd.index = texture2D(tIndex, s).ra;
  if (vd.index == vec2(0.0)) return airData(v);
  vd.rgb = texture2D(tRGB, vd.index).rgb;
  vec4 rmet = texture2D(tRMET, vd.index);
  vd.roughness = rmet.r;
  vd.metalness = rmet.g;
  vd.emission = rmet.b;
  vd.transparent = rmet.a;
  vd.ri = texture2D(tRi, vd.index).r;
  return vd;
}

VoxelData intersectFloor(vec3 r0, vec3 r) {
  // NOTE: Assumes this ray actually hits the floor.
  vec3 v = floor(r0 + r * -r0.y/r.y);
  v.y = -1.0;
  return floorData(v);
}

float raySphereIntersect(vec3 r0, vec3 rd, vec3 s0, float sr) {
    float a = dot(rd, rd);
    vec3 s0_r0 = r0 - s0;
    float b = 2.0 * dot(rd, s0_r0);
    float c = dot(s0_r0, s0_r0) - (sr * sr);
    if (b*b - 4.0*a*c < 0.0) {
        return -1.0;
    }
    return (-b - sqrt((b*b) - 4.0*a*c))/(2.0*a);
}

vec3 skyColor(vec3 r0, vec3 r, float sunScale) {
  if (r.y < 0.0) {
    return vec3(0.0);
  }
  vec3 sky = textureCube(tSky, r).rgb;
  if (raySphereIntersect(r0, r, lightPosition, lightRadius) > 0.0) {
    sky += vec3(lightIntensity) * sunScale;
  }
  return sky;
}

bool intersect(vec3 r0, vec3 r, inout VoxelData vd) {
  float tBounds = 0.0;
  vec3 v = vec3(0.0);
  if (!inBounds(r0)) {
    if (!rayAABB(r0, r, vec3(0.0), bounds, tBounds)) {
      if (r.y >= 0.0) {
        return false;
      }
      vd = intersectFloor(r0, r);
      return true;
    }
    r0 = r0 + r * tBounds + r * epsilon;
  }
  v = floor(r0);
  vec3 stp = sign(r);
  vec3 tDelta = 1.0 / abs(r);
  vec3 tMax = step(0.0, r) * (1.0 - fract(r0)) + (1.0 - step(0.0, r)) * fract(r0);
  tMax = tMax/abs(r);
  for (int i = 0; i < 8192; i++) {
    if (!inBounds(v)) {
      if (r.y >= 0.0) {
        return false;
      }
      vd = intersectFloor(r0, r);
      return true;
    }
    vec2 lastIndex = vd.index;
    vd = voxelData(v);
    if (lastIndex != vd.index) {
      return true;
    }
    vec3 s = vec3(
      step(tMax.x, tMax.y) * step(tMax.x, tMax.z),
      step(tMax.y, tMax.x) * step(tMax.y, tMax.z),
      step(tMax.z, tMax.x) * step(tMax.z, tMax.y)
    );
    v += s * stp;
    tMax += s * tDelta;
  }
  return false;
}

vec3 preview() {
  vec4 ndc = vec4(
    2.0 * gl_FragCoord.xy / res - 1.0,
    2.0 * gl_FragCoord.z - 1.0,
    1.0
  );
  vec4 clip = invpv * ndc;
  vec3 p3d = clip.xyz / clip.w;
  vec3 ray = normalize(p3d - eye);
  vec3 r0 = eye;

  VoxelData vd = airData(floor(r0));

  if (intersect(r0, ray, vd)) {
    if (vd.emission > 0.0) {
      return vd.rgb;
    }
    float tVoxel = 0.0;
    rayAABB(r0, ray, vd.xyz, vd.xyz + 1.0, tVoxel);
    vec3 r1 = r0 + tVoxel * ray;
    vec3 n = rayAABBNorm(r1, vd.xyz);
    vec3 rLight = normalize(lightPosition - r1);
    vec3 color = vd.rgb * (skyColor(r1, n, 0.0) + 0.25);
    r1 -= ray * epsilon;
    vd = voxelData(floor(r1));
    if (intersect(r1, rLight, vd)) {
      if (vd.xyz.y != -1.0) {
        color *= 0.5;
      }
    }
    return color;
  }
  return skyColor(r0, ray, 1.0);
}

void main() {

  vec4 src = texture2D(source, gl_FragCoord.xy/res);

  if (renderPreview) {
    gl_FragColor = vec4(preview(), 1) + src;
    return;
  }

  vec2 randOffset = vec2(0.0);

  // Recover NDC
  vec2 jitter = randUniform2(randOffset) - 0.5;
  vec4 ndc = vec4(
    2.0 * (gl_FragCoord.xy + jitter) / res - 1.0,
    2.0 * gl_FragCoord.z - 1.0,
    1.0
  );

  // Calculate clip
  vec4 clip = invpv * ndc;

  // Calculate 3D position
  vec3 p3d = clip.xyz / clip.w;

  vec3 ray = normalize(p3d - eye);
  vec3 r0 = eye;

  float ddof = dofDist * length(bounds) + length(0.5 * bounds - eye) - length(bounds) * 0.5;
  vec3 tdof = r0 + ddof * ray;
  r0 += rand2Sphere(randOffset) * dofMag;
  ray = normalize(tdof - r0);

  vec3 mask = vec3(1.0);
  vec3 accm = vec3(0.0);

  VoxelData vd = airData(floor(r0));

  bool reflected = false;
  for (int b = 0; b < nBounces; b++) {
    bool refracted = false;
    float lastRi = vd.ri;
    if (intersect(r0, ray, vd)) {
      if (vd.emission > 0.0) {
        accm += mask * vd.emission * vd.rgb;
        break;
      }
      float tVoxel = 0.0;
      rayAABB(r0, ray, vd.xyz, vd.xyz + 1.0, tVoxel);
      vec3 r1 = r0 + tVoxel * ray;
      vec3 n = rayAABBNorm(r1, vd.xyz);
      vec3 m = normalize(n + rand3Sphere(randOffset) * vd.roughness);
      vec3 diffuse = normalize(m + rand2Sphere(randOffset));
      vec3 ref = reflect(ray, m);
      if (randUniform1(randOffset) <= vd.metalness) {
        // metallic
        ray = ref;
        reflected = true;
        mask *= vd.rgb;
      } else {
        // nonmetallic
        const float F0 = 0.0;
        float F = F0 + (1.0 - F0) * pow(1.0 - dot(-ray, n), 5.0);
        if (randUniform1(randOffset) <= F) {
          // reflect
          ray = ref;
          reflected = true;
        } else {
          // diffuse
          mask *= vd.rgb;
          if (randUniform1(randOffset) <= vd.transparent) {
            // attempt refraction
            ray = refract(ray, m, lastRi/vd.ri);
            if (ray != vec3(0.0)) {
              // refracted
              ray = normalize(ray);
              refracted = true;
              reflected = false;
            } else {
              // total internal refraction, use reflection.
              ray = ref;
              refracted = false;
              reflected = true;
            }
          } else {
            // diffuse reflection
            ray = diffuse;
            reflected = false;
          }
        }
      }
      if (!refracted && dot(ray, n) < 0.0) {
        accm = vec3(0.0);
        break;
      }
      r0 = r1 + ray * epsilon;
      vd = voxelData(floor(r0));
      if (ray == diffuse) {
        // Perform next event estimation when a diffuse bounce occurs.
        vec3 pLight = lightPosition + rand2Sphere(randOffset) * lightRadius;
        vec3 rLight = normalize(pLight - r0);
        VoxelData _vd;
        if (!intersect(r0, rLight, _vd)) {
          accm += mask * skyColor(r0, rLight, 0.5) * clamp(dot(rLight, m), 0.0, 1.0);
        }
      }
    } else {
      accm += mask * skyColor(r0, ray, b == 0 ? 1.0 : 0.0).rgb;
      break;
    }
  }

  gl_FragColor = vec4(accm, 1) + src;
}


text:
precision highp float;

attribute vec2 position;

varying vec2 vPos;

void main() {
  gl_Position = vec4(position, 0, 1);
  vPos = 0.5 * position + 0.5;
}


text:
  <title>h4xx0r</title>

Hello World

repo_language: HTML


text:
# h4xx0r
h4xx0r.org home page

This is an experimental web page to practice things which serious open source/free software developers should not do, like

- user tracking (we might do that here just to show how evil it is)
- allowing XSS to happen
- cross-domain API retrieval
- bad cookie usage
- exposing information about visitors (like browser/os details and IP numbers) to other people, in the public

In fact, almost all the web pages are using such data, store it, sell it etc.
This page is a laboratory to do experiments with such techniques and to exose the results to the public.

To sum up: evil things may happen if you visit this page. Take care!


text:
#!/usr/bin/env bash
trap rc=1 ERR; rc=0; case $1 in ################################################
################################################################################

''install)
	nix-env -i -f default.nix

;;build-nix)
	nix-build default.nix

;;format)
	nixfmt default.nix shell.nix
	cargo fmt

;;update)
	cargo upgrade
	cargo update
	niv update

;;lint)
	cd tests/ && shellcheck *.sh

;;test)
	cargo test
	./tests/run.sh
	make -C ./nix-trace test

################################################################################
;;*) cat $0; rc=1; esac; exit $rc ##############################################

repo_name: xzfc/cached-nix-shell

repo_language: Rust

file_name: Makefile


text:
{ pkgs ? import <nixpkgs> { }, nix ? pkgs.nix }:
let
  sources = import ./nix/sources.nix;
  naersk = pkgs.callPackage sources.naersk { };
  gitignoreSource = (pkgs.callPackage sources.gitignore { }).gitignoreSource;
  blake3-src = sources.BLAKE3;
in (naersk.buildPackage {
  root = gitignoreSource ./.;
  buildInputs = [ pkgs.openssl nix pkgs.ronn ];
}).overrideAttrs (attrs: {
  CNS_GIT_COMMIT = if builtins.pathExists ./.git then
    pkgs.lib.commitIdFromGitRepo ./.git
  else
    "next";
  BLAKE3_CSRC = "${blake3-src}/c";
  postBuild = "make -f nix/Makefile post-build";
  postInstall = "make -f nix/Makefile post-install";
})


text:
# cached-nix-shell
[![Build Status](https://img.shields.io/github/actions/workflow/status/xzfc/cached-nix-shell/test.yml?branch=master&logo=github)](https://github.com/xzfc/cached-nix-shell/actions/workflows/test.yml?query=branch%3Amaster)
![License](https://img.shields.io/badge/license-Unlicense%20OR%20MIT-blue)
[![Nixpkgs unstable package](https://repology.org/badge/version-for-repo/nix_unstable/cached-nix-shell.svg)](https://nixos.org/nixos/packages.html?attr=cached-nix-shell&channel=nixpkgs-unstable&query=cached-nix-shell)
[![Man page](https://img.shields.io/badge/man-cached--nix--shell%281%29-blue)](https://xzfc.github.io/cached-nix-shell/cached-nix-shell.1)

`cached-nix-shell` is a caching layer for `nix-shell` featuring instant startup time on subsequent runs.

It supports NixOS and Linux.

## Installation

Install the release version from Nixpkgs:
```sh
nix-env -iA nixpkgs.cached-nix-shell
```

Or, install the latest development version from GitHub:
```sh
nix-env -if https://github.com/xzfc/cached-nix-shell/tarball/master
```

## Usage

Just replace `nix-shell` with `cached-nix-shell` in the shebang line:

```python
#! /usr/bin/env cached-nix-shell
#! nix-shell -i python3 -p python
print("Hello, World!")
```

Alternatively, call `cached-nix-shell` directly:

```sh
$ cached-nix-shell ./hello.py
$ cached-nix-shell -p python3 --run 'python --version'
```

Or use the `--wrap` option for programs that call `nix-shell` internally.

```sh
$ cached-nix-shell --wrap stack build
```

## Performance

```
$ time ./hello.py # first run; no cache used
cached-nix-shell: updating cache
Hello, World!
./hello.py  0.33s user 0.06s system 91% cpu 0.435 total
$ time ./hello.py
Hello, World!
./hello.py  0.02s user 0.01s system 97% cpu 0.029 total
```

## Caching and cache invalidation

`cached-nix-shell` stores environment variables set up by `nix-shell` and reuses them on subsequent runs.
It [traces](./nix-trace) which files are read by `nix` during an evaluation, and performs a proper cache invalidation if any of the used files are changed.
The cache is stored in `~/.cache/cached-nix-shell/`.

The following situations are covered:

* `builtins.readFile` is used
* `builtins.readDir` is used
* `import ./file.nix` is used
* updating `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`
* updating nix channels
* updating `$NIX_PATH` environment variable

The following situations aren't handled by `cached-nix-shell` and may lead to staled cache:

* `builtins.fetchurl` or other network builtins are used (e.g. in [nixpkgs-mozilla])

[nixpkgs-mozilla]: https://github.com/mozilla/nixpkgs-mozilla

## Related

* https://discourse.nixos.org/t/speeding-up-nix-shell-shebang/4048
* There are related projects focused on using `nix-shell` for project developing:
  * [direnv](https://direnv.net/) with [use_nix](https://github.com/direnv/direnv/wiki/Nix)
  * [Cached and Persistent Nix shell with direnv integration](https://gist.github.com/mbbx6spp/731076cb8fc620b064b8e5b28fb1c796)
  * [lorri](https://github.com/target/lorri), a `nix-shell` replacement for project development
    * [lorri #167](https://github.com/target/lorri/issues/167)
  * [nix-develop](https://gitlab.com/mightybyte/nix-develop), universal build tool featuring cached `nd shell` command

* [https://github.com/rycee/home-manager/issues/447](https://github.com/rycee/home-manager/issues/447)


text:
# cached-nix-shell - instant startup time for nix-shell(1)

## SYNOPSIS

`cached-nix-shell` \[_options_]...

`cached-nix-shell` _shebang-script_ \[_args_]...

`cached-nix-shell --wrap` _cmd_ \[_args_]...

## DESCRIPTION

`cached-nix-shell` is a caching layer for `nix-shell` featuring instant startup time on subsequent runs.
The design goal is to make a fast drop-in replacement for `nix-shell`, including support of shebang scripts and non-interactive commands (i.e., `nix-shell --run ...`).

## OPTIONS

`cached-nix-shell` supports the majority of `nix-shell` options,
  see the corresponding man page for the list.

Additionally, the following new options are unique for `cached-nix-shell`:

* `--exec` _cmd_ \[_args_]... (not in shebang):
  Command and arguments to be executed.
  It is similar to `--run` except that the command is executed directly rather than as shell command.
  It should be slightly faster and more convenient to pass arguments.

* `--wrap` _cmd_ \[_args_]... (not in shebang, should be the first arg):
  Run the command substituting every invocation of `nix-shell` with `cached-nix-shell`.
  This is done by adding our symlink named `nix-shell` to the `$PATH`.

## ENVIRONMENT VARIABLES

* `IN_CACHED_NIX_SHELL`:
  Is set to `1`.

## FILES

The cache is stored in `$XDG_CACHE_HOME/cached-nix-shell`,
  defaults to `~/.cache/cached-nix-shell`.

## LIMITATIONS

* Ambient environment variables:
It is necessary to pass `--keep` _var_ even without `--pure`
  if the variable _var_ is used inside a nix expression or a hook.
Note that updating the value of _var_ would invalidate the cache.

* Relative paths:
When `--expr` or `--packages` option is given,
  the cache is evaluated inside a separate empty directory,
  preventing access to relative paths from within nix expressions.
Contrariwise, when a path to a shebang script or nix file is given,
  the cache is evaluated in the directory containing that script or file.
This allows multiple `cached-nix-shell` invocations
  from different directories to reuse the same cache entry.

* Network access:
Accessing network resources (e.g. via `builtins.fetchurl`) is not considered in cache invalidation logic.
Consequently, `tarball-ttl` option (see `nix-conf`(5)) is not respected.

* Bash variables and functions:
Only exported environment variables are preserved,
  but not global shell variables and functions set by `setup.sh`.

* Shell hooks:
Shell hooks are executed only once, during a cache evaluation.

## HOMEPAGE

<https://github.com/xzfc/cached-nix-shell>

Please report bugs and feature requests in the issue tracker.

## SEE ALSO

nix-shell(1)


text:
[ "$IN_NIX_SHELL" = impure ] && [ -n "$PS1" ] && [ -e ~/.bashrc ] && source ~/.bashrc
[ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && PS1='\n\[\033[1;32m\][cached-nix-shell:\w]\$\[\033[0m\] '


text:
use std::env::{var, var_os};
use std::path::Path;
use std::process::Command;

fn main() {
    if var_os("CNS_IN_NIX_SHELL").is_none() {
        // Release build triggered by nix-build. Use paths relative to $out.
        let out = var("out").unwrap();
        println!("cargo:rustc-env=CNS_TRACE_NIX_SO={out}/lib/trace-nix.so");
        println!("cargo:rustc-env=CNS_VAR_EMPTY={out}/var/empty");
        println!(
            "cargo:rustc-env=CNS_RCFILE={out}/share/cached-nix-shell/rcfile.sh"
        );
        println!(
            "cargo:rustc-env=CNS_WRAP_PATH={out}/libexec/cached-nix-shell"
        );

        // Use pinned nix and nix-shell binaries.
        println!(
            "cargo:rustc-env=CNS_NIX={}/",
            which::which("nix")
                .expect("command not found: nix")
                .parent()
                .unwrap()
                .as_os_str()
                .to_str()
                .unwrap()
        );
    } else {
        // Developer build triggered by `nix-shell --run 'cargo build'`.
        // Use paths relative to the build directory. Additionally, place
        // trace-nix.so and a symlink to the build directory.
        let out_dir = var("OUT_DIR").unwrap();
        let cmd = Command::new("make")
            .args([
                "-C",
                "nix-trace",
                &format!("DESTDIR={out_dir}"),
                &format!("{out_dir}/trace-nix.so"),
            ])
            .status()
            .unwrap();
        assert!(cmd.success());

        println!("cargo:rustc-env=CNS_TRACE_NIX_SO={out_dir}/trace-nix.so");
        println!("cargo:rustc-env=CNS_VAR_EMPTY=/var/empty");
        println!(
            "cargo:rustc-env=CNS_RCFILE={}/rcfile.sh",
            var("CARGO_MANIFEST_DIR").unwrap()
        );

        if Path::new(&format!("{out_dir}/wrapper")).exists() {
            std::fs::remove_dir_all(format!("{out_dir}/wrapper")).unwrap();
        }
        std::fs::create_dir_all(format!("{out_dir}/wrapper")).unwrap();
        std::os::unix::fs::symlink(
            "../../../../cached-nix-shell",
            format!("{out_dir}/wrapper/nix-shell"),
        )
        .unwrap();
        println!("cargo:rustc-env=CNS_WRAP_PATH={out_dir}/wrapper");

        // Use nix and nix-shell from $PATH at runtime.
        println!("cargo:rustc-env=CNS_NIX=");
    }
}


text:
[package]
name = "cached-nix-shell"
version = "0.1.5"
authors = ["Albert Safin <xzfcpw@gmail.com>"]
license = "Unlicense OR MIT"
edition = "2018"

[build-dependencies]
which = "4.4.0"

[dependencies]
blake3 = "1.3.3"
bytelines = "2.4.0"
itertools = "0.10.5"
nix = "0.26.2"
once_cell = "1.17.1"
serde_json = "1.0.96"
tempfile = "3.5.0"
ufcs = "0.1.0"
xdg = "2.5.0"


text: max_width = 80


text:
{ pkgs ? import <nixpkgs> { } }:
let main = import ./default.nix { inherit pkgs; };
in with pkgs;
mkShell {
  buildInputs = main.buildInputs ++ main.nativeBuildInputs ++ [
    cargo-edit
    clippy
    niv
    nixfmt
    rust-analyzer
    rustc
    rustfmt
    shellcheck
    # Required for cargo
    git
    openssh
  ];
  inherit (main) BLAKE3_CSRC;
  CNS_IN_NIX_SHELL = "1";
  RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
}


text:
name: Test
on:
  pull_request:
  push:
jobs:
  tests:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
    - uses: actions/checkout@v3
    - uses: cachix/install-nix-action@v20
      with:
        nix_path: nixpkgs=channel:nixos-unstable
    - uses: cachix/cachix-action@v12
      with:
        name: xzfc
        signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
    - name: Build and install cached-nix-shell
      run: nix-env -i -f default.nix
    - name: Test cached-nix-shell
      run: ./tests/run.sh
    - name: Test nix-trace
      run: nix-shell -p b3sum --run "nix-shell ./default.nix --run 'make -C ./nix-trace test'"


text:
name: Pages
on:
  push:
    branches:
    - master
    paths:
    - cached-nix-shell.1.md
    - Cargo.toml # Trigger the action on releases
    - .github/workflows/pages.yml
jobs:
  pages:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - run: git fetch --prune --unshallow --tags
    - uses: cachix/install-nix-action@v12
      with:
        nix_path: nixpkgs=channel:nixos-unstable
    - run: nix-env -f '<nixpkgs>' -iA ronn
    - name: Build man page
      run: |
        ronn --organization="$(git describe --tags)" --style toc -5 cached-nix-shell.1.md
        mkdir pages
        mv ./cached-nix-shell.1.html pages
    - name: Deploy
      uses: peaceiris/actions-gh-pages@v3
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./pages
        user_name: github-actions[bot]
        user_email: github-actions[bot]@users.noreply.github.com
        commit_message: Regenerate man page


text:
use crate::args::Args;
use std::os::unix::ffi::OsStrExt;

/// True if either $NIX_PATH or -I argument contain a relative path.
pub fn contains_relative_paths(args: &Args) -> bool {
    let nix_path = std::env::var_os("NIX_PATH").unwrap_or("".into());

    let include_nix_path = args.include_nix_path.iter().map(|x| x.as_bytes());

    parse_nix_path(nix_path.as_bytes())
        .into_iter()
        .chain(include_nix_path)
        .any(is_relative)
}

fn is_relative(b: &[u8]) -> bool {
    let pos = b
        .iter()
        .position(|&c| c == b'=')
        .map(|x| x + 1)
        .unwrap_or(0);
    b.get(pos) != Some(&b'/') && !is_uri(&b[pos..])
}

/// Reference: https://github.com/NixOS/nix/blob/2.3.10/src/libexpr/eval.cc#L242-L277
fn parse_nix_path(s: &[u8]) -> Vec<&[u8]> {
    let mut res = Vec::new();
    let mut p = 0;
    while p < s.len() {
        let start = p;
        let mut start2 = p;

        while p < s.len() && s[p] != b':' {
            if s[p] == b'=' {
                start2 = p + 1;
            }
            p += 1;
        }

        if p == s.len() {
            if p != start {
                res.push(&s[start..p]);
            }
            break;
        }

        if s[p] == b':' {
            if is_uri(&s[start2..]) {
                p += 1;
                while p < s.len() && s[p] != b':' {
                    p += 1;
                }
            }
            res.push(&s[start..p]);
            if p == s.len() {
                break;
            }
        }

        p += 1;
    }

    res
}

/// Reference: https://github.com/NixOS/nix/blob/2.3.10/src/libstore/download.cc#L936-L943
pub fn is_uri(s: &[u8]) -> bool {
    let prefixes = &[
        "channel:",
        "http://",
        "https://",
        "file://",
        "channel://",
        "git://",
        "s3://",
        "ssh://",
    ];
    prefixes
        .iter()
        .any(|prefix| s.starts_with(prefix.as_bytes()))
}

#[cfg(test)]
mod tests {
    use super::{is_relative, parse_nix_path};
    macro_rules! v {
        ( $($a:literal),* ) => {{
            vec![ $( Vec::<u8>::from($a as &[_])),* ]
        }}
    }
    #[test]
    fn it_works() {
        assert_eq!(parse_nix_path(b"foo:bar:baz"), v![b"foo", b"bar", b"baz"]);
        assert_eq!(
            parse_nix_path(b"foo:bar=something:baz"),
            v![b"foo", b"bar=something", b"baz"]
        );
        assert_eq!(
            parse_nix_path(b"foo:bar=https://something:baz"),
            v![b"foo", b"bar=https://something", b"baz"]
        );

        assert!(is_relative(b"foo"));
        assert!(is_relative(b"foo=bar"));
        assert!(!is_relative(b"http://foo"));
        assert!(!is_relative(b"foo=/bar"));
        assert!(!is_relative(b"foo=http://bar"));
    }
}


text:
use bytelines::ByteLinesReader;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::os::unix::ffi::OsStrExt;

/// Parse script in the same way as nix-shell does.
/// Reference: https://github.com/NixOS/nix/blob/2.3.1/src/nix-build/nix-build.cc#L113-L126
pub fn parse_script(fname: &OsStr) -> Option<Vec<OsString>> {
    let f = File::open(fname).ok()?;
    let reader = std::io::BufReader::new(&f);
    let mut lines = reader.byte_lines();

    if !lines.next()?.ok()?.starts_with(b"#!") {
        return None; // First line isn't shebang
    }

    let mut result = Vec::new();

    while let Some(line) = lines.next() {
        let line = line.unwrap();
        if let Some(m) = re_nix_shell(line) {
            let mut items = shellwords(m)
                .into_iter()
                .map(|x| OsStr::from_bytes(&x).to_os_string())
                .collect::<Vec<OsString>>();
            result.append(&mut items);
        }
    }

    Some(result)
}

/// Reference: https://github.com/NixOS/nix/blob/2.3.1/src/nix-build/nix-build.cc#L26-L68
fn shellwords(s: &[u8]) -> Vec<Vec<u8>> {
    let mut res = Vec::new();
    let mut it = 0;
    let mut begin = 0;
    let mut cur = Vec::new();
    let mut state = true;
    loop {
        if state {
            if let Some(match_len) = re_whitespaces_len(&s[it..]) {
                cur.extend_from_slice(&s[begin..it]);
                res.push(cur);
                cur = Vec::new();
                it += match_len;
                begin = it;
            }
        }
        match s.get(it) {
            Some(b'"') => {
                cur.extend_from_slice(&s[begin..it]);
                begin = it + 1;
                state = !state;
            }
            Some(b'\\') => {
                cur.extend_from_slice(&s[begin..it]);
                begin = it + 1;
                it += 1;
            }
            Some(_) => {}
            None => break,
        }
        it += 1;
    }
    cur.extend_from_slice(&s[begin..it]);
    if !cur.is_empty() {
        res.push(cur);
    }
    res
}

/// Characters that are matched by `\s` or isspace(3).
const SPACES: &[u8; 6] = &[9, 10, 11, 12, 13, 32];

/// Match C++'s `std::regex("^#!\\s*nix-shell (.*)$")` and return `\1`
fn re_nix_shell(mut line: &[u8]) -> Option<&[u8]> {
    if !line.starts_with(b"#!") {
        return None;
    }
    line = &line[b"#!".len()..];

    while line.first().map(|c| SPACES.contains(c)) == Some(true) {
        line = &line[1..];
    }

    if !line.starts_with(b"nix-shell ") {
        return None;
    }
    line = &line[b"nix-shell ".len()..];

    Some(line)
}

/// Match C++'s `std::regex("^(\\s+).*")` and return length of `\1`
fn re_whitespaces_len(line: &[u8]) -> Option<usize> {
    let mut result = 0;
    while line.get(result).map(|c| SPACES.contains(c)) == Some(true) {
        result += 1
    }
    Some(result).filter(|&x| x != 0)
}

#[cfg(test)]
mod tests {
    use super::shellwords;
    macro_rules! v {
        ( $($a:literal),* ) => {{
            vec![ $( Vec::<u8>::from($a as &[_])),* ]
        }}
    }
    #[test]
    fn it_works() {
        assert_eq!(shellwords(b"foo bar baz"), v![b"foo", b"bar", b"baz"],);

        assert_eq!(
            shellwords(br#"foo "bar baz" qoox"#),
            v![b"foo", b"bar baz", b"qoox"],
        );

        assert_eq!(shellwords(br#"foo "bar baz "#), v![b"foo", b"bar baz "],);

        assert_eq!(
            shellwords(br#"foo \"bar baz"#),
            v![b"foo", br#""bar"#, b"baz"],
        );

        assert_eq!(
            shellwords(br#"foo "bar\"baz" qoox"#),
            v![b"foo", br#"bar"baz"#, b"qoox"],
        );

        assert_eq!(
            shellwords(br#"foo bar" "baz qoox"#),
            v![b"foo", b"bar baz", b"qoox"],
        );
    }
}


text:
//! nix-shell argument parsing
//!
//! While `cached-nix-shell` passes most of its arguments to `nix-shell` as-is
//! without even looking into them, there are some arguments that should be
//! extracted and processed by `cached-nix-shell` itself.  In order to do so, we
//! still need to parse the whole command line.
//!
//! **Q:** Why not just use a library like `clap` or `docopts`?
//! **A:** We need to emulate quirks of nix-shell argument parsing in a 100%
//! compatible way, so it is appropriate to code this explicitly rather than use
//! such libraries.

use std::collections::VecDeque;
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::CommandExt;
use std::process::{exit, Command};
use ufcs::Pipe;

pub enum RunMode {
    /// no arg
    InteractiveShell,
    /// --run CMD | --command CMD
    Shell(OsString),
    /// --exec CMD ARGS...
    Exec(OsString, Vec<OsString>),
}

pub struct Args {
    /// true: -p | --packages | -E | --expr
    pub packages_or_expr: bool,
    /// true: --pure; false: --impure
    pub pure: bool,
    /// -I
    pub include_nix_path: Vec<OsString>,
    /// -i (in shebang)
    pub interpreter: OsString,
    /// --run | --command | --exec (not in shebang)
    pub run: RunMode,
    /// --keep
    pub keep: Vec<OsString>,
    /// other positional arguments (after --)
    pub rest: Vec<OsString>,
    /// other keyword arguments
    pub other_kw: Vec<OsString>,
    /// weak keyword arguments
    pub weak_kw: Vec<OsString>,
}

struct NixShellOption {
    /// true if adding or removing this option should not invalidate the cache
    is_weak: bool,
    arg_count: u8,
    names: &'static [&'static str],
}

const fn opt(
    is_weak: bool,
    arg_count: u8,
    names: &'static [&'static str],
) -> NixShellOption {
    NixShellOption {
        is_weak,
        arg_count,
        names,
    }
}

const OPTIONS_DB: &[NixShellOption] = &[
    opt(false, 1, &["--attr", "-A"]),
    opt(false, 2, &["--arg"]),
    opt(false, 2, &["--argstr"]),
    opt(true, 0, &["--fallback"]),
    opt(true, 0, &["--keep-failed", "-K"]),
    opt(true, 0, &["--keep-going", "-k"]),
    opt(true, 0, &["--no-build-hook"]),
    opt(true, 0, &["--no-build-output", "-Q"]),
    opt(true, 0, &["--quiet"]),
    opt(true, 0, &["--repair"]),
    opt(true, 0, &["--show-trace"]),
    opt(true, 0, &["--verbose", "-v"]),
    opt(true, 1, &["--cores"]),
    opt(true, 1, &["--max-jobs", "-j"]),
    opt(true, 1, &["--max-silent-time"]),
    opt(true, 1, &["--timeout"]),
    opt(true, 2, &["--option"]),
];

impl Args {
    pub fn parse(
        args: Vec<OsString>,
        in_shebang: bool,
    ) -> Result<Args, String> {
        let mut res = Args {
            packages_or_expr: false,
            pure: false,
            include_nix_path: Vec::new(),
            interpreter: OsString::from("bash"),
            run: RunMode::InteractiveShell,
            keep: Vec::new(),
            rest: Vec::new(),
            other_kw: Vec::new(),
            weak_kw: Vec::new(),
        };
        let mut it = VecDeque::<OsString>::from(args);
        while let Some(arg) = get_next_arg(&mut it) {
            let mut next = || -> Result<OsString, String> {
                it.pop_front()
                    .ok_or_else(|| {
                        format!("flag {arg:?} requires more arguments")
                    })?
                    .pipe(Ok)
            };
            if let Some(db_item) = OPTIONS_DB
                .iter()
                .find(|it| it.names.iter().any(|&x| arg == x))
            {
                let vec = if db_item.is_weak {
                    &mut res.weak_kw
                } else {
                    &mut res.other_kw
                };
                vec.push(db_item.names[0].into());
                for _ in 0..db_item.arg_count {
                    vec.push(next()?);
                }
            } else if arg == "--pure" {
                res.pure = true;
            } else if arg == "--impure" {
                res.pure = false;
            } else if arg == "-I" {
                let path = next()?;
                res.include_nix_path.push(path.clone());
                res.other_kw.push(arg);
                res.other_kw.push(path);
            } else if arg == "-p"
                || arg == "--packages"
                || arg == "-E"
                || arg == "--expr"
            {
                res.packages_or_expr = true;
                res.other_kw.push(arg);
            } else if arg == "-i" && in_shebang {
                res.interpreter = next()?;
            } else if (arg == "--run" || arg == "--command") && !in_shebang {
                res.run = RunMode::Shell(next()?);
            } else if arg == "--exec" && !in_shebang {
                res.run = RunMode::Exec(next()?, it.into());
                break;
            } else if arg == "--keep" {
                res.keep.push(next()?);
            } else if arg == "--version" {
                exit_version();
            } else if arg == "--wrap" && !in_shebang {
                return Err("--wrap should be the first argument".to_string());
            } else if arg.as_bytes().first() == Some(&b'-') {
                return Err(format!("unexpected arg {arg:?}"));
            } else {
                res.rest.push(arg.clone());
            }
        }
        Ok(res)
    }
}

fn get_next_arg(it: &mut VecDeque<OsString>) -> Option<OsString> {
    let arg = it.pop_front()?;
    let argb = arg.as_bytes();
    if argb.len() > 2 && argb[0] == b'-' && is_alpha(argb[1]) {
        // Expand short options and put them back to the deque.
        // Reference: https://github.com/NixOS/nix/blob/2.3.1/src/libutil/args.cc#L29-L42

        let split_idx = argb[1..]
            .iter()
            .position(|&b| !is_alpha(b))
            .unwrap_or(argb.len() - 1);
        // E.g. "-pj16" -> ("pj", "16")
        let (letters, rest) = argb[1..].split_at(split_idx);

        if !rest.is_empty() {
            it.push_front(OsStr::from_bytes(rest).into());
        }
        for &c in letters.iter().rev() {
            it.push_front(OsStr::from_bytes(&[b'-', c]).into());
        }

        it.pop_front()
    } else {
        Some(arg)
    }
}

fn is_alpha(b: u8) -> bool {
    b.is_ascii_lowercase() || b.is_ascii_uppercase()
}

fn exit_version() {
    println!(
        "cached-nix-shell v{}{}",
        env!("CARGO_PKG_VERSION"),
        option_env!("CNS_GIT_COMMIT")
            .map(|x| format!("-{x}"))
            .unwrap_or("".into())
    );
    if env!("CNS_NIX").is_empty() {
        println!("Using nix-shell from $PATH");
    } else {
        println!("Using {}nix-shell", env!("CNS_NIX"));
    }
    std::io::stdout().flush().unwrap();
    Command::new(concat!(env!("CNS_NIX"), "nix-shell"))
        .arg("--version")
        .exec();
    exit(1);
}

#[cfg(test)]
mod test {
    use super::*;
    /// Expand an arg using `get_next_arg`
    fn expand(arg: &str) -> Vec<String> {
        let mut it: VecDeque<OsString> = VecDeque::from(vec![arg.into()]);
        std::iter::from_fn(|| get_next_arg(&mut it))
            .map(|s| s.to_string_lossy().into())
            .collect()
    }
    #[test]
    fn test_get_next_arg() {
        assert_eq!(expand("--"), vec!["--"]);
        assert_eq!(expand("default.nix"), vec!["default.nix"]);
        assert_eq!(expand("--argstr"), vec!["--argstr"]);
        assert_eq!(expand("-pi"), vec!["-p", "-i"]);
        assert_eq!(expand("-j4"), vec!["-j", "4"]);
        assert_eq!(expand("-j16"), vec!["-j", "16"]);
        assert_eq!(expand("-pj16"), vec!["-p", "-j", "16"]);
    }
}


text:
// Why I find [path_clean] crate unsuitable: since it operates on str/String, it
// assumes that paths are valid utf8 strings.  My implementation operates on
// Path/PathBuf/OsStr thus it doesn't make any assumptions.
// [path_clean]: https://docs.rs/path-clean/0.1.0/path_clean/

use std::ffi::OsStr;
use std::path::{Path, PathBuf};

pub trait PathClean {
    fn clean(&self) -> PathBuf;
}

impl PathClean for Path {
    fn clean(&self) -> PathBuf {
        let mut res = Vec::new();
        for elem in self {
            if elem == "/" {
                res.push(elem);
            } else if elem == "." {
                // do nothing
            } else if elem == ".." {
                if res.last().is_none() || res.last() == Some(&OsStr::new(".."))
                {
                    res.push(elem);
                } else if res.last() == Some(&OsStr::new("/")) {
                    // do nothing
                } else {
                    res.pop();
                }
            } else {
                res.push(elem);
            }
        }
        if res.is_empty() {
            res.push(OsStr::new("."));
        }
        res.into_iter().collect()
    }
}

#[test]
fn path_clean_test() {
    // Taken from https://golang.org/src/path/path_test.go
    let cases = [
        // Already clean
        ("", "."),
        ("abc", "abc"),
        ("abc/def", "abc/def"),
        ("a/b/c", "a/b/c"),
        (".", "."),
        ("..", ".."),
        ("../..", "../.."),
        ("../../abc", "../../abc"),
        ("/abc", "/abc"),
        ("/", "/"),
        // Remove trailing slash
        ("abc/", "abc"),
        ("abc/def/", "abc/def"),
        ("a/b/c/", "a/b/c"),
        ("./", "."),
        ("../", ".."),
        ("../../", "../.."),
        ("/abc/", "/abc"),
        // Remove doubled slash
        ("abc//def//ghi", "abc/def/ghi"),
        ("//abc", "/abc"),
        ("///abc", "/abc"),
        ("//abc//", "/abc"),
        ("abc//", "abc"),
        // Remove . elements
        ("abc/./def", "abc/def"),
        ("/./abc/def", "/abc/def"),
        ("abc/.", "abc"),
        // Remove .. elements
        ("abc/def/ghi/../jkl", "abc/def/jkl"),
        ("abc/def/../ghi/../jkl", "abc/jkl"),
        ("abc/def/..", "abc"),
        ("abc/def/../..", "."),
        ("/abc/def/../..", "/"),
        ("abc/def/../../..", ".."),
        ("/abc/def/../../..", "/"),
        ("abc/def/../../../ghi/jkl/../../../mno", "../../mno"),
        // Combinations
        ("abc/./../def", "def"),
        ("abc//./../def", "def"),
        ("abc/../../././../def", "../../def"),
    ];

    for (path, result) in cases.iter() {
        assert_eq!(Path::new(path).clean(), Path::new(result));
        assert_eq!(Path::new(result).clean(), Path::new(result));
    }
}