<commit_before>/**
 * @author biud436
 * @help
 */

const Aria2c = require('./Aria2c');
const fs = require('fs-extra');
const path = require('path');
const DecompressZip = require('decompress-zip');
const fileGet = require("./fileGet");
const async = require('async');
const { promisify } = require('util');
const readdirAsync = promisify(fs.readdir);
const Color = require("./ConsoleColor");

let config = {
    referrer : `https://dl.nwjs.io/`,
    userAgent : `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36`,
    inputFile : "input.txt",
    logFile : "log.txt",
};

String.prototype.splitOnLast = function (e) {
    var t = this.lastIndexOf(e) + 1;
    return 0 > t ? this : this.substr(t);
};

class Downloader {

    constructor(data) {
        this.initMembers(data);
        this.checkAria2c();
    }

    initMembers(data) {
        this._mainPath = data.mainPath;
        this._outputPath = data.outputPath;
        this._version = data.version;
        this._projectPath = data.projectPath;
        this._isForceHttps = data.forceHttps === "true";

        this._output = [];
        this._isAria2c = false;
    }

    processLine(line) {
        const filename = line.splitOnLast("/");
        this._output.push(line);
        this._output.push(`  dir=${this._outputPath}`);
        this._output.push(`  out=${filename}`);
    }

    checkAria2c() {

        const env = process.env["PATH"].split(";");
        const needed = env.filter(dir => dir.indexOf("aria") >= 0);
        needed.forEach(i => {
            if(fs.existsSync(path.join(i.replace(/\\/g, "/"), "aria2c.exe"))) {
                console.log("aria2c.exe 파일을 찾았습니다.");
                this._isAria2c = true;
            }
        });
    }

    removeFiles() {
        const files = {
            input: path.join(this._mainPath, "input.txt").replace( /\\/g, '/' ),
            log: path.join(this._mainPath, "log.txt").replace( /\\/g, '/' ),
        };

        if(fs.existsSync(files.input)) fs.removeSync(files.input);
        if(fs.existsSync(files.log)) fs.removeSync(files.log);
    }

    async *readOutputFolder() {
        try {
            const dir = await readdirAsync(this._outputPath).catch(err => {
                throw new Error(err);
            });
            for (const file of dir) {
                yield file;
            }
        } catch(e) {
            throw new Error(e);
        }
    }

    async unzip() {

        try {

            const files = await this.readOutputFolder();

            for await (const file of files) {
                const realPath = path.join(this._outputPath, file);
                const unzipper = new DecompressZip(realPath);

                if(fs.existsSync(realPath)) {
                    unzipper.on('error', (err) => console.log(err));
                    unzipper.on('extract', (log) => {
                        console.log('압축 해제를 완료하였습니다.');
                        fs.removeSync(realPath);

                        let dt = file.split(".");
                        dt.pop();
                        dt = dt.join(".");

                        if(process.platform === "win32") {
                            this.createRunFile(path.join(this._outputPath, dt, "play.bat"));
                        } else {
                            throw new Error(`${process.platform}은 아직 지원하지 않습니다. ${Color.BgRed}(맥이 없어서 테스트 불가능!)${Color.Reset}`);
                        }

                    });
                    unzipper.on('progress', function (fileIndex, fileCount) {
                        console.log('압축 해제 중 : ' + (fileIndex + 1) + ' of ' + fileCount);
                    });
                    unzipper.extract({
                        path: path.join(this._outputPath),
                        filter: function (file) {
                            return file.type !== "SymbolicLink";
                        }
                    });
                }

            }

        } catch(err) {
            console.error(err);
        }

    }

    createRunFile(filepath) {

        let contents = `
@SETLOCAL enableextensions enabledelayedexpansion
@ECHO OFF

set ARGS1=%~dp1
ECHO Current Path : %~dp0

ECHO RUN INDEX FILE : %ARGS1%index.html
${this._version > "v0.12.3" ? "%~dp0nw.exe %ARGS1% test": "start %~dp0nw.exe --url=%ARGS1%index.html?test"}

@ENDLOCAL
GOTO :EOF

:ERR
ECHO %ERRORMSG%
GOTO :EOF`;

        if(this._version >= "v0.45.4") {
            contents = `
@SETLOCAL enableextensions enabledelayedexpansion
@ECHO OFF

set ARGS1=%~dp1
ECHO Current Path : %~dp0

ECHO RUN INDEX FILE : %ARGS1%index.html
set TEMP_DIR=%cd%
cd %ARGS1%
%~dp0nw.exe . test
cd %TEMP_DIR%

@ENDLOCAL
GOTO :EOF

:ERR
ECHO %ERRORMSG%
GOTO :EOF`;
        }

        fs.writeFileSync(filepath, contents, "utf8");

        // 버전 텍스트에서 중간 버전 문자열을 획득한다.
        let v = 0;
        if(/^[Vv](\d+)\.(\d+)\.(\d+)$/i.exec(this._version)) {
            v = RegExp.$2;
        }

        // package.json 때문에 충돌 문제가 생기므로 npm run add에서 node로 변경해야 한다.
        contents = `node ..\\..\\index.js --add "play.bat" "${this._version}" "${v}" "${path.dirname(filepath)}"`

        let rootFolder = path.dirname(filepath);
        fs.writeFileSync(path.join(rootFolder, "add.bat"), contents, "utf8");

    }

    async start() {

        try  {

            const version = this._version;

            if(!version) {
                throw new Error(`버전 텍스트가 없습니다.`);
            }

            const NW_SDK = version > "v0.12.3" ? "nwjs-sdk":"nwjs";

            let osType = {
                "win32": "win",
                "linux": "linux",
                "darwin": "osx",
            };

            if(!osType[process.platform]) {
                throw new Error(`지원하지 않는 OS 입니다.`);
            }

            const needed_files = [
                `https://dl.nwjs.io/${version}/${NW_SDK}-${version}-${osType[process.platform]}-${process.arch}.${process.platform === "linux" ? "tar.gz" : "zip"}`
            ];

            if(this._isForceHttps) {
                this._isAria2c = false;
            }

            if(this._isAria2c) {

                needed_files.forEach(line => {
                    this.processLine(line);
                });

                let outputRaw = this._output.join("\r\n");

                fs.writeFileSync(config.inputFile, outputRaw, "utf8");

                let header = {
                    referrer : config.referrer,
                    userAgent : config.userAgent,
                    inputFile : `${config.inputFile}`,
                    logFile : `${this._mainPath}/${config.logFile}`,
                };

                // Create Aria2c
                let aria2cProcess = new Aria2c(header, function(err, stdout, stderr) {
                    if(err) {
                        console.log(err);
                        return;
                    }
                });

                aria2cProcess.onExit((code, signal) => {
                    console.log(`${Color.FgRed}다운로드가 완료되었습니다${Color.Reset}`);

                    this.removeFiles();
                    this.unzip().catch(e => {
                        throw new Error(e);
                    });
                });

                aria2cProcess.pendingTerminate();

            } else {

                if(!fs.existsSync(this._outputPath)) {
                    fs.mkdirSync(this._outputPath);
                }

                let functionTables = [];
                let counter = 0;

                needed_files.forEach(e => {
                    functionTables.push((callback) => {
                        const fileUrl = needed_files[counter];
                        const filename = needed_files[counter++].splitOnLast("/");

                        console.log(`동시 다운로드 시작 : ${fileUrl}, ${filename}`);
                        fileGet.downFileZipAsync(fileUrl, path.join(this._outputPath, filename)).then(res => {
                            callback(null, fileUrl);
                        }).catch(err => {
                            callback(err, null);
                        });
                    });
                });

                async.parallel(functionTables, (err, results) => {
                    if(err) {
                        throw new Error(err);
                    }

                    console.log(`${Color.FgRed}다운로드가 완료되었습니다${Color.Reset}`);

                    this.unzip().catch(e => {
                        throw new Error(e);
                    });
                });

            }

        } catch(e) {
            throw new Error(e);
        }

    }

}

class NodeWebkitRunner extends Downloader {
    async start() {
        try  {

            const files = await this.readOutputFolder();
            let isValidNW = false;

            for await (const file of files) {
                const realPath = path.join(this._outputPath, file).replace(/\\/g, "/");
                const stat = fs.lstatSync(realPath);
                if(stat.isDirectory()) {
                    let filename = path.join(realPath, 'nw.exe');
                    if(fs.existsSync(filename)) {
                        isValidNW = true;
                        console.log(`nw.exe를 찾았습니다.`);
                    }
                }
            }

            if(isValidNW) {
                const NodeWebkit = require('./NW');

                const nwProcess = new NodeWebkit({
                    projectPath : this._projectPath,
                    version: this._version,
                    outputPath: this._outputPath,
                }, function(err, stdout, stderr) {
                    if(err) {
                        console.log(err);
                        return;
                    }
                });

                nwProcess.onExit((code, signal) => {
                    console.log(`${Color.FgYellow}노드 웹킷 프로세스가 종료되었습니다.${Color.Reset}`);
                });

                nwProcess.pendingTerminate();

                return;
            }
        } catch(e) {
            throw new Error(e);
        }
    }
}

module.exports = {
    Downloader,
    NodeWebkitRunner
};<commit_msg>fixed play.bat file<commit_after>/**
 * @author biud436
 * @help
 */

const Aria2c = require('./Aria2c');
const fs = require('fs-extra');
const path = require('path');
const DecompressZip = require('decompress-zip');
const fileGet = require("./fileGet");
const async = require('async');
const { promisify } = require('util');
const readdirAsync = promisify(fs.readdir);
const Color = require("./ConsoleColor");

let config = {
    referrer : `https://dl.nwjs.io/`,
    userAgent : `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36`,
    inputFile : "input.txt",
    logFile : "log.txt",
};

String.prototype.splitOnLast = function (e) {
    var t = this.lastIndexOf(e) + 1;
    return 0 > t ? this : this.substr(t);
};

class Downloader {

    constructor(data) {
        this.initMembers(data);
        this.checkAria2c();
    }

    initMembers(data) {
        this._mainPath = data.mainPath;
        this._outputPath = data.outputPath;
        this._version = data.version;
        this._projectPath = data.projectPath;
        this._isForceHttps = data.forceHttps === "true";

        this._output = [];
        this._isAria2c = false;
    }

    processLine(line) {
        const filename = line.splitOnLast("/");
        this._output.push(line);
        this._output.push(`  dir=${this._outputPath}`);
        this._output.push(`  out=${filename}`);
    }

    checkAria2c() {

        const env = process.env["PATH"].split(";");
        const needed = env.filter(dir => dir.indexOf("aria") >= 0);
        needed.forEach(i => {
            if(fs.existsSync(path.join(i.replace(/\\/g, "/"), "aria2c.exe"))) {
                console.log("aria2c.exe 파일을 찾았습니다.");
                this._isAria2c = true;
            }
        });
    }

    removeFiles() {
        const files = {
            input: path.join(this._mainPath, "input.txt").replace( /\\/g, '/' ),
            log: path.join(this._mainPath, "log.txt").replace( /\\/g, '/' ),
        };

        if(fs.existsSync(files.input)) fs.removeSync(files.input);
        if(fs.existsSync(files.log)) fs.removeSync(files.log);
    }

    async *readOutputFolder() {
        try {
            const dir = await readdirAsync(this._outputPath).catch(err => {
                throw new Error(err);
            });
            for (const file of dir) {
                yield file;
            }
        } catch(e) {
            throw new Error(e);
        }
    }

    async unzip() {

        try {

            const files = await this.readOutputFolder();

            for await (const file of files) {
                const realPath = path.join(this._outputPath, file);
                const unzipper = new DecompressZip(realPath);

                if(fs.existsSync(realPath)) {
                    unzipper.on('error', (err) => console.log(err));
                    unzipper.on('extract', (log) => {
                        console.log('압축 해제를 완료하였습니다.');
                        fs.removeSync(realPath);

                        let dt = file.split(".");
                        dt.pop();
                        dt = dt.join(".");

                        if(process.platform === "win32") {
                            this.createRunFile(path.join(this._outputPath, dt, "play.bat"));
                        } else {
                            throw new Error(`${process.platform}은 아직 지원하지 않습니다. ${Color.BgRed}(맥이 없어서 테스트 불가능!)${Color.Reset}`);
                        }

                    });
                    unzipper.on('progress', function (fileIndex, fileCount) {
                        console.log('압축 해제 중 : ' + (fileIndex + 1) + ' of ' + fileCount);
                    });
                    unzipper.extract({
                        path: path.join(this._outputPath),
                        filter: function (file) {
                            return file.type !== "SymbolicLink";
                        }
                    });
                }

            }

        } catch(err) {
            console.error(err);
        }

    }

    createRunFile(filepath) {

        let contents = `
@SETLOCAL enableextensions enabledelayedexpansion
@ECHO OFF

set ARGS1=%~dp1
ECHO Current Path : %~dp0

ECHO RUN INDEX FILE : %ARGS1%index.html
${this._version > "v0.12.3" ? "%~dp0nw.exe %ARGS1% test": "start %~dp0nw.exe --url=%ARGS1%index.html?test"}

@ENDLOCAL
GOTO :EOF

:ERR
ECHO %ERRORMSG%
GOTO :EOF`;

        if(this._version >= "v0.45.4") {
            contents = `
@SETLOCAL enableextensions enabledelayedexpansion
@ECHO OFF

set ARGS1=%~dp1
ECHO Current Path : %~dp0

ECHO RUN INDEX FILE : %ARGS1%index.html

set ARGS2=%~dp0
"%~dp0..\..\..\bin\main.exe" %ARGS1% %ARGS2%

@ENDLOCAL
GOTO :EOF

:ERR
ECHO %ERRORMSG%
GOTO :EOF`;
        }

        fs.writeFileSync(filepath, contents, "utf8");

        // 버전 텍스트에서 중간 버전 문자열을 획득한다.
        let v = 0;
        if(/^[Vv](\d+)\.(\d+)\.(\d+)$/i.exec(this._version)) {
            v = RegExp.$2;
        }

        // package.json 때문에 충돌 문제가 생기므로 npm run add에서 node로 변경해야 한다.
        contents = `node ..\\..\\index.js --add "play.bat" "${this._version}" "${v}" "${path.dirname(filepath)}"`

        let rootFolder = path.dirname(filepath);
        fs.writeFileSync(path.join(rootFolder, "add.bat"), contents, "utf8");

    }

    async start() {

        try  {

            const version = this._version;

            if(!version) {
                throw new Error(`버전 텍스트가 없습니다.`);
            }

            const NW_SDK = version > "v0.12.3" ? "nwjs-sdk":"nwjs";

            let osType = {
                "win32": "win",
                "linux": "linux",
                "darwin": "osx",
            };

            if(!osType[process.platform]) {
                throw new Error(`지원하지 않는 OS 입니다.`);
            }

            const needed_files = [
                `https://dl.nwjs.io/${version}/${NW_SDK}-${version}-${osType[process.platform]}-${process.arch}.${process.platform === "linux" ? "tar.gz" : "zip"}`
            ];

            if(this._isForceHttps) {
                this._isAria2c = false;
            }

            if(this._isAria2c) {

                needed_files.forEach(line => {
                    this.processLine(line);
                });

                let outputRaw = this._output.join("\r\n");

                fs.writeFileSync(config.inputFile, outputRaw, "utf8");

                let header = {
                    referrer : config.referrer,
                    userAgent : config.userAgent,
                    inputFile : `${config.inputFile}`,
                    logFile : `${this._mainPath}/${config.logFile}`,
                };

                // Create Aria2c
                let aria2cProcess = new Aria2c(header, function(err, stdout, stderr) {
                    if(err) {
                        console.log(err);
                        return;
                    }
                });

                aria2cProcess.onExit((code, signal) => {
                    console.log(`${Color.FgRed}다운로드가 완료되었습니다${Color.Reset}`);

                    this.removeFiles();
                    this.unzip().catch(e => {
                        throw new Error(e);
                    });
                });

                aria2cProcess.pendingTerminate();

            } else {

                if(!fs.existsSync(this._outputPath)) {
                    fs.mkdirSync(this._outputPath);
                }

                let functionTables = [];
                let counter = 0;

                needed_files.forEach(e => {
                    functionTables.push((callback) => {
                        const fileUrl = needed_files[counter];
                        const filename = needed_files[counter++].splitOnLast("/");

                        console.log(`동시 다운로드 시작 : ${fileUrl}, ${filename}`);
                        fileGet.downFileZipAsync(fileUrl, path.join(this._outputPath, filename)).then(res => {
                            callback(null, fileUrl);
                        }).catch(err => {
                            callback(err, null);
                        });
                    });
                });

                async.parallel(functionTables, (err, results) => {
                    if(err) {
                        throw new Error(err);
                    }

                    console.log(`${Color.FgRed}다운로드가 완료되었습니다${Color.Reset}`);

                    this.unzip().catch(e => {
                        throw new Error(e);
                    });
                });

            }

        } catch(e) {
            throw new Error(e);
        }

    }

}

class NodeWebkitRunner extends Downloader {
    async start() {
        try  {

            const files = await this.readOutputFolder();
            let isValidNW = false;

            for await (const file of files) {
                const realPath = path.join(this._outputPath, file).replace(/\\/g, "/");
                const stat = fs.lstatSync(realPath);
                if(stat.isDirectory()) {
                    let filename = path.join(realPath, 'nw.exe');
                    if(fs.existsSync(filename)) {
                        isValidNW = true;
                        console.log(`nw.exe를 찾았습니다.`);
                    }
                }
            }

            if(isValidNW) {
                const NodeWebkit = require('./NW');

                const nwProcess = new NodeWebkit({
                    projectPath : this._projectPath,
                    version: this._version,
                    outputPath: this._outputPath,
                }, function(err, stdout, stderr) {
                    if(err) {
                        console.log(err);
                        return;
                    }
                });

                nwProcess.onExit((code, signal) => {
                    console.log(`${Color.FgYellow}노드 웹킷 프로세스가 종료되었습니다.${Color.Reset}`);
                });

                nwProcess.pendingTerminate();

                return;
            }
        } catch(e) {
            throw new Error(e);
        }
    }
}

module.exports = {
    Downloader,
    NodeWebkitRunner
};<|endoftext|>


<commit_before>const {eq, schema, doc, p} = require("prosemirror-test-builder")
const {EditorState, Plugin, TextSelection} = require("prosemirror-state")
const ist = require("ist")

const {history, closeHistory, undo, redo, undoDepth, redoDepth} = require("../dist/history")

let plugin = history()

function mkState(doc, config) {
  let plugins = [config ? history(config) : plugin]
  if (config && config.preserveItems) plugins.push(new Plugin({historyPreserveItems: true}))
  return EditorState.create({schema, plugins, doc})
}

function type(state, text) {
  return state.apply(state.tr.insertText(text))
}
function command(state, command) {
  command(state, tr => state = state.apply(tr))
  return state
}

function compress(state) {
  // NOTE: This is mutating stuff that shouldn't be mutated. Not safe
  // to do outside of these tests.
  plugin.getState(state).done = plugin.getState(state).done.compress()
}

describe("history", () => {
  it("enables undo", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("enables redo", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("ab")), eq)
  })

  it("tracks multiple levels of history", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    state = state.apply(state.tr.insertText("c", 1))
    ist(state.doc, doc(p("cab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("cab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("ab")), eq)
  })

  it("starts a new event when newGroupDelay elapses", () => {
    let state = mkState(null, {newGroupDelay: 1000})
    state = state.apply(state.tr.insertText("a").setTime(1000))
    state = state.apply(state.tr.insertText("b").setTime(1600))
    ist(undoDepth(state), 1)
    state = state.apply(state.tr.insertText("c").setTime(2700))
    ist(undoDepth(state), 2)
    state = command(state, undo)
    state = state.apply(state.tr.insertText("d").setTime(2800))
    ist(undoDepth(state), 2)
  })

  it("allows changes that aren't part of the history", () => {
    let state = mkState()
    state = type(state, "hello")
    state = state.apply(state.tr.insertText("oops", 1).setMeta("addToHistory", false))
    state = state.apply(state.tr.insertText("!", 10).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.doc, doc(p("oops!")), eq)
  })

  it("doesn't get confused by an undo not adding any redo item", () => {
    let state = mkState()
    state = state.apply(state.tr.insertText("foo"))
    state = state.apply(state.tr.replaceWith(1, 4, schema.text("bar")).setMeta("addToHistory", false))
    state = command(state, undo)
    state = command(state, redo)
    ist(state.doc, doc(p("bar")), eq)
  })

  function unsyncedComplex(state, doCompress) {
    state = type(state, "hello")
    state = state.apply(closeHistory(state.tr))
    state = type(state, "!")
    state = state.apply(state.tr.insertText("....", 1).setMeta("addToHistory", false))
    state = state.apply(state.tr.split(3))
    ist(state.doc, doc(p(".."), p("..hello!")), eq)
    state = state.apply(state.tr.split(2).setMeta("addToHistory", false))
    if (doCompress) compress(state)
    state = command(state, undo)
    state = command(state, undo)
    ist(state.doc, doc(p("."), p("...hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("."), p("...")), eq)
  }

  it("can handle complex editing sequences", () => {
    unsyncedComplex(mkState(), false)
  })

  it("can handle complex editing sequences with compression", () => {
    unsyncedComplex(mkState(), true)
  })

  it("supports overlapping edits", () => {
    let state = mkState()
    state = type(state, "hello")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.delete(1, 6))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("supports overlapping edits that aren't collapsed", () => {
    let state = mkState()
    state = state.apply(state.tr.insertText("h", 1).setMeta("addToHistory", false))
    state = type(state, "ello")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.delete(1, 6))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("h")), eq)
  })

  it("supports overlapping unsynced deletes", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = type(state, "hello")
    state = state.apply(state.tr.delete(1, 8).setMeta("addToHistory", false))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("can go back and forth through history multiple times", () => {
    let state = mkState()
    state = type(state, "one")
    state = type(state, " two")
    state = state.apply(closeHistory(state.tr))
    state = type(state, " three")
    state = state.apply(state.tr.insertText("zero ", 1))
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
    state = type(state, "top")
    for (let i = 0; i < 6; i++) {
      let re = i % 2
      for (let j = 0; j < 4; j++) state = command(state, re ? redo : undo)
      ist(state.doc, re ? doc(p("top"), p("zero one two three")) : doc(p()), eq)
    }
  })

  it("supports non-tracked changes next to tracked changes", () => {
    let state = mkState()
    state = type(state, "o")
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.insertText("zzz", 4).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.doc, doc(p("zzz")), eq)
  })

  it("can go back and forth through history when preserving items", () => {
    let state = mkState()
    state = type(state, "one")
    state = type(state, " two")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.insertText("xxx", state.selection.head).setMeta("addToHistory", false))
    state = type(state, " three")
    state = state.apply(state.tr.insertText("zero ", 1))
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
    state = type(state, "top")
    state = state.apply(state.tr.insertText("yyy", 1).setMeta("addToHistory", false))
    for (let i = 0; i < 3; i++) {
      if (i == 2) compress(state)
      for (let j = 0; j < 4; j++) state = command(state, undo)
      ist(state.doc, doc(p("yyyxxx")), eq)
      for (let j = 0; j < 4; j++) state = command(state, redo)
      ist(state.doc, doc(p("yyytop"), p("zero one twoxxx three")), eq)
    }
  })

  it("restores selection on undo", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    let selection = state.selection
    state = state.apply(state.tr.replaceWith(selection.from, selection.to, schema.text("hello")))
    let selection2 = state.selection
    state = command(state, undo)
    ist(state.selection.eq(selection))
    state = command(state, redo)
    ist(state.selection.eq(selection2))
  })

  it("rebases selection on undo", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    state = state.apply(state.tr.insert(1, schema.text("hello")))
    state = state.apply(state.tr.insert(1, schema.text("---")).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.selection.head, 6)
  })

  it("handles change overwriting in item-preserving mode", () => {
    let state = mkState(null, {preserveItems: true})
    state = type(state, "a")
    state = type(state, "b")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    state = type(state, "c")
    state = command(state, undo)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("supports querying for the undo and redo depth", () => {
    let state = mkState()
    state = type(state, "a")
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
    state = state.apply(state.tr.insertText("b", 1).setMeta("addToHistory", false))
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
    state = command(state, undo)
    ist(undoDepth(state), 0)
    ist(redoDepth(state), 1)
    state = command(state, redo)
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
  })
})
<commit_msg>Cover passing EditorStates without history field<commit_after>const {eq, schema, doc, p} = require("prosemirror-test-builder")
const {EditorState, Plugin, TextSelection} = require("prosemirror-state")
const ist = require("ist")

const {history, closeHistory, undo, redo, undoDepth, redoDepth} = require("../dist/history")

let plugin = history()

function mkState(doc, config) {
  let plugins = [config ? history(config) : plugin]
  if (config && config.preserveItems) plugins.push(new Plugin({historyPreserveItems: true}))
  return EditorState.create({schema, plugins, doc})
}

function type(state, text) {
  return state.apply(state.tr.insertText(text))
}
function command(state, command) {
  command(state, tr => state = state.apply(tr))
  return state
}

function compress(state) {
  // NOTE: This is mutating stuff that shouldn't be mutated. Not safe
  // to do outside of these tests.
  plugin.getState(state).done = plugin.getState(state).done.compress()
}

describe("history", () => {
  it("enables undo", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("enables redo", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("ab")), eq)
  })

  it("tracks multiple levels of history", () => {
    let state = mkState()
    state = type(state, "a")
    state = type(state, "b")
    state = state.apply(state.tr.insertText("c", 1))
    ist(state.doc, doc(p("cab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("ab")), eq)
    state = command(state, redo)
    ist(state.doc, doc(p("cab")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("ab")), eq)
  })

  it("starts a new event when newGroupDelay elapses", () => {
    let state = mkState(null, {newGroupDelay: 1000})
    state = state.apply(state.tr.insertText("a").setTime(1000))
    state = state.apply(state.tr.insertText("b").setTime(1600))
    ist(undoDepth(state), 1)
    state = state.apply(state.tr.insertText("c").setTime(2700))
    ist(undoDepth(state), 2)
    state = command(state, undo)
    state = state.apply(state.tr.insertText("d").setTime(2800))
    ist(undoDepth(state), 2)
  })

  it("allows changes that aren't part of the history", () => {
    let state = mkState()
    state = type(state, "hello")
    state = state.apply(state.tr.insertText("oops", 1).setMeta("addToHistory", false))
    state = state.apply(state.tr.insertText("!", 10).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.doc, doc(p("oops!")), eq)
  })

  it("doesn't get confused by an undo not adding any redo item", () => {
    let state = mkState()
    state = state.apply(state.tr.insertText("foo"))
    state = state.apply(state.tr.replaceWith(1, 4, schema.text("bar")).setMeta("addToHistory", false))
    state = command(state, undo)
    state = command(state, redo)
    ist(state.doc, doc(p("bar")), eq)
  })

  function unsyncedComplex(state, doCompress) {
    state = type(state, "hello")
    state = state.apply(closeHistory(state.tr))
    state = type(state, "!")
    state = state.apply(state.tr.insertText("....", 1).setMeta("addToHistory", false))
    state = state.apply(state.tr.split(3))
    ist(state.doc, doc(p(".."), p("..hello!")), eq)
    state = state.apply(state.tr.split(2).setMeta("addToHistory", false))
    if (doCompress) compress(state)
    state = command(state, undo)
    state = command(state, undo)
    ist(state.doc, doc(p("."), p("...hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("."), p("...")), eq)
  }

  it("can handle complex editing sequences", () => {
    unsyncedComplex(mkState(), false)
  })

  it("can handle complex editing sequences with compression", () => {
    unsyncedComplex(mkState(), true)
  })

  it("supports overlapping edits", () => {
    let state = mkState()
    state = type(state, "hello")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.delete(1, 6))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("supports overlapping edits that aren't collapsed", () => {
    let state = mkState()
    state = state.apply(state.tr.insertText("h", 1).setMeta("addToHistory", false))
    state = type(state, "ello")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.delete(1, 6))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("hello")), eq)
    state = command(state, undo)
    ist(state.doc, doc(p("h")), eq)
  })

  it("supports overlapping unsynced deletes", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = type(state, "hello")
    state = state.apply(state.tr.delete(1, 8).setMeta("addToHistory", false))
    ist(state.doc, doc(p()), eq)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("can go back and forth through history multiple times", () => {
    let state = mkState()
    state = type(state, "one")
    state = type(state, " two")
    state = state.apply(closeHistory(state.tr))
    state = type(state, " three")
    state = state.apply(state.tr.insertText("zero ", 1))
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
    state = type(state, "top")
    for (let i = 0; i < 6; i++) {
      let re = i % 2
      for (let j = 0; j < 4; j++) state = command(state, re ? redo : undo)
      ist(state.doc, re ? doc(p("top"), p("zero one two three")) : doc(p()), eq)
    }
  })

  it("supports non-tracked changes next to tracked changes", () => {
    let state = mkState()
    state = type(state, "o")
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.insertText("zzz", 4).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.doc, doc(p("zzz")), eq)
  })

  it("can go back and forth through history when preserving items", () => {
    let state = mkState()
    state = type(state, "one")
    state = type(state, " two")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.insertText("xxx", state.selection.head).setMeta("addToHistory", false))
    state = type(state, " three")
    state = state.apply(state.tr.insertText("zero ", 1))
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.split(1))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1)))
    state = type(state, "top")
    state = state.apply(state.tr.insertText("yyy", 1).setMeta("addToHistory", false))
    for (let i = 0; i < 3; i++) {
      if (i == 2) compress(state)
      for (let j = 0; j < 4; j++) state = command(state, undo)
      ist(state.doc, doc(p("yyyxxx")), eq)
      for (let j = 0; j < 4; j++) state = command(state, redo)
      ist(state.doc, doc(p("yyytop"), p("zero one twoxxx three")), eq)
    }
  })

  it("restores selection on undo", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    let selection = state.selection
    state = state.apply(state.tr.replaceWith(selection.from, selection.to, schema.text("hello")))
    let selection2 = state.selection
    state = command(state, undo)
    ist(state.selection.eq(selection))
    state = command(state, redo)
    ist(state.selection.eq(selection2))
  })

  it("rebases selection on undo", () => {
    let state = mkState()
    state = type(state, "hi")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    state = state.apply(state.tr.insert(1, schema.text("hello")))
    state = state.apply(state.tr.insert(1, schema.text("---")).setMeta("addToHistory", false))
    state = command(state, undo)
    ist(state.selection.head, 6)
  })

  it("handles change overwriting in item-preserving mode", () => {
    let state = mkState(null, {preserveItems: true})
    state = type(state, "a")
    state = type(state, "b")
    state = state.apply(closeHistory(state.tr))
    state = state.apply(state.tr.setSelection(TextSelection.create(state.doc, 1, 3)))
    state = type(state, "c")
    state = command(state, undo)
    state = command(state, undo)
    ist(state.doc, doc(p()), eq)
  })

  it("supports querying for the undo and redo depth", () => {
    let state = mkState()
    state = type(state, "a")
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
    state = state.apply(state.tr.insertText("b", 1).setMeta("addToHistory", false))
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
    state = command(state, undo)
    ist(undoDepth(state), 0)
    ist(redoDepth(state), 1)
    state = command(state, redo)
    ist(undoDepth(state), 1)
    ist(redoDepth(state), 0)
  })

  it("all functions gracefully handle EditorStates without history", () => {
    let state = new EditorState()
    ist(undoDepth(state), 0)
    ist(redoDepth(state), 0)
    ist(undo(state), false)
    ist(redo(state), false)
  })
})
<|endoftext|>


<commit_before>var nc = require('ncurses'),
        http = require('http'),
          win = new nc.Window(),
          config = require('./config.js'),
            request = require('superagent');
var data = {
    username:'arscanable',
    level:1,
    health:45,
    healthMax:50,
    exp: 16,
    expMax: 20,
    gold: 5,
    silver: 10,
    habits: [
        {name:'1 Hour Productive Work',up:1,down:0},
        ],
    daily: [
        {name:'Go to Gym',done:0},
        ],
    todos: [
        {name:'Ring Insurance', done:0},
    rewards: [   /* NOT IMPLEMENTED YET */
        { name: 'Leather Armor', 'price': 30, description: 'Helps with stuff'},
        ],

};
var refresh =  function(){
        request.get(config.apiurl + "/user").set('Accept', 'application/json').set('X-API-User', config.apiuser).set('X-API-Key', config.apitoken).end(function(res){
        data.username = res.body.auth.local.username;
        data.level = res.body.stats.lvl;
        data.health = Math.ceil(res.body.stats.hp);
        data.healthMax=50;
        data.exp = Math.floor(res.body.stats.exp);
        data.expMax = 200;
        data.habits = [];
        data.daily = [];
        data.todos = [];
        for(var i = 0; i<res.body.habitIds.length;i++){
            data.habits[i] = {};
            data.habits[i].id = res.body.habitIds[i]
            data.habits[i].name = res.body.tasks[res.body.habitIds[i]].text;
            data.habits[i].up = res.body.tasks[res.body.habitIds[i]].up;
            data.habits[i].down = res.body.tasks[res.body.habitIds[i]].down;
        }
        for(var i = 0; i<res.body.dailyIds.length;i++){
            data.daily[i] = {};
            data.daily[i].id = res.body.dailyIds[i]
            data.daily[i].name = res.body.tasks[res.body.dailyIds[i]].text;
            data.daily[i].up = res.body.tasks[res.body.dailyIds[i]].up;
            data.daily[i].down = res.body.tasks[res.body.dailyIds[i]].down;
            data.daily[i].done = res.body.tasks[res.body.dailyIds[i]].completed;
        }
        for(var i = 0; i<res.body.todoIds.length;i++){
            data.todos[i] = {};
            data.todos[i].id = res.body.dailyIds[i]
            data.todos[i].name = res.body.tasks[res.body.todoIds[i]].text;
            data.todos[i].up = res.body.tasks[res.body.todoIds[i]].up;
            data.todos[i].down = res.body.tasks[res.body.todoIds[i]].down;
            data.todos[i].done = res.body.tasks[res.body.todoIds[i]].completed;
        }
        drawFn();
        });

    }

var addtask = function(task){

//        request.get(config.apiurl + "/user").set('Accept', 'application/json').set('X-API-User', config.apiuser).set('X-API-Key', config.apitoken).end(function(res){

}

refresh();
setInterval(refresh,10000);

var items = [];

nc.colorPair(1,nc.colors.BLACK,nc.colors.WHITE);
nc.colorPair(2,nc.colors.WHITE,nc.colors.BLACK);
nc.colorPair(3,nc.colors.WHITE,nc.colors.CYAN);
var statusWindow = new nc.Window(7,nc.cols-4);
var currentIndex = 0;
var unsaved = false;

var drawFn = function(){
    items = [];

    /* draw the header */

    drawFooter(win,(unsaved?'unsaved':''));

        /* draw the status box */
    statusWindow.move(1,2);
    statusWindow.box();
    statusWindow.cursor(0,2);
    statusWindow.addstr(data.username);
    statusWindow.addstr(' [lvl ' + data.level + ']');
    statusWindow.refresh();
    drawBar(statusWindow,1,2,data.health,data.healthMax,'Health',2);
    drawBar(statusWindow,1,2,data.exp,data.expMax,'Exp',4);

    win.cursor(9,0);
    var habitsDone = 0;

    win.cursor(win.cury+1,0);
    for(var i = 0; i<data.habits.length;i++){
        win.cursor(win.cury+1,2);
        data.habits[i].cury = win.cury;
        data.habits[i].type = 'habits';
        items.push(data.habits[i]);
        win.addstr('[' + (data.habits[i].up - data.habits[i].down == 0?' ':'' + Math.abs(data.habits[i].up - data.habits[i].down)) + '] ' + data.habits[i].name.substr(0,win.width-5));

        if (data.habits[i].up - data.habits[i].down < 0){
            win.cursor(win.cury,3);
            win.addstr('-');
            //win.chgat(win.cury, 3, 1, nc.attrs.NORMAL, nc.colorPair(3));

        }
        habitsDone += data.habits[i].up;
        habitsDone -= data.habits[i].down;

    }
    win.cursor(win.cury-data.habits.length-1,0);
    drawHeader(win,"Habits [" + habitsDone + " today]");
    win.cursor(win.cury+data.habits.length+4,0);

    var dailyDone = 0;

    for(var i = 0; i<data.daily.length;i++){
        win.cursor(win.cury+1,2);
        data.daily[i].cury = win.cury;
        data.daily[i].type = 'daily';
        items.push(data.daily[i]);
        win.addstr('[' + (data.daily[i].done?'X':' ') + '] ' + data.daily[i].name.substr(0,win.width-5));
        dailyDone += data.daily[i].done;

    }
    win.cursor(win.cury-data.daily.length-1,0);
    drawHeader(win,"Daily [" + dailyDone + " complete]");
    win.cursor(win.cury+data.daily.length+4,0);

    var todosDone = 0;

    for(var i = 0; i<data.todos.length;i++){
        win.cursor(win.cury+1,2);
        data.todos[i].cury = win.cury;
        data.todos[i].type = 'todos';
        items.push(data.todos[i]);
        win.addstr('[' + (data.todos[i].done?'X':' ') + '] ' + data.todos[i].name.substr(0,win.width-5));
        todosDone += data.todos[i].done;
    }

    win.cursor(win.cury-data.todos.length-1,0);
    drawHeader(win,"Todos [" + todosDone + " completed today]");
    win.cursor(win.cury+data.todos.length+4,0);

    win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
    win.refresh();
}

//drawFn();

function drawHeader(mywin, title){
    mywin.hline(win.width-3, nc.ACS.HLINE);
    mywin.cursor(mywin.cury,0);
    mywin.addstr('  ');
    mywin.cursor(mywin.cury,3);
    mywin.addstr(' ' + title + ' ');
    mywin.cursor(mywin.cury,0);
}

var inputWindow = new nc.Window(1,nc.cols);
var mode = 'normal';
nc.showCursor = false;
inputWindow.move(win.height-1,0);
inputWindow.refresh();

inputWindow.on('inputChar', function (c, i) {

    if(mode == 'normal'){
        inputWindow.clear();

        if(i === 106){

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.NORMAL, nc.colorPair(0));
            currentIndex++;
            if(currentIndex > items.length -1){
                currentIndex = 0;
            }

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
            win.cursor(items[currentIndex].cury,2);
            win.refresh();

        } else if(i === 107){

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.NORMAL, nc.colorPair(0));
            currentIndex--;
            if(currentIndex < 0){
                currentIndex = items.length-1;
            }

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
            win.cursor(items[currentIndex].cury,2);
            win.refresh();

        } else if(i === 105){
            unsaved = true;
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                items[currentIndex].done = 1;
                process.nextTick(drawFn);

            } else if(items[currentIndex].type == 'habits'){
                items[currentIndex].up += 1;
                process.nextTick(drawFn);
            }

        } else if(i === 120 || i == 32){
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                unsaved = true;
                items[currentIndex].done = (items[currentIndex].done * -1) + 1;
                process.nextTick(drawFn);
            }

        } else if(i === 100){
            unsaved = true;
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                items[currentIndex].done = 0;
                process.nextTick(drawFn);
            } else if(items[currentIndex].type == 'habits'){
                items[currentIndex].down += 1;
                process.nextTick(drawFn);
            }
        }  else if(i === 58){
            mode = 'command';
            inputWindow.inbuffer = '';
            nc.showCursor = true;

        }
    }

    if(mode == 'command'){
        if(i === 9){
            mode = 'normal';
            inputWindow.inbuffer = '';
            nc.showCursor = false;
            inputWindow.refresh();
        }else if(i === 330){

            var prev_x = inputWindow.curx;
            inputWindow.delch(inputWindow.height-1, inputWindow.curx);
            inputWindow.inbuffer = inputWindow.inbuffer.substring(0, inputWindow.curx-1) + inputWindow.inbuffer.substring(inputWindow.curx);
            inputWindow.cursor(inputWindow.height-1, prev_x);
            if(inputWindow.inbuffer.length == 0){
                mode = 'normal';
                nc.showCursor = false;
            }
            inputWindow.refresh();

        } else if (i === 127 && inputWindow.curx > 0) {
            var prev_x = inputWindow.curx-1;
            inputWindow.delch(inputWindow.height-1, prev_x);
            inputWindow.inbuffer = inputWindow.inbuffer.substring(0, prev_x) + inputWindow.inbuffer.substring(prev_x+1);
            inputWindow.cursor(inputWindow.height-1, prev_x);
            if(inputWindow.inbuffer.length == 0){
                mode = 'normal';
                nc.showCursor = false;
            }
            inputWindow.refresh();
        } else if (i === nc.keys.NEWLINE) {
            if (inputWindow.inbuffer.length) {

                if (inputWindow.inbuffer[0] === ':') {
                    var cmd = inputWindow.inbuffer.substring(1).split(' ', 1).join('').trim(),
                        args = inputWindow.inbuffer.substring(inputWindow.inbuffer.indexOf(cmd)+cmd.length+1).trim();
                    switch (cmd.toLowerCase()) {
                        case 'a':
                            if (args.length) {
                                data.todos.push({'name':args, done:0});
                                inputWindow.clear();
                                inputWindow.inbuffer = '';
                                process.nextTick(drawFn);
                                nc.showCursor = false;
                                mode = 'normal';
                                unsaved = true;

                            }
                            break;

                        case 'q':
                            nc.cleanup();
                            process.exit(0);
                            break;
                        default:
                            inputWindow.clear();
                            inputWindow.addstr('Unknown command: ' + cmd);
                            mode = 'normal';
                            nc.showCursor = false;

                    }
                }
            }

        } else if (i >= 32 && i <= 126 && inputWindow.curx < inputWindow.width-4) {
           inputWindow.echochar(i);
           inputWindow.inbuffer += c;
        }

    }
    inputWindow.refresh();

});

function drawFooter(mywin, state){
    mywin.cursor(mywin.height-2,0);
    mywin.clrtoeol();
    win.addstr("HabitRPG");
    mywin.addstr(mywin.height-2, mywin.width-(Math.min(state.length, mywin.width)), state, mywin.width);
    mywin.chgat(mywin.height-2, 0, mywin.width, nc.attrs.STANDOUT, nc.colorPair(5));
}

function drawBar(mywin, onColorPair, offColorPair, val, valMax, label, rowStart){
    var totalwidth = mywin.width - 6,
        onWidth = Math.floor(totalwidth * (val / valMax)),
        offWidth = totalwidth - onWidth,
        poststring = '' + val + '/' + valMax,
        paddedstring = label + Array(totalwidth - label.length + 1 - poststring.length).join(' ') + poststring;

    mywin.cursor(rowStart,3);
    mywin.attron(nc.colorPair(1));
    for (var i = 0; i < paddedstring.length; i++){
        if(i > onWidth){
            mywin.attroff(nc.colorPair(1));
            mywin.attron(nc.colorPair(2));
        }
        mywin.addstr(paddedstring.charAt(i));
    }
    mywin.attroff(nc.colorPair(2));
    mywin.refresh();
}
process.on('SIGWINCH',drawFn);
<commit_msg>whoops, deleted one too many lines during cleanup<commit_after>var nc = require('ncurses'),
        http = require('http'),
          win = new nc.Window(),
          config = require('./config.js'),
            request = require('superagent');
var data = {
    username:'arscanable',
    level:1,
    health:45,
    healthMax:50,
    exp: 16,
    expMax: 20,
    gold: 5,
    silver: 10,
    habits: [
        {name:'1 Hour Productive Work',up:1,down:0},
        ],
    daily: [
        {name:'Go to Gym',done:0},
        ],
    todos: [
        {name:'Ring Insurance', done:0},
    ],
    rewards: [   /* NOT IMPLEMENTED YET */
        { name: 'Leather Armor', 'price': 30, description: 'Helps with stuff'},
        ],

};
var refresh =  function(){
        request.get(config.apiurl + "/user").set('Accept', 'application/json').set('X-API-User', config.apiuser).set('X-API-Key', config.apitoken).end(function(res){
        data.username = res.body.auth.local.username;
        data.level = res.body.stats.lvl;
        data.health = Math.ceil(res.body.stats.hp);
        data.healthMax=50;
        data.exp = Math.floor(res.body.stats.exp);
        data.expMax = 200;
        data.habits = [];
        data.daily = [];
        data.todos = [];
        for(var i = 0; i<res.body.habitIds.length;i++){
            data.habits[i] = {};
            data.habits[i].id = res.body.habitIds[i]
            data.habits[i].name = res.body.tasks[res.body.habitIds[i]].text;
            data.habits[i].up = res.body.tasks[res.body.habitIds[i]].up;
            data.habits[i].down = res.body.tasks[res.body.habitIds[i]].down;
        }
        for(var i = 0; i<res.body.dailyIds.length;i++){
            data.daily[i] = {};
            data.daily[i].id = res.body.dailyIds[i]
            data.daily[i].name = res.body.tasks[res.body.dailyIds[i]].text;
            data.daily[i].up = res.body.tasks[res.body.dailyIds[i]].up;
            data.daily[i].down = res.body.tasks[res.body.dailyIds[i]].down;
            data.daily[i].done = res.body.tasks[res.body.dailyIds[i]].completed;
        }
        for(var i = 0; i<res.body.todoIds.length;i++){
            data.todos[i] = {};
            data.todos[i].id = res.body.dailyIds[i]
            data.todos[i].name = res.body.tasks[res.body.todoIds[i]].text;
            data.todos[i].up = res.body.tasks[res.body.todoIds[i]].up;
            data.todos[i].down = res.body.tasks[res.body.todoIds[i]].down;
            data.todos[i].done = res.body.tasks[res.body.todoIds[i]].completed;
        }
        drawFn();
        });

    }

var addtask = function(task){

//        request.get(config.apiurl + "/user").set('Accept', 'application/json').set('X-API-User', config.apiuser).set('X-API-Key', config.apitoken).end(function(res){

}

refresh();
setInterval(refresh,10000);

var items = [];

nc.colorPair(1,nc.colors.BLACK,nc.colors.WHITE);
nc.colorPair(2,nc.colors.WHITE,nc.colors.BLACK);
nc.colorPair(3,nc.colors.WHITE,nc.colors.CYAN);
var statusWindow = new nc.Window(7,nc.cols-4);
var currentIndex = 0;
var unsaved = false;

var drawFn = function(){
    items = [];

    /* draw the header */

    drawFooter(win,(unsaved?'unsaved':''));

        /* draw the status box */
    statusWindow.move(1,2);
    statusWindow.box();
    statusWindow.cursor(0,2);
    statusWindow.addstr(data.username);
    statusWindow.addstr(' [lvl ' + data.level + ']');
    statusWindow.refresh();
    drawBar(statusWindow,1,2,data.health,data.healthMax,'Health',2);
    drawBar(statusWindow,1,2,data.exp,data.expMax,'Exp',4);

    win.cursor(9,0);
    var habitsDone = 0;

    win.cursor(win.cury+1,0);
    for(var i = 0; i<data.habits.length;i++){
        win.cursor(win.cury+1,2);
        data.habits[i].cury = win.cury;
        data.habits[i].type = 'habits';
        items.push(data.habits[i]);
        win.addstr('[' + (data.habits[i].up - data.habits[i].down == 0?' ':'' + Math.abs(data.habits[i].up - data.habits[i].down)) + '] ' + data.habits[i].name.substr(0,win.width-5));

        if (data.habits[i].up - data.habits[i].down < 0){
            win.cursor(win.cury,3);
            win.addstr('-');
            //win.chgat(win.cury, 3, 1, nc.attrs.NORMAL, nc.colorPair(3));

        }
        habitsDone += data.habits[i].up;
        habitsDone -= data.habits[i].down;

    }
    win.cursor(win.cury-data.habits.length-1,0);
    drawHeader(win,"Habits [" + habitsDone + " today]");
    win.cursor(win.cury+data.habits.length+4,0);

    var dailyDone = 0;

    for(var i = 0; i<data.daily.length;i++){
        win.cursor(win.cury+1,2);
        data.daily[i].cury = win.cury;
        data.daily[i].type = 'daily';
        items.push(data.daily[i]);
        win.addstr('[' + (data.daily[i].done?'X':' ') + '] ' + data.daily[i].name.substr(0,win.width-5));
        dailyDone += data.daily[i].done;

    }
    win.cursor(win.cury-data.daily.length-1,0);
    drawHeader(win,"Daily [" + dailyDone + " complete]");
    win.cursor(win.cury+data.daily.length+4,0);

    var todosDone = 0;

    for(var i = 0; i<data.todos.length;i++){
        win.cursor(win.cury+1,2);
        data.todos[i].cury = win.cury;
        data.todos[i].type = 'todos';
        items.push(data.todos[i]);
        win.addstr('[' + (data.todos[i].done?'X':' ') + '] ' + data.todos[i].name.substr(0,win.width-5));
        todosDone += data.todos[i].done;
    }

    win.cursor(win.cury-data.todos.length-1,0);
    drawHeader(win,"Todos [" + todosDone + " completed today]");
    win.cursor(win.cury+data.todos.length+4,0);

    win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
    win.refresh();
}

//drawFn();

function drawHeader(mywin, title){
    mywin.hline(win.width-3, nc.ACS.HLINE);
    mywin.cursor(mywin.cury,0);
    mywin.addstr('  ');
    mywin.cursor(mywin.cury,3);
    mywin.addstr(' ' + title + ' ');
    mywin.cursor(mywin.cury,0);
}

var inputWindow = new nc.Window(1,nc.cols);
var mode = 'normal';
nc.showCursor = false;
inputWindow.move(win.height-1,0);
inputWindow.refresh();

inputWindow.on('inputChar', function (c, i) {

    if(mode == 'normal'){
        inputWindow.clear();

        if(i === 106){

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.NORMAL, nc.colorPair(0));
            currentIndex++;
            if(currentIndex > items.length -1){
                currentIndex = 0;
            }

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
            win.cursor(items[currentIndex].cury,2);
            win.refresh();

        } else if(i === 107){

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.NORMAL, nc.colorPair(0));
            currentIndex--;
            if(currentIndex < 0){
                currentIndex = items.length-1;
            }

            win.chgat(items[currentIndex].cury, 2, win.width-5, nc.attrs.STANDOUT, nc.colorPair(5));
            win.cursor(items[currentIndex].cury,2);
            win.refresh();

        } else if(i === 105){
            unsaved = true;
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                items[currentIndex].done = 1;
                process.nextTick(drawFn);

            } else if(items[currentIndex].type == 'habits'){
                items[currentIndex].up += 1;
                process.nextTick(drawFn);
            }

        } else if(i === 120 || i == 32){
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                unsaved = true;
                items[currentIndex].done = (items[currentIndex].done * -1) + 1;
                process.nextTick(drawFn);
            }

        } else if(i === 100){
            unsaved = true;
            if(items[currentIndex].type == 'daily' || items[currentIndex].type == 'todos'){
                items[currentIndex].done = 0;
                process.nextTick(drawFn);
            } else if(items[currentIndex].type == 'habits'){
                items[currentIndex].down += 1;
                process.nextTick(drawFn);
            }
        }  else if(i === 58){
            mode = 'command';
            inputWindow.inbuffer = '';
            nc.showCursor = true;

        }
    }

    if(mode == 'command'){
        if(i === 9){
            mode = 'normal';
            inputWindow.inbuffer = '';
            nc.showCursor = false;
            inputWindow.refresh();
        }else if(i === 330){

            var prev_x = inputWindow.curx;
            inputWindow.delch(inputWindow.height-1, inputWindow.curx);
            inputWindow.inbuffer = inputWindow.inbuffer.substring(0, inputWindow.curx-1) + inputWindow.inbuffer.substring(inputWindow.curx);
            inputWindow.cursor(inputWindow.height-1, prev_x);
            if(inputWindow.inbuffer.length == 0){
                mode = 'normal';
                nc.showCursor = false;
            }
            inputWindow.refresh();

        } else if (i === 127 && inputWindow.curx > 0) {
            var prev_x = inputWindow.curx-1;
            inputWindow.delch(inputWindow.height-1, prev_x);
            inputWindow.inbuffer = inputWindow.inbuffer.substring(0, prev_x) + inputWindow.inbuffer.substring(prev_x+1);
            inputWindow.cursor(inputWindow.height-1, prev_x);
            if(inputWindow.inbuffer.length == 0){
                mode = 'normal';
                nc.showCursor = false;
            }
            inputWindow.refresh();
        } else if (i === nc.keys.NEWLINE) {
            if (inputWindow.inbuffer.length) {

                if (inputWindow.inbuffer[0] === ':') {
                    var cmd = inputWindow.inbuffer.substring(1).split(' ', 1).join('').trim(),
                        args = inputWindow.inbuffer.substring(inputWindow.inbuffer.indexOf(cmd)+cmd.length+1).trim();
                    switch (cmd.toLowerCase()) {
                        case 'a':
                            if (args.length) {
                                data.todos.push({'name':args, done:0});
                                inputWindow.clear();
                                inputWindow.inbuffer = '';
                                process.nextTick(drawFn);
                                nc.showCursor = false;
                                mode = 'normal';
                                unsaved = true;

                            }
                            break;

                        case 'q':
                            nc.cleanup();
                            process.exit(0);
                            break;
                        default:
                            inputWindow.clear();
                            inputWindow.addstr('Unknown command: ' + cmd);
                            mode = 'normal';
                            nc.showCursor = false;

                    }
                }
            }

        } else if (i >= 32 && i <= 126 && inputWindow.curx < inputWindow.width-4) {
           inputWindow.echochar(i);
           inputWindow.inbuffer += c;
        }

    }
    inputWindow.refresh();

});

function drawFooter(mywin, state){
    mywin.cursor(mywin.height-2,0);
    mywin.clrtoeol();
    win.addstr("HabitRPG");
    mywin.addstr(mywin.height-2, mywin.width-(Math.min(state.length, mywin.width)), state, mywin.width);
    mywin.chgat(mywin.height-2, 0, mywin.width, nc.attrs.STANDOUT, nc.colorPair(5));
}

function drawBar(mywin, onColorPair, offColorPair, val, valMax, label, rowStart){
    var totalwidth = mywin.width - 6,
        onWidth = Math.floor(totalwidth * (val / valMax)),
        offWidth = totalwidth - onWidth,
        poststring = '' + val + '/' + valMax,
        paddedstring = label + Array(totalwidth - label.length + 1 - poststring.length).join(' ') + poststring;

    mywin.cursor(rowStart,3);
    mywin.attron(nc.colorPair(1));
    for (var i = 0; i < paddedstring.length; i++){
        if(i > onWidth){
            mywin.attroff(nc.colorPair(1));
            mywin.attron(nc.colorPair(2));
        }
        mywin.addstr(paddedstring.charAt(i));
    }
    mywin.attroff(nc.colorPair(2));
    mywin.refresh();
}
process.on('SIGWINCH',drawFn);
<|endoftext|>