├── .nvmrc
├── .gitignore
├── src
├── util.imba
├── component.imba
├── terminal.imba
├── packet.imba
├── open-editor.imba
├── wss.imba
├── terminal
│ ├── buffer.imba
│ ├── runner.imba
│ ├── model.imba
│ └── terminal.imba
├── helpers.imba
├── fs.imba
└── git.imba
├── vendor
├── hterm_vt.concat
└── bashrc
├── deploy.imba
├── extwindow.js
├── machine.js
├── lib
├── util.js
├── cli.js
├── component.js
├── terminal.js
├── open-editor.js
├── index.js
├── packet.js
├── wss.js
├── terminal
│ ├── buffer.js
│ ├── runner.js
│ ├── model.js
│ └── terminal.js
├── helpers.js
├── fs.js
└── git.js
├── README.md
├── index.html
├── splash.html
├── appveyor.yml
├── logo.svg
├── .travis.yml
├── preload.js
├── menu.js
├── package.json
├── LICENSE.md
└── main.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | v12.4.0
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #directories
2 | bower_components
3 | node_modules
4 | dist
5 | release-builds
6 |
7 | #files
8 | *.tgz
9 | *.log
10 | .DS_Store
--------------------------------------------------------------------------------
/src/util.imba:
--------------------------------------------------------------------------------
1 | var crypto = require("crypto")
2 |
3 | export def randomId len = 8
4 | return crypto.randomBytes(32).toString('base64').replace(/[^\w]/g,'').slice(0,len)
5 | return crypto.randomBytes(32).toString("hex")
6 |
7 | export def split
8 | no
9 |
10 | export def other
11 | no
12 |
13 | export def countLines str
14 | (str.match(/\r?\n/g) || ''):length + 1
--------------------------------------------------------------------------------
/vendor/hterm_vt.concat:
--------------------------------------------------------------------------------
1 |
2 | libdot/js/lib.js
3 | libdot/js/lib_colors.js
4 | libdot/js/lib_f.js
5 | libdot/js/lib_utf8.js
6 |
7 | hterm/js/hterm.js
8 | @echo hterm.Keyboard = { KeyActions: {} };
9 | hterm/js/hterm_parser.js
10 | hterm/js/hterm_text_attributes.js
11 | hterm/js/hterm_vt.js
12 | hterm/js/hterm_vt_character_map.js
13 |
14 | @echo exports.lib = lib; exports.hterm = hterm;
15 |
16 |
--------------------------------------------------------------------------------
/deploy.imba:
--------------------------------------------------------------------------------
1 | var pkg = require('./package.json')
2 | var cp = require 'child_process'
3 | console.log "deploy version {pkg:version}"
4 |
5 | def exec cmd
6 | console.log "exec: {cmd}"
7 | cp.execSync(cmd)
8 |
9 | # tag the version
10 |
11 | exec("git push origin --tags")
12 | exec("git push origin master:release")
13 | exec("hub release create -d -m \"v{pkg:version}\" v{pkg:version}")
14 | # now create the release on github
15 |
--------------------------------------------------------------------------------
/vendor/bashrc:
--------------------------------------------------------------------------------
1 | if [ -f $HOME/.profile ]; then
2 | source $HOME/.profile
3 | elif [ -f $HOME/.bash_profile ]; then
4 | source $HOME/.bash_profile
5 | elif [ -f $HOME/.bashrc ]; then
6 | source $HOME/.bashrc
7 | fi
8 |
9 | export CLICOLOR=1
10 |
11 | # export PS1="\W:"
12 | # export SCRIMBA_TEST="\W:"
13 |
14 | LS_COLORS=$LS_COLORS:'di=0;35:';
15 | export LS_COLORS
16 |
17 | # ls --color=auto &> /dev/null && alias ls='ls --color=auto' ||
18 |
19 | export PS1="\[$(tput setaf 5)\]\[$(tput bold)\]\W$ \[$(tput sgr0)\]"
20 | alias ls='ls -F -G'
--------------------------------------------------------------------------------
/extwindow.js:
--------------------------------------------------------------------------------
1 | const {ipcRenderer} = require('electron');
2 | const remote = require('@electron/remote');
3 | const remoteMain = remote.require("@electron/remote/main");
4 | remoteMain.enable(window.webContents);
5 |
6 | window.GitSpeak = {
7 | ipc: {
8 | on(channel,cb){
9 | ipcRenderer.on(channel,cb);
10 | },
11 |
12 | send(channel,args){
13 | ipcRenderer.send(channel,args);
14 | },
15 |
16 | getGitInfo(dir) {
17 | return machine.getGitInfo(dir);
18 | },
19 | },
20 |
21 | win: remote.getCurrentWindow()
22 | };
--------------------------------------------------------------------------------
/machine.js:
--------------------------------------------------------------------------------
1 |
2 | // const {app, BrowserWindow,Tray,Menu,session, protocol,ipcMain} = require('electron')
3 | const {Notification} = require('electron')
4 | const {getGitInfo, getGitBlob, getGitTree, getGitDiff} = require('./lib/git');
5 | const {openEditor, getAvailableEditors} = require('./lib/open-editor');
6 | exports.getGitInfo = getGitInfo;
7 | exports.getGitBlob = getGitBlob;
8 | exports.getGitTree = getGitTree;
9 | exports.getGitDiff = getGitDiff;
10 | exports.Notification = Notification;
11 | exports.openEditor = openEditor;
12 | exports.getAvailableEditors = getAvailableEditors;
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | var self = {};
2 | var crypto = require("crypto");
3 |
4 | exports.randomId = self.randomId = function (len){
5 | if(len === undefined) len = 8;
6 | return crypto.randomBytes(32).toString('base64').replace(/[^\w]/g,'').slice(0,len);
7 | return crypto.randomBytes(32).toString("hex");
8 | };
9 |
10 | exports.split = self.split = function (){
11 | return false;
12 | };
13 |
14 | exports.other = self.other = function (){
15 | return false;
16 | };
17 |
18 | exports.countLines = self.countLines = function (str){
19 | return (str.match(/\r?\n/g) || '').length + 1;
20 | };
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A powerful client for GitHub repositories
2 |
3 | ### ...without asking for access to your codebase
4 |
5 | Our goal is to make it easier for your team to discuss code. Through this desktop app, you'll be able to easily manage your Issues and Pull Requests, while also getting powerful tools to talk about your codebase.
6 |
7 | Some of the benefits of using GitSpeak:
8 |
9 | - Stellar writing experience
10 | - Record audiovisual code walk-throughs
11 | - Attach contextual code snippets
12 | - Ultrafast navigation
13 | - Simple interface
14 |
15 | Head over to [GitSpeak.com](https://gitspeak.com/) to learn more!
16 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello World!
6 |
7 |
8 | Hello World!
9 |
10 | We are using Node.js ,
11 | Chromium ,
12 | and Electron .
13 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/component.imba:
--------------------------------------------------------------------------------
1 | var counter = 1
2 | import randomId from './util'
3 |
4 | export class Component
5 |
6 | prop owner
7 |
8 | def emit name, *params do Imba.emit(self,name,params)
9 | def on name, *params do Imba.listen(self,name,*params)
10 | def once name, *params do Imba.once(self,name,*params)
11 | def un name, *params do Imba.unlisten(self,name,*params)
12 |
13 | def ref
14 | @ref ||= randomId()
15 |
16 | def log *params
17 | @owner.log(*params)
18 | self
19 |
20 | def timeouts
21 | @timeouts ||= {}
22 |
23 | def delay fn, time
24 | clearTimeout(timeouts[fn])
25 | timeouts[fn] = setTimeout(self[fn].bind(self),time)
26 | return self
27 |
28 | def toJSON
29 | {ref: ref}
30 |
31 | def dispose
32 | self
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | var helpers = require('./helpers');
2 | var pkg = require('../package.json');
3 |
4 | var parseOpts = {
5 | alias: {h: 'help',v: 'version',e: 'eval'},
6 | schema: {eval: {type: 'string'}}
7 | };
8 |
9 |
10 | var help = "Usage: gitpeak [options] [start]\n \n --debug\n --dev start with custom host\n -h, --help display this help message\n -v, --version display the version number\n";
11 |
12 | var Socket = require('./socket').Socket;
13 |
14 | function run(){
15 | var args = process.argv;
16 | var o = helpers.parseArgs(args.slice(2),parseOpts);
17 |
18 | if (o.version) {
19 | return console.log(pkg.version);
20 | } else if ((!o.main && !o.eval) || o.help) {
21 | return console.log(help);
22 | };
23 |
24 | o.root = process.cwd();
25 | o.version = pkg.version;
26 | var socket = new Socket(o);
27 | return socket.start();
28 | }; exports.run = run;
29 |
--------------------------------------------------------------------------------
/splash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | GitSpeak
6 |
32 |
33 |
34 |
35 | Starting up...
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # comment
2 | image: Visual Studio 2017
3 |
4 | environment:
5 | nodejs_version: "12"
6 |
7 | # Post-install test scripts!
8 | test_script:
9 | # Output useful info for debugging
10 | - node --version
11 | - npm --version
12 | # run tests
13 | - npm test
14 |
15 | platform:
16 | - x64
17 |
18 | cache:
19 | - node_modules
20 | - '%USERPROFILE%\.electron'
21 |
22 | init:
23 | - git config --global core.autocrlf input
24 |
25 | install:
26 | - ps: Install-Product node 12 x64
27 | # - ps: $env:package_version = "v" + (Get-Content -Raw -Path package.json | ConvertFrom-JSON).version
28 | # - ps: echo $env:package_version
29 | # - ps: Update-AppveyorBuild -Version "$env:package_version:APPVEYOR_BUILD_NUMBER"
30 | - npm uninstall node-pty --save
31 | - npm install
32 |
33 | build_script:
34 | - npm run build --win
35 |
36 | branches:
37 | only:
38 | - release
39 |
40 | # If we want AppVeyor to handle deployment instead of electron-builder
41 | # artifacts:
42 | # - path: dist
43 | # name: gitspeak-desktop
44 |
45 | # deploy:
46 | # provider: GitHub
47 | # artifact: gitspeak-desktop
48 | # auth_token:
49 | # secure: 0EFPhJviDyp/yQqq+6te+ph61+3vCnmAcK7LUrHHY9a7ECXDTZqKIqDgszMkyo4B
50 | # on:
51 | # branch: release
52 |
53 |
54 | test: off
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | osx_image: xcode10.1
2 |
3 | dist: trusty
4 | sudo: false
5 |
6 | language: node_js
7 | node_js: "12.4.0"
8 |
9 | env:
10 | global:
11 | - ELECTRON_CACHE=$HOME/.cache/electron
12 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
13 |
14 | os:
15 | - linux
16 | - osx
17 |
18 | cache:
19 | directories:
20 | - node_modules
21 | - $HOME/.cache/electron
22 | - $HOME/.cache/electron-builder
23 | - $HOME/.npm/_prebuilds
24 |
25 | before_install:
26 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v2.2.0/git-lfs-$([ "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-2.2.0.tar.gz | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull
27 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm install --linux; fi
28 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm install --mac; fi
29 |
30 | script:
31 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then npm run build --linux; fi
32 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then npm run build --mac; fi
33 |
34 | before_cache:
35 | - rm -rf $HOME/.cache/electron-builder/wine
36 |
37 | branches:
38 | only:
39 | - release
40 |
41 | # deploy:
42 | # provider: releases
43 | # api_key: "$GH_TOKEN"
44 | # skip_cleanup: true
45 | # on:
46 | # branch: release
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/terminal.imba:
--------------------------------------------------------------------------------
1 |
2 | import Component from './component'
3 | import Runner from './terminal/runner'
4 | import randomId from './util'
5 |
6 | var os = require 'os'
7 | var path = require 'path'
8 | var shell = os.platform === 'win32' ? 'powershell.exe' : 'bash'
9 |
10 | # For each bash/terminal session we spawn an instance of Terminal
11 | export class Terminal < Component
12 |
13 | prop ref
14 |
15 | def initialize owner, options
16 | @owner = owner
17 | @options = options
18 | @options:width ||= 100
19 | @options:height ||= 24
20 |
21 | @ref = options:id or randomId()
22 | @runner = Runner.new(self,options)
23 |
24 | unless @runner.isSupported
25 | @owner?.err("Cannot initiate terminal")
26 | emit('error',"Cannot initiate")
27 | @owner?.send({type: 'error', data: "Cannot initiate"})
28 | return self
29 |
30 | Imba.listen(@runner,'message') do |msg|
31 | msg:width = options:width
32 | msg:height = options:height
33 | emit('change', msg)
34 | @owner?.send({type: 'change', data: msg})
35 | # @owner.send(type: 'terminal.change', ref: ref, data: msg)
36 |
37 | let pars = []
38 | if shell == 'bash'
39 | let bashrc = path.resolve(__dirname,'..','vendor','bashrc')
40 | pars = ['--rcfile',bashrc]
41 |
42 | setTimeout(&,20) do
43 | @runner.run(shell,pars)
44 | self
45 |
46 | def log *params
47 | @owner?.log(*params)
48 |
49 | def write string
50 | @runner?.write(string)
51 |
52 | def dispose
53 | self
--------------------------------------------------------------------------------
/src/packet.imba:
--------------------------------------------------------------------------------
1 | var msgpack = require 'msgpack-lite'
2 | var Bufferish = msgpack.Decoder:prototype:bufferish
3 |
4 | export var MSG =
5 | CONNECTED: 13
6 | CREATE_SCRIM: 29
7 | CREATED_SCRIM: 30
8 |
9 |
10 | module pack
11 | def concat buffers
12 | Bufferish.concat(buffers)
13 |
14 | def encode *chunks
15 | var encoder = msgpack.Encoder.new({})
16 | encoder.write(item) for item in chunks
17 | return encoder.read
18 |
19 | export class Packet
20 |
21 | prop params
22 | prop socket
23 | prop data
24 |
25 | def self.serialize data
26 | if $node$
27 | return data isa Array ? pack.encode(data) : data
28 |
29 | def initialize data, socket
30 | @socket = socket
31 |
32 | if $node$
33 | @data = Buffer.from(data)
34 | else
35 | @data = Uint8Array.new(data)
36 |
37 | var decoder = msgpack.Decoder.new
38 | decoder.write(@data)
39 | @params = decoder.fetch
40 |
41 | if @params isa Number
42 | let offset = decoder:offset
43 | @ref = @params
44 | @data = ($web$ ? @data.subarray(offset) : @data.slice(offset))
45 | @params = decoder.fetch
46 | @offset = decoder:offset - offset
47 | else
48 | @offset = decoder:offset
49 |
50 | self.CODE = @params[0]
51 | self[1] = @params[1]
52 | self[2] = @params[2]
53 | self[3] = @params[3]
54 | self[4] = @params[4]
55 | self
56 |
57 | def payloadSize
58 | @data:byteLength - @offset
59 |
60 | def peer
61 | socket
62 |
63 | def retain
64 | # packets are inited with data from an arraybuffer
65 | # that is reused across requests. If we do anything
66 | # asynchronous with the data, we need to retain it
67 |
68 | if $node$
69 | @data = Buffer.from(@data)
70 | @payload = null
71 | self
72 |
73 | def payload
74 | if $node$
75 | @payload ||= @data.slice(@offset)
76 | else
77 | @payload ||= @data.subarray(@offset)
78 |
79 | def reply msg
80 | msg = Packet.prepare(msg)
81 | if @ref
82 | msg = Bufferish.concat([msgpack.encode(@ref),msg])
83 | socket.send(msg)
84 | self
85 |
86 | def sid
87 | socket:sid
88 |
89 | def pid
90 | socket:id
91 |
--------------------------------------------------------------------------------
/lib/component.js:
--------------------------------------------------------------------------------
1 | var Imba = require('imba');
2 | var counter = 1;
3 | var randomId = require('./util').randomId;
4 |
5 | function Component(){ };
6 |
7 | exports.Component = Component; // export class
8 | Component.prototype.owner = function(v){ return this._owner; }
9 | Component.prototype.setOwner = function(v){ this._owner = v; return this; };
10 |
11 | Component.prototype.emit = function (name){
12 | var $0 = arguments, i = $0.length;
13 | var params = new Array(i>1 ? i-1 : 0);
14 | while(i>1) params[--i - 1] = $0[i];
15 | return Imba.emit(this,name,params);
16 | };
17 | Component.prototype.on = function (name){
18 | var Imba_;
19 | var $0 = arguments, i = $0.length;
20 | var params = new Array(i>1 ? i-1 : 0);
21 | while(i>1) params[--i - 1] = $0[i];
22 | return Imba.listen.apply(Imba,[].concat([this,name], [].slice.call(params)));
23 | };
24 | Component.prototype.once = function (name){
25 | var Imba_;
26 | var $0 = arguments, i = $0.length;
27 | var params = new Array(i>1 ? i-1 : 0);
28 | while(i>1) params[--i - 1] = $0[i];
29 | return Imba.once.apply(Imba,[].concat([this,name], [].slice.call(params)));
30 | };
31 | Component.prototype.un = function (name){
32 | var Imba_;
33 | var $0 = arguments, i = $0.length;
34 | var params = new Array(i>1 ? i-1 : 0);
35 | while(i>1) params[--i - 1] = $0[i];
36 | return Imba.unlisten.apply(Imba,[].concat([this,name], [].slice.call(params)));
37 | };
38 |
39 | Component.prototype.ref = function (){
40 | return this._ref || (this._ref = randomId());
41 | };
42 |
43 | Component.prototype.log = function (){
44 | var $0 = arguments, i = $0.length;
45 | var params = new Array(i>0 ? i : 0);
46 | while(i>0) params[i-1] = $0[--i];
47 | this._owner.log.apply(this._owner,params);
48 | return this;
49 | };
50 |
51 | Component.prototype.timeouts = function (){
52 | return this._timeouts || (this._timeouts = {});
53 | };
54 |
55 | Component.prototype.delay = function (fn,time){
56 | clearTimeout(this.timeouts()[fn]);
57 | this.timeouts()[fn] = setTimeout(this[fn].bind(this),time);
58 | return this;
59 | };
60 |
61 | Component.prototype.toJSON = function (){
62 | return {ref: this.ref()};
63 | };
64 |
65 | Component.prototype.dispose = function (){
66 | return this;
67 | };
68 |
--------------------------------------------------------------------------------
/src/open-editor.imba:
--------------------------------------------------------------------------------
1 | import Component from './component'
2 | var cp = require 'child_process'
3 | var fs = require 'fs'
4 |
5 | const EDITOR = {
6 | code: {
7 | name: "Visual Studio Code"
8 | linux: "/usr/bin/code"
9 | darwin: "/Applications/Visual\\ Studio\\ Code.app/Contents/Resources/app/bin/code"
10 | }
11 | sublime: {
12 | name: "Sublime Text"
13 | linux: "/usr/bin/subl"
14 | darwin: "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl"
15 | }
16 | }
17 |
18 | def get_cmd_fmt editor,file,line
19 | const type = EDITOR["{editor}"]
20 | if not type
21 | return { error: "Unknown editor" }
22 | const bin = type["{process:platform}"]
23 | if not bin
24 | return { error: "Unsupported platform" }
25 |
26 | switch editor
27 | when "code"
28 | "{bin} -g {file}:{line}"
29 | when "sublime"
30 | "{bin} {file}:{line}"
31 |
32 | ###
33 | The line might have changed since it became a snippet.
34 | Try to use git blame to find the correct line.
35 | ###
36 | def normalizeLine path,line,gitref,repoPath
37 | try
38 | const cmd = "git blame -L {line},{line} -n --reverse {gitref}..head -- {path}"
39 | const output = try cp.execSync(cmd, cwd: repoPath, env: process:env).toString()
40 | # Uncomment the below line to look at the output format and command
41 | console.log "cmd",cmd,"output",output
42 | if output
43 | return output.split(' ')[1]
44 | console.log "normalize failed"
45 |
46 | export def openEditor data
47 | const startLine = data:startLine
48 | const repoPath = data:repoPath
49 | const absPath = data:absPath
50 | const gitref = data:gitref
51 | const editor = data:editor
52 | const path = data:path
53 |
54 | const normalize = normalizeLine(path, startLine, gitref, repoPath)
55 | let line = normalize ? normalize : startLine
56 | const cmd = get_cmd_fmt(editor, absPath, line)
57 | if not cmd:error
58 | cp.execSync(cmd, cwd: repoPath, env: process:env)
59 | else
60 | # TODO: give the user feedback
61 | cmd:error
62 |
63 | export def getAvailableEditors
64 | let m = Object.keys(EDITOR).map do |x|
65 | const editor = EDITOR[x]
66 | const bin = editor["{process:platform}"]
67 | if fs.existsSync(bin.replace(/\\/g, ""))
68 | { name: "{EDITOR[x]:name}", identifier: x }
69 | m.filter(Boolean)
--------------------------------------------------------------------------------
/lib/terminal.js:
--------------------------------------------------------------------------------
1 | var Imba = require('imba');
2 |
3 | var Component = require('./component').Component;
4 | var Runner = require('./terminal/runner').Runner;
5 | var randomId = require('./util').randomId;
6 |
7 | var os = require('os');
8 | var path = require('path');
9 | var shell = (os.platform() === 'win32') ? 'powershell.exe' : 'bash';
10 |
11 | // For each bash/terminal session we spawn an instance of Terminal
12 | function Terminal(owner,options){
13 | var self = this;
14 | self._owner = owner;
15 | self._options = options;
16 | self._options.width || (self._options.width = 100);
17 | self._options.height || (self._options.height = 24);
18 |
19 | self._ref = options.id || randomId();
20 | self._runner = new Runner(self,options);
21 |
22 | if (!self._runner.isSupported()) {
23 | self._owner && self._owner.err && self._owner.err("Cannot initiate terminal");
24 | self.emit('error',"Cannot initiate");
25 | self._owner && self._owner.send && self._owner.send({type: 'error',data: "Cannot initiate"});
26 | return self;
27 | };
28 |
29 | Imba.listen(self._runner,'message',function(msg) {
30 | msg.width = options.width;
31 | msg.height = options.height;
32 | self.emit('change',msg);
33 | return self._owner && self._owner.send && self._owner.send({type: 'change',data: msg});
34 | // @owner.send(type: 'terminal.change', ref: ref, data: msg)
35 | });
36 |
37 | let pars = [];
38 | if (shell == 'bash') {
39 | let bashrc = path.resolve(__dirname,'..','vendor','bashrc');
40 | pars = ['--rcfile',bashrc];
41 | };
42 |
43 | setTimeout(function() {
44 | return self._runner.run(shell,pars);
45 | },20);
46 | self;
47 | };
48 |
49 | Imba.subclass(Terminal,Component);
50 | exports.Terminal = Terminal; // export class
51 | Terminal.prototype.ref = function(v){ return this._ref; }
52 | Terminal.prototype.setRef = function(v){ this._ref = v; return this; };
53 |
54 | Terminal.prototype.log = function (){
55 | var $0 = arguments, i = $0.length;
56 | var params = new Array(i>0 ? i : 0);
57 | while(i>0) params[i-1] = $0[--i];
58 | return this._owner && this._owner.log && this._owner.log.apply(this._owner,params);
59 | };
60 |
61 | Terminal.prototype.write = function (string){
62 | return this._runner && this._runner.write && this._runner.write(string);
63 | };
64 |
65 | Terminal.prototype.dispose = function (){
66 | return this;
67 | };
68 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | // ==============
2 | // Preload script
3 | // ==============
4 | const {ipcRenderer,shell} = require('electron');
5 | const remote = require('@electron/remote');
6 |
7 | const machine = remote.require('./machine')
8 |
9 | window.interop = {
10 |
11 | getGitInfo(dir) {
12 | return machine.getGitInfo(dir);
13 | },
14 |
15 | selectDirectory(o) {
16 | var opts = Object.assign({
17 | title: "Open folder...",
18 | message: "Open folder...",
19 | properties: ['openDirectory']
20 | },o || {});
21 | var res = remote.dialog.showOpenDialogSync(remote.getCurrentWindow(),opts);
22 | return res && res[0];
23 | },
24 |
25 | ipcSend(channel,args){
26 | ipcRenderer.send(channel,args);
27 | },
28 |
29 | ipcListen(channel,cb){
30 | ipcRenderer.on(channel,cb);
31 | },
32 |
33 | ipc: {
34 | on(channel,cb){
35 | ipcRenderer.on(channel,cb);
36 | },
37 |
38 | send(channel,args){
39 | ipcRenderer.send(channel,args);
40 | },
41 |
42 | openExternal(url){
43 | shell.openExternal(url);
44 | },
45 |
46 | openEditor(data) {
47 | return machine.openEditor(data)
48 | },
49 |
50 | getAvailableEditors() {
51 | return machine.getAvailableEditors()
52 | },
53 |
54 | getSync(key){
55 | return ipcRenderer.sendSync('state.get',key)
56 | },
57 |
58 | getGitInfo(dir) {
59 | return machine.getGitInfo(dir);
60 | },
61 |
62 | fstat(dir) {
63 | return ipcRenderer.sendSync('fstat',dir)
64 | // return machine.fstat(dir);
65 | },
66 |
67 | tunnelUrl(){
68 | let port = this.getSync('tunnelPort');
69 | return "ws://127.0.0.1:" + port;
70 | },
71 |
72 | setBadgeCount(count) {
73 | return remote.app.setBadgeCount(count);
74 | },
75 |
76 | getGitBlob(localDirectory, sha, refToFetch) {
77 | return machine.getGitBlob(localDirectory, sha, refToFetch)
78 | },
79 |
80 | getGitTree(localDirectory, sha, refToFetch) {
81 | return machine.getGitTree(localDirectory, sha, refToFetch)
82 | },
83 |
84 | getGitDiff(localDirectory, base, head, includePatch) {
85 | return machine.getGitDiff(localDirectory, base, head, includePatch)
86 | },
87 |
88 | sendNotification(data){
89 | new machine.Notification(data).show();
90 | },
91 | },
92 |
93 | win: remote.getCurrentWindow()
94 | };
--------------------------------------------------------------------------------
/menu.js:
--------------------------------------------------------------------------------
1 | const {app, BrowserWindow,Tray,Menu,session, protocol,ipcMain, clipboard,dialog} = require('electron');
2 |
3 | var template
4 | exports.template = template = [
5 | // {
6 | // label: 'File',
7 | // submenu: [
8 | // {label: "Open...", click: browseDirectory},
9 | // {
10 | // role: 'recentdocuments',
11 | // submenu: [
12 | // {
13 | // role: 'clearrecentdocuments'
14 | // }
15 | // ]
16 | // }
17 | // ]
18 | // },
19 | {
20 | label: 'Edit',
21 | submenu: [
22 | {role: 'undo'},
23 | {role: 'redo'},
24 | {type: 'separator'},
25 | {role: 'cut'},
26 | {role: 'copy'},
27 | {role: 'paste'},
28 | {role: 'pasteandmatchstyle'},
29 | {role: 'delete'},
30 | {role: 'selectall'}
31 | ]
32 | },
33 | {
34 | label: 'View',
35 | submenu: [
36 | {role: 'reload'},
37 | {role: 'forcereload'},
38 | {role: 'toggledevtools'},
39 | {type: 'separator'},
40 | {role: 'resetzoom'},
41 | {role: 'zoomin'},
42 | {role: 'zoomout'},
43 | {type: 'separator'},
44 | {role: 'togglefullscreen'}
45 | ]
46 | },
47 | {
48 | role: 'window',
49 | submenu: [
50 | {role: 'minimize'},
51 | {role: 'close'}
52 | ]
53 | },
54 | {
55 | role: 'help',
56 | submenu: [
57 | {
58 | label: 'Learn More',
59 | click () { require('electron').shell.openExternal('https://gitspeak.com') }
60 | }
61 | ]
62 | }
63 | ]
64 |
65 | if (process.platform === 'darwin') {
66 | template.unshift({
67 | label: app.getName(),
68 | submenu: [
69 | {role: 'about'},
70 | {type: 'separator'},
71 | {role: 'services', submenu: []},
72 | {type: 'separator'},
73 | {role: 'hide'},
74 | {role: 'hideothers'},
75 | {role: 'unhide'},
76 | {type: 'separator'},
77 | {role: 'quit'}
78 | ]
79 | })
80 |
81 | // Edit menu
82 | template[1].submenu.push(
83 | {type: 'separator'},
84 | {
85 | label: 'Speech',
86 | submenu: [
87 | {role: 'startspeaking'},
88 | {role: 'stopspeaking'}
89 | ]
90 | }
91 | )
92 |
93 | // Window menu
94 | template[3].submenu = [
95 | {role: 'close'},
96 | {role: 'minimize'},
97 | {role: 'zoom'},
98 | {type: 'separator'},
99 | {role: 'front'}
100 | ]
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/lib/open-editor.js:
--------------------------------------------------------------------------------
1 | var self = {};
2 | var Component = require('./component').Component;
3 | var cp = require('child_process');
4 | var fs = require('fs');
5 |
6 | const EDITOR = {
7 | code: {
8 | name: "Visual Studio Code",
9 | linux: "/usr/bin/code",
10 | darwin: "/Applications/Visual\\ Studio\\ Code.app/Contents/Resources/app/bin/code"
11 | },
12 | sublime: {
13 | name: "Sublime Text",
14 | linux: "/usr/bin/subl",
15 | darwin: "/Applications/Sublime\\ Text.app/Contents/SharedSupport/bin/subl"
16 | }
17 | };
18 |
19 | self.get_cmd_fmt = function (editor,file,line){
20 | const type = EDITOR[("" + editor)];
21 | if (!type) {
22 | return {error: "Unknown editor"};
23 | };
24 | const bin = type[("" + (process.platform))];
25 | if (!bin) {
26 | return {error: "Unsupported platform"};
27 | };
28 |
29 | switch (editor) {
30 | case "code": {
31 | return ("" + bin + " -g " + file + ":" + line);
32 | break;
33 | }
34 | case "sublime": {
35 | return ("" + bin + " " + file + ":" + line);
36 | break;
37 | }
38 | };
39 | };
40 |
41 | /*
42 | The line might have changed since it became a snippet.
43 | Try to use git blame to find the correct line.
44 | */
45 |
46 | self.normalizeLine = function (path,line,gitref,repoPath){
47 | try {
48 | const cmd = ("git blame -L " + line + "," + line + " -n --reverse " + gitref + "..head -- " + path);
49 | try {
50 | const output = cp.execSync(cmd,{cwd: repoPath,env: process.env}).toString();
51 | } catch (e) { };
52 | // Uncomment the below line to look at the output format and command
53 | console.log("cmd",cmd,"output",output);
54 | if (output) {
55 | return output.split(' ')[1];
56 | };
57 | } catch (e) { };
58 | return console.log("normalize failed");
59 | };
60 |
61 | exports.openEditor = self.openEditor = function (data){
62 | const startLine = data.startLine;
63 | const repoPath = data.repoPath;
64 | const absPath = data.absPath;
65 | const gitref = data.gitref;
66 | const editor = data.editor;
67 | const path = data.path;
68 |
69 | const normalize = self.normalizeLine(path,startLine,gitref,repoPath);
70 | let line = normalize ? normalize : startLine;
71 | const cmd = self.get_cmd_fmt(editor,absPath,line);
72 | if (!cmd.error) {
73 | return cp.execSync(cmd,{cwd: repoPath,env: process.env});
74 | } else {
75 | // TODO: give the user feedback
76 | return cmd.error;
77 | };
78 | };
79 |
80 | exports.getAvailableEditors = self.getAvailableEditors = function (){
81 | let m = Object.keys(EDITOR).map(function(x) {
82 | const editor = EDITOR[x];
83 | const bin = editor[("" + (process.platform))];
84 | if (fs.existsSync(bin.replace(/\\/g,""))) {
85 | return {name: ("" + (EDITOR[x].name)),identifier: x};
86 | };
87 | });
88 | return m.filter(Boolean);
89 | };
90 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | var t = Date.now();
2 | var pkg = require('../package.json');
3 | var ga = require('./ga');
4 |
5 | const program = require('commander');
6 |
7 | const fs = require('fs');
8 | const path = require('path');
9 | const ora = require('ora');
10 | var chalk = require('chalk');
11 | const resolve = require('path').resolve;
12 |
13 | // const inquirer = require('inquirer');
14 | // const inquirer_path = require('inquirer-path');
15 | // inquirer.prompt.registerPrompt('path', inquirer_path.PathPrompt);
16 |
17 | var logger = console;
18 | var Socket = require('./socket').Socket;
19 | var development = false;
20 |
21 | function run(){
22 | // console.log("loaded in ",Date.now() - t);
23 | // Detects dev mode and removes dev flag from args
24 | // (CLI frameworks doesn't like undefined flags)
25 | // if(process.argv.includes('--dev')) {
26 | // development = true;
27 | // process.argv = process.argv.filter((item) => {
28 | // if(item == '--dev')
29 | // return false;
30 | // return true;
31 | // })
32 | // }
33 |
34 | program
35 | .version(pkg.version)
36 | .description(pkg.description)
37 | // .command('start',{isDefault: true})
38 | .arguments('')
39 | .action((folder, options) => {
40 | if(folder == 'start'){
41 | // console.log("just start",program.args);
42 | folder = '.';
43 | }
44 |
45 | let args = {folder: folder}
46 | if(args.folder != undefined) {
47 | let abs = resolve(args.folder);
48 | if(!fs.existsSync(abs)) {
49 | logger.error(chalk.red("You need to supply a valid folder"));
50 | program.outputHelp();
51 | } else {
52 | start(abs, logger, {command: 'start'});
53 | }
54 | return;
55 | } else {
56 | program.outputHelp();
57 | }
58 | // disabled for now
59 | // directoryPrompt((directory) => { start(directory, logger) });
60 | });
61 |
62 | // program
63 | // .command('commit')
64 | // .action(() => {
65 | // console.log("action here!");
66 | // start('.', logger,{command: 'commit'});
67 | // })
68 |
69 | program.parse(process.argv);
70 |
71 | if (!program.args.length){
72 | program.outputHelp();
73 | }
74 |
75 | };
76 |
77 | function directoryPrompt(cb) {
78 | const basePath = process.cwd();
79 |
80 | inquirer.prompt([
81 | {
82 | "name": "directory",
83 | "directoryOnly": true,
84 | "type": "path",
85 | "cwd": basePath,
86 | "message": "What directory would you like to screencast from?",
87 | "default": basePath
88 | }
89 | ])
90 | .then(answers => {
91 | if(answers.directory) {
92 | cb(answers.directory);
93 | }
94 | });
95 |
96 | }
97 |
98 | function start(dir, logger, o) {
99 | // Set a small delay so the user has a chance to
100 | // cancel if they feel like they need to -
101 | // nothing is uploaded or anything, so should not be needed
102 | setTimeout(() => {
103 | dir = resolve(dir);
104 | o = o || {};
105 | o.logger = logger;
106 | o.root = dir;
107 | // o.dev = development;
108 | o.version = pkg.version;
109 | ga.sendEvent('CLI', 'start', o.version);
110 | var socket = new Socket(o);
111 | return socket.start();
112 | }, 100);
113 | }
114 |
115 | exports.run = run;
116 |
117 | run();
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitspeak",
3 | "version": "0.2.35",
4 | "productName": "GitSpeak",
5 | "description": "Desktop client for GitSpeak",
6 | "main": "main.js",
7 | "author": "Sindre Aarsæther (https://gitspeak.com)",
8 | "homepage": "https://gitspeak.com",
9 | "email": "hello@scrimba.com",
10 | "scripts": {
11 | "start": "electron .",
12 | "start-dev": "open node_modules/electron/dist/Electron.app --args $PWD",
13 | "pack": "electron-builder --dir",
14 | "dist": "electron-builder",
15 | "prepare-win": "npm uninstall node-pty --save",
16 | "build": "electron-builder --publish always",
17 | "rebuild": "electron-rebuild -f -w node-pty",
18 | "watch": "imbac -w -o lib src",
19 | "postinstall": "electron-builder install-app-deps",
20 | "deploy": "imba deploy.imba"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/gitspeak/gitspeak-desktop.git"
25 | },
26 | "keywords": [
27 | "git",
28 | "gitspeak",
29 | "github",
30 | "pr",
31 | "liveshare",
32 | "commit"
33 | ],
34 | "license": "CC0-1.0",
35 | "devDependencies": {
36 | "electron": "^17.1.0",
37 | "electron-builder": "^22.14.13",
38 | "electron-packager": "^12.2.0",
39 | "electron-rebuild": "^1.8.6"
40 | },
41 | "build": {
42 | "appId": "com.gitspeak.app",
43 | "asar": false,
44 | "publish": {
45 | "provider": "github",
46 | "owner": "gitspeak"
47 | },
48 | "directories": {
49 | "buildResources": "./build"
50 | },
51 | "mac": {
52 | "target": [
53 | {
54 | "target": "default",
55 | "arch": "universal"
56 | }
57 | ],
58 | "icon": "./build/icon.icns",
59 | "category": "your.app.category.type"
60 | },
61 | "win": {
62 | "target": "NSIS",
63 | "icon": "./build/icon.ico",
64 | "publisherName": "Scrimba AS",
65 | "rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
66 | },
67 | "nsis": {
68 | "installerIcon": "./build/icon.ico",
69 | "createDesktopShortcut": "always",
70 | "createStartMenuShortcut": true,
71 | "artifactName": "${productName}Setup.${ext}"
72 | },
73 | "dmg": {
74 | "icon": "./build/icon.icns",
75 | "artifactName": "${productName}Setup.${ext}"
76 | },
77 | "appImage": {
78 | "artifactName": "${productName}Setup.${ext}"
79 | },
80 | "linux": {
81 | "target": [
82 | "AppImage"
83 | ],
84 | "category": "Development"
85 | },
86 | "protocols": [
87 | {
88 | "name": "gitspeak",
89 | "role": "Viewer",
90 | "schemes": [
91 | "gitspeak"
92 | ]
93 | }
94 | ]
95 | },
96 | "dependencies": {
97 | "@electron/remote": "^2.0.5",
98 | "electron-log": "^2.2.17",
99 | "electron-updater": "^4.0.3",
100 | "find-free-port": "^2.0.0",
101 | "fix-path": "^2.1.0",
102 | "git-repo-info": "^2.0.0",
103 | "hosted-git-info": "^2.7.1",
104 | "imba": "^1.4.1",
105 | "install": "^0.12.2",
106 | "isbinaryfile": "^3.0.3",
107 | "msgpack-lite": "^0.1.26",
108 | "node-watch": "^0.5.8",
109 | "parse-git-config": "^2.0.3",
110 | "simple-git": "^1.106.0",
111 | "ws": "^6.1.0"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/packet.js:
--------------------------------------------------------------------------------
1 | function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; };
2 | var msgpack = require('msgpack-lite');
3 | var Bufferish = msgpack.Decoder.prototype.bufferish;
4 |
5 | var MSG = exports.MSG = {
6 | CONNECTED: 13,
7 | CREATE_SCRIM: 29,
8 | CREATED_SCRIM: 30
9 | };
10 |
11 |
12 | var pack = (function($mod$){
13 | $mod$.concat = function (buffers){
14 | "use strict";
15 | var self = this || $mod$;
16 | return Bufferish.concat(buffers);
17 | };
18 |
19 | $mod$.encode = function (){
20 | "use strict";
21 | var self = this || $mod$;
22 | var $0 = arguments, i = $0.length;
23 | var chunks = new Array(i>0 ? i : 0);
24 | while(i>0) chunks[i-1] = $0[--i];
25 | var encoder = new (msgpack.Encoder)({});
26 | for (let i = 0, items = iter$(chunks), len = items.length; i < len; i++) {
27 | encoder.write(items[i]);
28 | };
29 | return encoder.read();
30 | };
31 | return $mod$;
32 | })({});
33 |
34 | function Packet(data,socket){
35 | this._socket = socket;
36 |
37 | if (true) {
38 | this._data = Buffer.from(data);
39 | };
40 |
41 | var decoder = new (msgpack.Decoder)();
42 | decoder.write(this._data);
43 | this._params = decoder.fetch();
44 |
45 | if ((typeof this._params=='number'||this._params instanceof Number)) {
46 | let offset = decoder.offset;
47 | this._ref = this._params;
48 | this._data = (false ? true : this._data.slice(offset));
49 | this._params = decoder.fetch();
50 | this._offset = decoder.offset - offset;
51 | } else {
52 | this._offset = decoder.offset;
53 | };
54 |
55 | this.CODE = this._params[0];
56 | this[1] = this._params[1];
57 | this[2] = this._params[2];
58 | this[3] = this._params[3];
59 | this[4] = this._params[4];
60 | this;
61 | };
62 |
63 | exports.Packet = Packet; // export class
64 | Packet.prototype.params = function(v){ return this._params; }
65 | Packet.prototype.setParams = function(v){ this._params = v; return this; };
66 | Packet.prototype.socket = function(v){ return this._socket; }
67 | Packet.prototype.setSocket = function(v){ this._socket = v; return this; };
68 | Packet.prototype.data = function(v){ return this._data; }
69 | Packet.prototype.setData = function(v){ this._data = v; return this; };
70 |
71 | Packet.serialize = function (data){
72 | if (true) {
73 | return (data instanceof Array) ? pack.encode(data) : data;
74 | };
75 | };
76 |
77 | Packet.prototype.payloadSize = function (){
78 | return this._data.byteLength - this._offset;
79 | };
80 |
81 | Packet.prototype.peer = function (){
82 | return this.socket();
83 | };
84 |
85 | Packet.prototype.retain = function (){
86 | // packets are inited with data from an arraybuffer
87 | // that is reused across requests. If we do anything
88 | // asynchronous with the data, we need to retain it
89 |
90 | if (true) {
91 | this._data = Buffer.from(this._data);
92 | this._payload = null;
93 | };
94 | return this;
95 | };
96 |
97 | Packet.prototype.payload = function (){
98 | if (true) {
99 | return this._payload || (this._payload = this._data.slice(this._offset));
100 | };
101 | };
102 |
103 | Packet.prototype.reply = function (msg){
104 | msg = Packet.prepare(msg);
105 | if (this._ref) {
106 | msg = Bufferish.concat([msgpack.encode(this._ref),msg]);
107 | };
108 | this.socket().send(msg);
109 | return this;
110 | };
111 |
112 | Packet.prototype.sid = function (){
113 | return this.socket().sid;
114 | };
115 |
116 | Packet.prototype.pid = function (){
117 | return this.socket().id;
118 | };
119 |
--------------------------------------------------------------------------------
/src/wss.imba:
--------------------------------------------------------------------------------
1 | var WebSocket = require 'ws'
2 |
3 | import {Terminal} from './terminal'
4 | import {FileSystem} from './fs'
5 | import {Git,GitRepo} from './git'
6 |
7 | class SocketClient
8 | prop ws
9 | prop widget
10 | prop options
11 |
12 | def initialize ws, options
13 | @options = options
14 | @ws = ws
15 | @ws.on 'message' do |message|
16 | try onmessage(JSON.parse(message))
17 | @ws.on 'close' do dispose
18 | setup(@options)
19 | self
20 |
21 | def log *params
22 | process:stdout.write(JSON.stringify(params))
23 |
24 | def send msg
25 | process.send(JSON.stringify("send message?"))
26 | @ws.send(JSON.stringify(msg)) do |err|
27 | if err
28 | log "error sending message",err
29 | self
30 |
31 | def onmessage msg
32 | log "wss.onmessage",msg[0],msg[1]
33 | let callback
34 | if msg[0] isa Number
35 | callback = msg.shift
36 |
37 | if widget and widget[msg[0]] isa Function
38 | let res = await widget[msg[0]].apply(widget,msg[1])
39 | send([-callback,res]) if callback
40 | self
41 |
42 | def dispose
43 | log "disposing wss client"
44 | widget?.dispose
45 |
46 | class TerminalClient < SocketClient
47 | def setup opts
48 | widget = Terminal.new(self,opts)
49 |
50 | class FileSystemClient < SocketClient
51 | def setup opts
52 | widget = FileSystem.new(self,opts)
53 | widget.on('all') do |type, params|
54 | log 'fs',type,params
55 | send([type,params])
56 | widget.start
57 |
58 | class RepoClient < SocketClient
59 |
60 | def setup opts
61 | widget = GitRepo.new(self,opts:cwd,opts)
62 | widget.on('all') do |type, params|
63 | log 'ws.git',type,params
64 | send([type,params])
65 | widget.start
66 |
67 | export class SocketServer
68 |
69 | def initialize
70 | @port = process:env.TUNNEL_PORT
71 | @pinger = do this:isAlive = true
72 | @noop = do yes
73 | self
74 |
75 | def log *params
76 | process:stdout.write(JSON.stringify(params))
77 | # process.send(JSON.stringify(params))
78 | self
79 |
80 | def ping
81 | self:isAlive = true
82 |
83 | def start
84 | log "starting socket server on {@port}"
85 | @wss = WebSocket.Server.new(port: @port)
86 |
87 | @checker = setInterval(&,10000) do
88 | log "checking ws connections"
89 | @wss:clients.forEach do |ws|
90 | if ws:isAlive === false
91 | log "close ws connection"
92 | return ws.terminate
93 | ws:isAlive = false
94 | ws.ping(@noop)
95 |
96 | @wss.on 'connection' do |ws,req|
97 | try
98 |
99 | ws:isAlive = true
100 | ws.on('pong', @pinger)
101 |
102 | var opts = {}
103 | var url = URL.new('http://127.0.0.1' + req:url)
104 | let type = opts:type = url:searchParams.get('type')
105 | let cwd = opts:cwd = url:searchParams.get('cwd')
106 | let baseRef = url:searchParams.get('baseRef')
107 |
108 | for key in ['width','height','cwd','type','baseRef','headRef']
109 | if url:searchParams.has(key)
110 | opts[key] = url:searchParams.get(key)
111 | if String(opts[key]).match(/^\d+$/)
112 | opts[key] = Number(opts[key])
113 |
114 | log "connected!!!",req:url,opts
115 |
116 | if opts:type == 'terminal'
117 | TerminalClient.new(ws,opts)
118 | elif opts:type == 'fs'
119 | FileSystemClient.new(ws,opts)
120 | elif opts:type == 'repo'
121 | RepoClient.new(ws,opts)
122 | catch e
123 | log "error",e:message
124 |
125 | @wss.on 'error' do |e|
126 | log "error from wss",e && e:message
127 | return
128 |
129 | export var wss = SocketServer.new.start
--------------------------------------------------------------------------------
/src/terminal/buffer.imba:
--------------------------------------------------------------------------------
1 | import wc from "../../vendor/lib_wc"
2 |
3 | export def getFill width, char
4 | Array.new(width+1).join(char)
5 |
6 | export def getWhitespace width
7 | getFill(width, " ")
8 |
9 | def createColor source, color
10 | if source == 'rgb'
11 | color.match(/\d+/g).map(do parseInt($1, 10))
12 | elif source == 'default'
13 | null
14 | else
15 | source
16 |
17 | export var TextStyling =
18 | DEFAULT: [null, null, 0]
19 |
20 | fromAttributes: do |attrs|
21 | var bitfield = 0
22 | if attrs:bold
23 | bitfield |= (1 << 0)
24 | if attrs:faint
25 | bitfield |= (1 << 1)
26 | if attrs:italic
27 | bitfield |= (1 << 2)
28 | if attrs:blink
29 | bitfield |= (1 << 3)
30 | if attrs:underline
31 | bitfield |= (1 << 4)
32 | if attrs:strikethrough
33 | bitfield |= (1 << 5)
34 | if attrs:inverse
35 | bitfield |= (1 << 6)
36 | if attrs:invisible
37 | bitfield |= (1 << 7)
38 |
39 | [
40 | createColor(attrs:foregroundSource, attrs:foreground)
41 | createColor(attrs:backgroundSource, attrs:background)
42 | bitfield
43 | ]
44 |
45 | export class Line
46 | prop text
47 | prop styles
48 |
49 | def getFill width, char
50 | Array.new(width+1).join(char)
51 |
52 | def getWhitespace width
53 | getFill(width, " ")
54 |
55 | def initialize width, styling = null
56 | @width = width
57 | if styling
58 | clear(styling)
59 |
60 | def equal other
61 | (other.text == text) and (other.styles.toString == styles.toString)
62 |
63 | def substr start, end
64 | wc.substr(@text, start, end)
65 |
66 | def shiftLeft column, width, rightStyling
67 | var before = wc.substr(@text, 0, column)
68 | var after = wc.substr(@text, column+width)
69 |
70 | @text = before + after
71 | @styles.splice(column, width)
72 |
73 | for i in [0 ... width]
74 | @styles.push(rightStyling)
75 | self
76 |
77 | def erase column, width, styling
78 | var before = wc.substr(@text, 0, column)
79 | var after = wc.substr(@text, column+width)
80 |
81 | if after
82 | var space = getWhitespace(width)
83 | @text = before + space + after
84 | else
85 | @text = before
86 |
87 | for i in [column ... (column + width)]
88 | @styles[i] = styling
89 |
90 | self
91 |
92 | def clear styling
93 | @text = ""
94 | @styles = Array.new(@width)
95 |
96 | for i in [0 ... @width]
97 | @styles[i] = styling
98 |
99 | def replace column, text, styling
100 | var currentWidth = wc.strWidth(@text)
101 | if currentWidth < column
102 | @text += getWhitespace(column - currentWidth)
103 |
104 | var width = wc.strWidth(text)
105 | var before = wc.substr(@text, 0, column)
106 | var after = wc.substr(@text, column+width)
107 |
108 | @text = before + text + after
109 |
110 | for i in [column ... (column + width)]
111 | @styles[i] = styling
112 |
113 | self
114 |
115 |
116 | export class ScreenBuffer
117 | prop width
118 | prop height
119 | prop row
120 | prop column
121 | prop cursorVisible
122 | prop lines
123 | prop version
124 |
125 | def initialize width, height
126 | @width = width
127 | @height = height
128 | @version = 0
129 |
130 | @row = 0
131 | @column = 0
132 | @cursorVisible = yes
133 |
134 | @lines = []
135 |
136 | def equal other
137 | if other.lines:length != lines:length
138 | return false
139 |
140 | lines.every do |line, idx|
141 | line.equal(other.lines[idx])
142 |
143 | def createLine styling
144 | Line.new(@width, styling)
145 |
146 | def indexForRow row
147 | var extraRows = Math.max(0, @lines:length - @height)
148 | extraRows + row
149 |
150 | def lineAtRow row
151 | var idx = indexForRow(row)
152 | @lines[idx]
153 |
154 | let WS_ONLY = /^ *$/
155 |
156 | def clear styling
157 | var lastSignificantIdx = @lines:length - 1
158 | while lastSignificantIdx >= 0
159 | if WS_ONLY.test(@lines[lastSignificantIdx].text)
160 | lastSignificantIdx--
161 | else
162 | break
163 |
164 | @lines = @lines.slice(0, lastSignificantIdx + 1)
165 |
166 | for i in [0 ... @height]
167 | @lines.push(createLine(styling))
168 |
--------------------------------------------------------------------------------
/src/terminal/runner.imba:
--------------------------------------------------------------------------------
1 |
2 | try
3 | var pty = require 'node-pty'
4 | catch e
5 | console.log "could not load pty",e
6 |
7 | var path = require "path"
8 | var fs = require "fs"
9 |
10 | # var msgpack = require "msgpack"
11 | import Terminal from "./terminal"
12 | import TerminalModel from "./model"
13 |
14 | export class CommandRunner
15 | prop terminal
16 |
17 | def initialize terminal, options = {}
18 | @cwd = options:cwd
19 | @terminal = terminal
20 | @terminal:io:sendString = do |data| sendString(data)
21 | @vt = @terminal.vt
22 | @vt:characterEncoding = 'raw'
23 | @child = null
24 | @pty = null
25 |
26 | def ontick
27 | # do nothing
28 |
29 | def onend
30 | # do nothing
31 |
32 | def sendString data
33 | if !isRunning
34 | throw Error.new("No command attached")
35 |
36 | (@pty or @child:stdin).write(data)
37 |
38 | def isRunning
39 | (@pty or @child) != null
40 |
41 | def kill
42 | @child.kill("SIGHUP") if isRunning
43 | self
44 |
45 | def run command, args, options = {}
46 | if isRunning
47 | throw Error.new("Existing command is already attached")
48 |
49 | if !pty
50 | return null
51 |
52 | var ptyOptions = Object.assign({}, options, {
53 | rows: terminal.height
54 | columns: terminal.width
55 | cols: terminal.width
56 | cwd: @cwd or options:cwd or process:env:cwd
57 | env: process:env
58 | })
59 |
60 | # console.log "start pty",shell,command
61 | @pty = pty.spawn(command,args,ptyOptions)
62 |
63 | (@pty or @child:stdout).on('data') do |data|
64 | if process:env.DEBUG
65 | fs.appendFileSync(__dirname + '/../../logs/terminal.log',JSON.stringify(data.toString('utf8')) + '\n')
66 | @vt.interpret(data.toString('utf8'))
67 | ontick
68 |
69 | false && @child:stdout.once('end') do
70 | @child = null
71 | # TODO: reset?
72 | onend
73 |
74 | self
75 |
76 | export class Runner
77 | prop options
78 |
79 | def initialize owner,options
80 | @owner = owner
81 | @options = options
82 | @cwd = options:cwd
83 |
84 | @terminal = Terminal.new(options)
85 | @model = TerminalModel.new(@terminal.width, @terminal.height)
86 |
87 | @index = 0
88 | @receiver = null
89 |
90 | if pty
91 | @cmdRunner = CommandRunner.new(@terminal,cwd: @cwd)
92 | @cmdRunner:ontick = do didChange
93 | @cmdRunner:onend = do didEnd
94 | self
95 |
96 | def isSupported
97 | !!pty
98 |
99 | def run command, args, options = {}
100 | @cmdRunner?.run(command, args, cwd: @cwd)
101 |
102 | def write string
103 | @cmdRunner?.sendString(string)
104 |
105 | def kill
106 | @cmdRunner?.kill
107 |
108 | def send obj = {}
109 | obj:index = @index++
110 | Imba.emit(self,'message',[obj])
111 |
112 | if @receiver
113 | var binary = msgpack.pack(obj)
114 | @receiver.send(binary)
115 |
116 | def sendState
117 | send(state)
118 |
119 | def state
120 | {
121 | type: 'state'
122 | connectUrl: options:connectUrl
123 | isRunning: @cmdRunner ? @cmdRunner.isRunning : no
124 | screenIndex: @terminal.screenIndex
125 | }
126 |
127 | def didChange
128 | clearTimeout(@flushTimeout)
129 | @flushTimeout = setTimeout(&,5) do flushChanges
130 | # flushChanges
131 | self
132 |
133 | def flushChanges
134 | var patch = @model.createPatch(@terminal.primaryScreen, @terminal.alternateScreen)
135 | @model.applyPatch(patch)
136 | # console.log "flushChanges",JSON.stringify(patch)
137 | send
138 | type: 'update'
139 | patch: patch
140 | row: @terminal.screen.row
141 | column: @terminal.screen.column
142 | screenIndex: @terminal.screenIndex
143 |
144 | def didEnd
145 | sendState
146 |
147 | def receive msg
148 | return unless @cmdRunner
149 |
150 | switch msg:type
151 | when "cmd.run"
152 | @cmdRunner.run(msg:cmd, msg:args, cwd: @cwd)
153 | sendState
154 |
155 | when "stdin.write"
156 | @cmdRunner.sendString(msg:content)
157 | else
158 | console.log("Unknown command: {msg}")
159 |
160 | def bind ws
161 | @receiver = ws
162 |
163 | ws.on "message" do |binary|
164 | if @receiver !== ws
165 | # Err! Received message on old connection
166 | ws.close
167 | return
168 |
169 | var buffer = Buffer.from(binary)
170 | var msg = msgpack.unpack(buffer)
171 | receive(msg)
172 |
173 | ws.on "close" do
174 | if @receiver !== ws
175 | return
176 | @receiver = null
177 |
178 | sendState
179 |
180 | export def uuid a,b
181 | b = a = ''
182 | while a++ < 36
183 | b += a*51 & 52 ? (a^15 ? 8^Math.random * (a^20 ? 16 : 4) : 4).toString(16) : '-'
184 | return b
185 |
186 | export class MultiRunner
187 | def initialize options
188 | @options = options
189 | @tmpdir = options:tmpdir
190 | @baseurl = options:baseurl or ""
191 | if !@tmpdir
192 | throw Error.new("tmpdir is required")
193 | @runners = {}
194 |
195 | def findRunner id
196 | @runners[id]
197 |
198 | def createRunner
199 | var id = uuid
200 | var runnerDir = path.join(@tmpdir, id)
201 | fs.mkdirSync(runnerDir)
202 | var cwd = path.join(runnerDir, "gitspeak")
203 | fs.mkdirSync(cwd)
204 | @runners[id] = Runner.new
205 | width: @options:width
206 | height: @options:height
207 | cwd: cwd
208 | connectUrl: "{@baseurl}{id}"
209 | id
210 |
--------------------------------------------------------------------------------
/lib/wss.js:
--------------------------------------------------------------------------------
1 | var Imba = require('imba');
2 | var WebSocket = require('ws');
3 |
4 | var Terminal = require('./terminal').Terminal;
5 | var FileSystem = require('./fs').FileSystem;
6 | var git$ = require('./git'), Git = git$.Git, GitRepo = git$.GitRepo;
7 |
8 | function SocketClient(ws,options){
9 | var self = this;
10 | self._options = options;
11 | self._ws = ws;
12 | self._ws.on('message',function(message) {
13 | try {
14 | return self.onmessage(JSON.parse(message));
15 | } catch (e) { };
16 | });
17 | self._ws.on('close',function() { return self.dispose(); });
18 | self.setup(self._options);
19 | self;
20 | };
21 |
22 | SocketClient.prototype.ws = function(v){ return this._ws; }
23 | SocketClient.prototype.setWs = function(v){ this._ws = v; return this; };
24 | SocketClient.prototype.widget = function(v){ return this._widget; }
25 | SocketClient.prototype.setWidget = function(v){ this._widget = v; return this; };
26 | SocketClient.prototype.options = function(v){ return this._options; }
27 | SocketClient.prototype.setOptions = function(v){ this._options = v; return this; };
28 |
29 | SocketClient.prototype.log = function (){
30 | var $0 = arguments, i = $0.length;
31 | var params = new Array(i>0 ? i : 0);
32 | while(i>0) params[i-1] = $0[--i];
33 | return process.stdout.write(JSON.stringify(params));
34 | };
35 |
36 | SocketClient.prototype.send = function (msg){
37 | var self = this;
38 | process.send(JSON.stringify("send message?"));
39 | self._ws.send(JSON.stringify(msg),function(err) {
40 | if (err) {
41 | return self.log("error sending message",err);
42 | };
43 | });
44 | return self;
45 | };
46 |
47 | SocketClient.prototype.onmessage = async function (msg){
48 | this.log("wss.onmessage",msg[0],msg[1]);
49 | let callback;
50 | if ((typeof msg[0]=='number'||msg[0] instanceof Number)) {
51 | callback = msg.shift();
52 | };
53 |
54 | if (this.widget() && (this.widget()[msg[0]] instanceof Function)) {
55 | let res = await this.widget()[msg[0]].apply(this.widget(),msg[1]);
56 | if (callback) { this.send([-callback,res]) };
57 | };
58 | return this;
59 | };
60 |
61 | SocketClient.prototype.dispose = function (){
62 | var widget_;
63 | this.log("disposing wss client");
64 | return (widget_ = this.widget()) && widget_.dispose && widget_.dispose();
65 | };
66 |
67 | function TerminalClient(){ return SocketClient.apply(this,arguments) };
68 |
69 | Imba.subclass(TerminalClient,SocketClient);
70 | TerminalClient.prototype.setup = function (opts){
71 | var v_;
72 | return (this.setWidget(v_ = new Terminal(this,opts)),v_);
73 | };
74 |
75 | function FileSystemClient(){ return SocketClient.apply(this,arguments) };
76 |
77 | Imba.subclass(FileSystemClient,SocketClient);
78 | FileSystemClient.prototype.setup = function (opts){
79 | var self = this;
80 | self.setWidget(new FileSystem(self,opts));
81 | self.widget().on('all',function(type,params) {
82 | self.log('fs',type,params);
83 | return self.send([type,params]);
84 | });
85 | return self.widget().start();
86 | };
87 |
88 | function RepoClient(){ return SocketClient.apply(this,arguments) };
89 |
90 | Imba.subclass(RepoClient,SocketClient);
91 | RepoClient.prototype.setup = function (opts){
92 | var self = this;
93 | self.setWidget(new GitRepo(self,opts.cwd,opts));
94 | self.widget().on('all',function(type,params) {
95 | self.log('ws.git',type,params);
96 | return self.send([type,params]);
97 | });
98 | return self.widget().start();
99 | };
100 |
101 | function SocketServer(){
102 | this._port = process.env.TUNNEL_PORT;
103 | this._pinger = function() { return this.isAlive = true; };
104 | this._noop = function() { return true; };
105 | this;
106 | };
107 |
108 | exports.SocketServer = SocketServer; // export class
109 | SocketServer.prototype.log = function (){
110 | var $0 = arguments, i = $0.length;
111 | var params = new Array(i>0 ? i : 0);
112 | while(i>0) params[i-1] = $0[--i];
113 | process.stdout.write(JSON.stringify(params));
114 | // process.send(JSON.stringify(params))
115 | return this;
116 | };
117 |
118 | SocketServer.prototype.ping = function (){
119 | return this.isAlive = true;
120 | };
121 |
122 | SocketServer.prototype.start = function (){
123 | var self = this;
124 | self.log(("starting socket server on " + (self._port)));
125 | self._wss = new WebSocket.Server({port: self._port});
126 |
127 | self._checker = setInterval(function() {
128 | self.log("checking ws connections");
129 | return self._wss.clients.forEach(function(ws) {
130 | if (ws.isAlive === false) {
131 | self.log("close ws connection");
132 | return ws.terminate();
133 | };
134 | ws.isAlive = false;
135 | return ws.ping(self._noop);
136 | });
137 | },10000);
138 |
139 | self._wss.on('connection',function(ws,req) {
140 | try {
141 |
142 | ws.isAlive = true;
143 | ws.on('pong',self._pinger);
144 |
145 | var opts = {};
146 | var url = new URL('http://127.0.0.1' + req.url);
147 | let type = opts.type = url.searchParams.get('type');
148 | let cwd = opts.cwd = url.searchParams.get('cwd');
149 | let baseRef = url.searchParams.get('baseRef');
150 |
151 | for (let i = 0, items = ['width','height','cwd','type','baseRef','headRef'], len = items.length, key; i < len; i++) {
152 | key = items[i];
153 | if (url.searchParams.has(key)) {
154 | opts[key] = url.searchParams.get(key);
155 | if (String(opts[key]).match(/^\d+$/)) {
156 | opts[key] = Number(opts[key]);
157 | };
158 | };
159 | };
160 |
161 | self.log("connected!!!",req.url,opts);
162 |
163 | if (opts.type == 'terminal') {
164 | return new TerminalClient(ws,opts);
165 | } else if (opts.type == 'fs') {
166 | return new FileSystemClient(ws,opts);
167 | } else if (opts.type == 'repo') {
168 | return new RepoClient(ws,opts);
169 | };
170 | } catch (e) {
171 | return self.log("error",e.message);
172 | };
173 | });
174 |
175 | self._wss.on('error',function(e) {
176 | return self.log("error from wss",e && e.message);
177 | });
178 | return;
179 | };
180 |
181 | var wss = exports.wss = new SocketServer().start();
182 |
--------------------------------------------------------------------------------
/src/terminal/model.imba:
--------------------------------------------------------------------------------
1 | import Line, ScreenBuffer, TextStyling from "./buffer"
2 |
3 | var REPLACE_LINE = 0
4 | var PUSH_LINE = 1
5 |
6 | export class Palette
7 | prop index
8 | prop values
9 |
10 | def initialize startAt = 1
11 | @startAt = startAt
12 | @index = startAt
13 | @mapping = {}
14 | @values = []
15 |
16 | def findIndex value
17 | @mapping[value.toString]
18 |
19 | def insert value
20 | Object.freeze(value)
21 | var idx = @index++
22 | @mapping[value.toString] = idx
23 | @values.push(value)
24 | idx
25 |
26 | def append values
27 | for value in values
28 | insert(value)
29 | self
30 |
31 | def findIndexOrInsert value
32 | findIndex(value) or insert(value)
33 |
34 | def lookup idx
35 | var value = @values[idx - @startAt]
36 | if !value
37 | throw Error.new("No value with index={idx}")
38 | return value
39 |
40 |
41 | export class TerminalModel
42 | prop primaryScreen
43 | prop alternateScreen
44 | prop screens
45 |
46 | prop width
47 | prop height
48 |
49 | def initialize width, height
50 | @width = width
51 | @primaryScreen = ScreenBuffer.new(width, height)
52 | @alternateScreen = ScreenBuffer.new(width, height)
53 | @stylingPalette = Palette.new
54 |
55 | @primaryScreen.clear(TextStyling.DEFAULT)
56 | @alternateScreen.clear(TextStyling.DEFAULT)
57 |
58 | @screens = [@primaryScreen, @alternateScreen]
59 |
60 | def encodeRunLength data
61 | var result = []
62 | var count = 1
63 | var value = data[0]
64 |
65 | for idx in [1 ... data:length]
66 | if value == data[idx]
67 | count += 1
68 | else
69 | result.push(count)
70 | result.push(value)
71 | count = 1
72 | value = data[idx]
73 | result.push(count)
74 | result.push(value)
75 | result
76 |
77 | def decodeRunLength data
78 | var result = []
79 | var idx = 0
80 | while idx < data:length
81 | var count = data[idx++]
82 | var value = data[idx++]
83 | while count--
84 | result.push(value)
85 | return result
86 |
87 | def encodeStyles styles, newPalette
88 | styles = styles.map do |styling|
89 | @stylingPalette.findIndex(styling) or newPalette.findIndexOrInsert(styling)
90 | styles = encodeRunLength(styles)
91 |
92 | if styles:length == 2 and styles[1] == 1
93 | return ""
94 |
95 | styles
96 |
97 | def decodeStyles styles
98 | if styles == null
99 | styles = [@width, 1]
100 | styles = decodeRunLength(styles)
101 | styles = styles.map do |idx|
102 | @stylingPalette.lookup(idx)
103 |
104 | def diffText old, new
105 | var startIdx = 0
106 | var endNewIdx = new:length - 1
107 | var endOldIdx = old:length - 1
108 |
109 | while old[startIdx] == new[startIdx]
110 | startIdx++
111 |
112 | while startIdx < endOldIdx and startIdx < endNewIdx and old[endOldIdx] == new[endNewIdx]
113 | endNewIdx--
114 | endOldIdx--
115 |
116 | if startIdx > 0 or (endNewIdx < new:length - 1)
117 | [startIdx, endOldIdx+1, new.slice(startIdx, endNewIdx+1)]
118 | else
119 | new
120 |
121 | def patchText old, patch
122 | if typeof patch == 'string'
123 | return patch
124 |
125 | var start = patch[0]
126 | var stop = patch[1]
127 | old.slice(0, start) + patch[2] + old.slice(stop)
128 |
129 | def actionsBetween old, new, newPalette
130 | var actions = []
131 |
132 | for line, idx in old.lines
133 | var newLine = new.lines[idx]
134 | if !newLine
135 | # should not happen?
136 | break
137 |
138 | var styles = encodeStyles(line.styles, newPalette)
139 | var newStyles = encodeStyles(newLine.styles, newPalette)
140 |
141 | var sameText = (line.text == newLine.text)
142 | var sameStyles = (styles.toString == newStyles.toString)
143 |
144 |
145 | if sameText and sameStyles
146 | # Nothing to do!
147 | continue
148 |
149 | actions.push([
150 | REPLACE_LINE, idx,
151 | sameText ? null : diffText(line.text, newLine.text),
152 | sameStyles ? null : newStyles,
153 | ])
154 |
155 | for idx in [old.lines:length ... new.lines:length]
156 | var line = new.lines[idx]
157 | var styles = encodeStyles(line.styles, newPalette)
158 |
159 | var action = [PUSH_LINE, line.text]
160 | if styles
161 | action.push(styles)
162 | actions.push(action)
163 |
164 | actions
165 |
166 | def applyActions screen, actions
167 | for action in actions
168 | if action[0] == PUSH_LINE
169 | var line = Line.new(@width)
170 | line.text = action[1]
171 | line.styles = (action.STYLES ||= decodeStyles(action[2]))
172 | screen.lines.push(line)
173 |
174 | elif action[0] == REPLACE_LINE
175 | var lineIdx = action[1]
176 | var line = screen.lines[lineIdx]
177 | if action[2] != null
178 | action.OLDTEXT = line.text
179 | line.text = patchText(line.text, action[2])
180 | if action[3] != null
181 | action.OLDSTYLES = line.styles
182 | line.styles = (action.STYLES ||= decodeStyles(action[3]))
183 | screen.version++
184 | self
185 |
186 | def revertActions screen, actions
187 | # NOTE: Now the order of actions is not significent.
188 | # In the future we might want to loop backwards
189 | for action in actions
190 | if action[0] == PUSH_LINE
191 | screen.lines.pop
192 | elif action[0] == REPLACE_LINE
193 | var lineIdx = action[1]
194 | var line = screen.lines[lineIdx]
195 | if action.OLDTEXT != null
196 | line.text = action.OLDTEXT
197 | if action.OLDSTYLES != null
198 | line.styles = action.OLDSTYLES
199 | screen.version++
200 | self
201 |
202 | def createPatch newPrimary, newAlternate
203 | var newPalette = Palette.new(@stylingPalette.index)
204 | var primaryActions = actionsBetween(@primaryScreen, newPrimary, newPalette)
205 | var alternateActions = actionsBetween(@alternateScreen, newAlternate, newPalette)
206 |
207 | if primaryActions:length == 0 and alternateActions:length == 0
208 | return null
209 |
210 | [newPalette.values, primaryActions, alternateActions]
211 |
212 | def applyPatch patch
213 | if !patch
214 | return
215 |
216 | @stylingPalette.append(patch[0])
217 | applyActions(@primaryScreen, patch[1])
218 | applyActions(@alternateScreen, patch[2])
219 |
220 | def revertPatch patch
221 | if !patch
222 | return
223 |
224 | revertActions(@primaryScreen, patch[1])
225 | revertActions(@alternateScreen, patch[2])
226 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 | ==================
3 |
4 | Statement of Purpose
5 | ---------------------
6 |
7 | The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work").
8 |
9 | Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others.
10 |
11 | For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights.
12 |
13 | 1. Copyright and Related Rights.
14 | --------------------------------
15 | A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following:
16 |
17 | i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work;
18 | ii. moral rights retained by the original author(s) and/or performer(s);
19 | iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work;
20 | iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below;
21 | v. rights protecting the extraction, dissemination, use and reuse of data in a Work;
22 | vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and
23 | vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof.
24 |
25 | 2. Waiver.
26 | -----------
27 | To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose.
28 |
29 | 3. Public License Fallback.
30 | ----------------------------
31 | Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose.
32 |
33 | 4. Limitations and Disclaimers.
34 | --------------------------------
35 |
36 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document.
37 | b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.
38 | c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work.
39 | d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work.
40 |
--------------------------------------------------------------------------------
/lib/terminal/buffer.js:
--------------------------------------------------------------------------------
1 | var self = {};
2 | var wc = require("../../vendor/lib_wc").wc;
3 |
4 | exports.getFill = self.getFill = function (width,char$){
5 | return new Array(width + 1).join(char$);
6 | };
7 |
8 | exports.getWhitespace = self.getWhitespace = function (width){
9 | return self.getFill(width," ");
10 | };
11 |
12 | self.createColor = function (source,color){
13 | if (source == 'rgb') {
14 | return color.match(/\d+/g).map(function(_0) { return parseInt(_0,10); });
15 | } else if (source == 'default') {
16 | return null;
17 | } else {
18 | return source;
19 | };
20 | };
21 |
22 | var TextStyling = exports.TextStyling = {
23 | DEFAULT: [null,null,0],
24 |
25 | fromAttributes: function(attrs) {
26 | var bitfield = 0;
27 | if (attrs.bold) {
28 | bitfield |= (1 << 0);
29 | };
30 | if (attrs.faint) {
31 | bitfield |= (1 << 1);
32 | };
33 | if (attrs.italic) {
34 | bitfield |= (1 << 2);
35 | };
36 | if (attrs.blink) {
37 | bitfield |= (1 << 3);
38 | };
39 | if (attrs.underline) {
40 | bitfield |= (1 << 4);
41 | };
42 | if (attrs.strikethrough) {
43 | bitfield |= (1 << 5);
44 | };
45 | if (attrs.inverse) {
46 | bitfield |= (1 << 6);
47 | };
48 | if (attrs.invisible) {
49 | bitfield |= (1 << 7);
50 | };
51 |
52 | return [
53 | self.createColor(attrs.foregroundSource,attrs.foreground),
54 | self.createColor(attrs.backgroundSource,attrs.background),
55 | bitfield
56 | ];
57 | }
58 | };
59 |
60 | function Line(width,styling){
61 | if(styling === undefined) styling = null;
62 | this._width = width;
63 | if (styling) {
64 | this.clear(styling);
65 | };
66 | };
67 |
68 | exports.Line = Line; // export class
69 | Line.prototype.text = function(v){ return this._text; }
70 | Line.prototype.setText = function(v){ this._text = v; return this; };
71 | Line.prototype.styles = function(v){ return this._styles; }
72 | Line.prototype.setStyles = function(v){ this._styles = v; return this; };
73 |
74 | Line.prototype.getFill = function (width,char$){
75 | return new Array(width + 1).join(char$);
76 | };
77 |
78 | Line.prototype.getWhitespace = function (width){
79 | return this.getFill(width," ");
80 | };
81 |
82 | Line.prototype.equal = function (other){
83 | return (other.text() == this.text()) && (other.styles().toString() == this.styles().toString());
84 | };
85 |
86 | Line.prototype.substr = function (start,end){
87 | return wc.substr(this._text,start,end);
88 | };
89 |
90 | Line.prototype.shiftLeft = function (column,width,rightStyling){
91 | var before = wc.substr(this._text,0,column);
92 | var after = wc.substr(this._text,column + width);
93 |
94 | this._text = before + after;
95 | this._styles.splice(column,width);
96 |
97 | for (let len = width, i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
98 | this._styles.push(rightStyling);
99 | };
100 | return this;
101 | };
102 |
103 | Line.prototype.erase = function (column,width,styling){
104 | var before = wc.substr(this._text,0,column);
105 | var after = wc.substr(this._text,column + width);
106 |
107 | if (after) {
108 | var space = this.getWhitespace(width);
109 | this._text = before + space + after;
110 | } else {
111 | this._text = before;
112 | };
113 |
114 | for (let len = (column + width), i = column, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
115 | this._styles[i] = styling;
116 | };
117 |
118 | return this;
119 | };
120 |
121 | Line.prototype.clear = function (styling){
122 | this._text = "";
123 | this._styles = new Array(this._width);
124 |
125 | let res = [];
126 | for (let len = this._width, i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
127 | res.push((this._styles[i] = styling));
128 | };
129 | return res;
130 | };
131 |
132 | Line.prototype.replace = function (column,text,styling){
133 | var currentWidth = wc.strWidth(this._text);
134 | if (currentWidth < column) {
135 | this._text += this.getWhitespace(column - currentWidth);
136 | };
137 |
138 | var width = wc.strWidth(text);
139 | var before = wc.substr(this._text,0,column);
140 | var after = wc.substr(this._text,column + width);
141 |
142 | this._text = before + text + after;
143 |
144 | for (let len = (column + width), i = column, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
145 | this._styles[i] = styling;
146 | };
147 |
148 | return this;
149 | };
150 |
151 |
152 | function ScreenBuffer(width,height){
153 | this._width = width;
154 | this._height = height;
155 | this._version = 0;
156 |
157 | this._row = 0;
158 | this._column = 0;
159 | this._cursorVisible = true;
160 |
161 | this._lines = [];
162 | };
163 |
164 | exports.ScreenBuffer = ScreenBuffer; // export class
165 | ScreenBuffer.prototype.width = function(v){ return this._width; }
166 | ScreenBuffer.prototype.setWidth = function(v){ this._width = v; return this; };
167 | ScreenBuffer.prototype.height = function(v){ return this._height; }
168 | ScreenBuffer.prototype.setHeight = function(v){ this._height = v; return this; };
169 | ScreenBuffer.prototype.row = function(v){ return this._row; }
170 | ScreenBuffer.prototype.setRow = function(v){ this._row = v; return this; };
171 | ScreenBuffer.prototype.column = function(v){ return this._column; }
172 | ScreenBuffer.prototype.setColumn = function(v){ this._column = v; return this; };
173 | ScreenBuffer.prototype.cursorVisible = function(v){ return this._cursorVisible; }
174 | ScreenBuffer.prototype.setCursorVisible = function(v){ this._cursorVisible = v; return this; };
175 | ScreenBuffer.prototype.lines = function(v){ return this._lines; }
176 | ScreenBuffer.prototype.setLines = function(v){ this._lines = v; return this; };
177 | ScreenBuffer.prototype.version = function(v){ return this._version; }
178 | ScreenBuffer.prototype.setVersion = function(v){ this._version = v; return this; };
179 |
180 | ScreenBuffer.prototype.equal = function (other){
181 | if (other.lines().length != this.lines().length) {
182 | return false;
183 | };
184 |
185 | return this.lines().every(function(line,idx) {
186 | return line.equal(other.lines()[idx]);
187 | });
188 | };
189 |
190 | ScreenBuffer.prototype.createLine = function (styling){
191 | return new Line(this._width,styling);
192 | };
193 |
194 | ScreenBuffer.prototype.indexForRow = function (row){
195 | var extraRows = Math.max(0,this._lines.length - this._height);
196 | return extraRows + row;
197 | };
198 |
199 | ScreenBuffer.prototype.lineAtRow = function (row){
200 | var idx = this.indexForRow(row);
201 | return this._lines[idx];
202 | };
203 |
204 | let WS_ONLY = /^ *$/;
205 |
206 | ScreenBuffer.prototype.clear = function (styling){
207 | var lastSignificantIdx = this._lines.length - 1;
208 | while (lastSignificantIdx >= 0){
209 | if (WS_ONLY.test(this._lines[lastSignificantIdx].text())) {
210 | lastSignificantIdx--;
211 | } else {
212 | break;
213 | };
214 | };
215 |
216 | this._lines = this._lines.slice(0,lastSignificantIdx + 1);
217 |
218 | let res = [];
219 | for (let len = this._height, i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) {
220 | res.push(this._lines.push(this.createLine(styling)));
221 | };
222 | return res;
223 | };
224 |
--------------------------------------------------------------------------------
/src/helpers.imba:
--------------------------------------------------------------------------------
1 |
2 | var ansiMap =
3 | reset: [0, 0],
4 | bold: [1, 22],
5 | dim: [2, 22],
6 | italic: [3, 23],
7 | underline: [4, 24],
8 | inverse: [7, 27],
9 | hidden: [8, 28],
10 | strikethrough: [9, 29]
11 |
12 | black: [30, 39],
13 | red: [31, 39],
14 | green: [32, 39],
15 | yellow: [33, 39],
16 | blue: [34, 39],
17 | magenta: [35, 39],
18 | cyan: [36, 39],
19 | white: [37, 39],
20 | gray: [90, 39],
21 |
22 | redBright: [91, 39],
23 | greenBright: [92, 39],
24 | yellowBright: [93, 39],
25 | blueBright: [94, 39],
26 | magentaBright: [95, 39],
27 | cyanBright: [96, 39],
28 | whiteBright: [97, 39]
29 |
30 | export var ansi =
31 | bold: do |text| '\u001b[1m' + text + '\u001b[22m'
32 | red: do |text| '\u001b[31m' + text + '\u001b[39m'
33 | green: do |text| '\u001b[32m' + text + '\u001b[39m'
34 | yellow: do |text| '\u001b[33m' + text + '\u001b[39m'
35 | gray: do |text| '\u001b[90m' + text + '\u001b[39m'
36 | white: do |text| '\u001b[37m' + text + '\u001b[39m'
37 | f: do |name,text|
38 | let pair = ansiMap[name]
39 | return '\u001b['+pair[0]+'m' + text + '\u001b['+pair[1]+'m'
40 |
41 | ansi:warn = ansi:yellow
42 | ansi:error = ansi:red
43 |
44 | export def brace str
45 | var lines = str.match(/\n/)
46 | # what about indentation?
47 |
48 | if lines
49 | '{' + str + '\n}'
50 | else
51 | '{\n' + str + '\n}'
52 |
53 | export def normalizeIndentation str
54 | var m
55 | var reg = /\n+([^\n\S]*)/g
56 | var ind = null
57 |
58 | while m = reg.exec(str)
59 | var attempt = m[1]
60 | if ind is null or 0 < attempt:length < ind:length
61 | ind = attempt
62 |
63 | str = str.replace(RegExp("\\n{ind}","g"), '\n') if ind
64 | return str
65 |
66 |
67 | export def flatten arr
68 | var out = []
69 | arr.forEach do |v| v isa Array ? out:push.apply(out,flatten(v)) : out.push(v)
70 | return out
71 |
72 |
73 | export def pascalCase str
74 | str.replace(/(^|[\-\_\s])(\w)/g) do |m,v,l| l.toUpperCase
75 |
76 | export def camelCase str
77 | str = String(str)
78 | # should add shortcut out
79 | str.replace(/([\-\_\s])(\w)/g) do |m,v,l| l.toUpperCase
80 |
81 | export def dashToCamelCase str
82 | str = String(str)
83 | if str.indexOf('-') >= 0
84 | # should add shortcut out
85 | str = str.replace(/([\-\s])(\w)/g) do |m,v,l| l.toUpperCase
86 | return str
87 |
88 | export def snakeCase str
89 | var str = str.replace(/([\-\s])(\w)/g,'_')
90 | str.replace(/()([A-Z])/g,"_$1") do |m,v,l| l.toUpperCase
91 |
92 | export def setterSym sym
93 | dashToCamelCase("set-{sym}")
94 |
95 | export def quote str
96 | '"' + str + '"'
97 |
98 | export def singlequote str
99 | "'" + str + "'"
100 |
101 | export def symbolize str
102 | str = String(str)
103 | var end = str.charAt(str:length - 1)
104 |
105 | if end == '='
106 | str = 'set' + str[0].toUpperCase + str.slice(1,-1)
107 |
108 | if str.indexOf("-") >= 0
109 | str = str.replace(/([\-\s])(\w)/g) do |m,v,l| l.toUpperCase
110 |
111 | return str
112 |
113 |
114 | export def indent str
115 | String(str).replace(/^/g,"\t").replace(/\n/g,"\n\t").replace(/\n\t$/g,"\n")
116 |
117 | export def bracketize str, ind = yes
118 | str = "\n" + indent(str) + "\n" if ind
119 | '{' + str + '}'
120 |
121 | export def parenthesize str
122 | '(' + String(str) + ')'
123 |
124 | export def unionOfLocations *locs
125 | var a = Infinity
126 | var b = -Infinity
127 |
128 | for loc in locs
129 | if loc and loc.@loc != undefined
130 | loc = loc.@loc
131 |
132 | if loc and loc:loc isa Function
133 | loc = loc.loc
134 |
135 | if loc isa Array
136 | a = loc[0] if a > loc[0]
137 | b = loc[1] if b < loc[0]
138 | elif loc isa Number
139 | a = loc if a > loc
140 | b = loc if b < loc
141 |
142 | return [a,b]
143 |
144 |
145 |
146 | export def locationToLineColMap code
147 | var lines = code.split(/\n/g)
148 | var map = []
149 |
150 | var chr
151 | var loc = 0
152 | var col = 0
153 | var line = 0
154 |
155 | while chr = code[loc]
156 | map[loc] = [line,col]
157 |
158 | if chr == '\n'
159 | line++
160 | col = 0
161 | else
162 | col++
163 |
164 | loc++
165 |
166 | return map
167 |
168 | export def markLineColForTokens tokens, code
169 | self
170 |
171 | export def parseArgs argv, o = {}
172 | var aliases = o:alias ||= {}
173 | var groups = o:groups ||= []
174 | var schema = o:schema || {}
175 |
176 | schema:main = {}
177 |
178 | var options = {}
179 | var explicit = {}
180 | argv = argv || process:argv.slice(2)
181 | var curr = null
182 | var i = 0
183 | var m
184 |
185 | while(i < argv:length)
186 | var arg = argv[i]
187 | i++
188 |
189 | if m = arg.match(/^\-([a-zA-Z]+)$/)
190 | curr = null
191 | let chars = m[1].split('')
192 |
193 | for item,i in chars
194 | # console.log "parsing {item} at {i}",aliases
195 | var key = aliases[item] or item
196 | chars[i] = key
197 | options[key] = yes
198 |
199 | if chars:length == 1
200 | curr = chars
201 |
202 | elif m = arg.match(/^\-\-([a-z0-9\-\_A-Z]+)$/)
203 | var val = true
204 | var key = m[1]
205 |
206 | if key.indexOf('no-') == 0
207 | key = key.substr(3)
208 | val = false
209 |
210 | for g in groups
211 | if key.substr(0,g:length) == g
212 | console.log 'should be part of group'
213 |
214 | key = dashToCamelCase(key)
215 |
216 | options[key] = val
217 | curr = key
218 |
219 | else
220 | var desc = schema[curr]
221 |
222 | unless curr and schema[curr]
223 | curr = 'main'
224 |
225 | if arg.match(/^\d+$/)
226 | arg = parseInt(arg)
227 |
228 | var val = options[curr]
229 | if val == true or val == false
230 | options[curr] = arg
231 | elif val isa String or val isa Number
232 | options[curr] = [val].concat(arg)
233 | elif val isa Array
234 | val.push(arg)
235 | else
236 | options[curr] = arg
237 |
238 | unless desc and desc:multi
239 | curr = 'main'
240 |
241 | if options:env isa String
242 | options["ENV_{options:env}"] = yes
243 |
244 | return options
245 |
246 | export def printExcerpt code, loc, hl: no, gutter: yes, type: 'warn', pad: 2
247 | var lines = code.split(/\n/g)
248 | var locmap = locationToLineColMap(code)
249 | var lc = locmap[loc[0]] or [0,0]
250 | var ln = lc[0]
251 | var col = lc[1]
252 | var line = lines[ln]
253 |
254 | var ln0 = Math.max(0,ln - pad)
255 | var ln1 = Math.min(ln0 + pad + 1 + pad,lines:length)
256 | let lni = ln - ln0
257 | var l = ln0
258 |
259 | var out = while l < ln1
260 | lines[l++]
261 |
262 | if gutter
263 | out = out.map do |line,i|
264 | let prefix = "{ln0 + i + 1}"
265 | let str
266 | while prefix:length < String(ln1):length
267 | prefix = " {prefix}"
268 | if i == lni
269 | str = " -> {prefix} | {line}"
270 | str = ansi.f(hl,str) if hl
271 | else
272 | str = " {prefix} | {line}"
273 | str = ansi.f('gray',str) if hl
274 | return str
275 |
276 | # if colors isa String
277 | # out[lni] = ansi.f(colors,out[lni])
278 | # elif colors
279 | # let color = ansi[type] or ansi:red
280 | # out[lni] = color(out[lni])
281 |
282 | let res = out.join('\n')
283 | return res
284 |
285 | export def printWarning code, warn
286 | let msg = warn:message # b("{yellow('warn: ')}") + yellow(warn:message)
287 | let excerpt = printExcerpt(code,warn:loc, hl: 'whiteBright', type: 'warn', pad: 1)
288 | return msg + '\n' + excerpt
289 |
290 |
291 |
292 |
293 |
--------------------------------------------------------------------------------
/lib/terminal/runner.js:
--------------------------------------------------------------------------------
1 | var Imba = require('imba'), self = {};
2 |
3 | try {
4 | var pty = require('node-pty');
5 | } catch (e) {
6 | console.log("could not load pty",e);
7 | };
8 |
9 | var path = require("path");
10 | var fs = require("fs");
11 |
12 | // var msgpack = require "msgpack"
13 | var Terminal = require("./terminal").Terminal;
14 | var TerminalModel = require("./model").TerminalModel;
15 |
16 | function CommandRunner(terminal,options){
17 | var self = this;
18 | if(options === undefined) options = {};
19 | self._cwd = options.cwd;
20 | self._terminal = terminal;
21 | self._terminal.io.sendString = function(data) { return self.sendString(data); };
22 | self._vt = self._terminal.vt();
23 | self._vt.characterEncoding = 'raw';
24 | self._child = null;
25 | self._pty = null;
26 | };
27 |
28 | exports.CommandRunner = CommandRunner; // export class
29 | CommandRunner.prototype.terminal = function(v){ return this._terminal; }
30 | CommandRunner.prototype.setTerminal = function(v){ this._terminal = v; return this; };
31 |
32 | CommandRunner.prototype.ontick = function (){
33 | // do nothing
34 | };
35 |
36 | CommandRunner.prototype.onend = function (){
37 | // do nothing
38 | };
39 |
40 | CommandRunner.prototype.sendString = function (data){
41 | if (!(this.isRunning())) {
42 | throw new Error("No command attached");
43 | };
44 |
45 | return (this._pty || this._child.stdin).write(data);
46 | };
47 |
48 | CommandRunner.prototype.isRunning = function (){
49 | return (this._pty || this._child) != null;
50 | };
51 |
52 | CommandRunner.prototype.kill = function (){
53 | if (this.isRunning()) { this._child.kill("SIGHUP") };
54 | return this;
55 | };
56 |
57 | CommandRunner.prototype.run = function (command,args,options){
58 | var self = this;
59 | if(options === undefined) options = {};
60 | if (self.isRunning()) {
61 | throw new Error("Existing command is already attached");
62 | };
63 |
64 | if (!pty) {
65 | return null;
66 | };
67 |
68 | var ptyOptions = Object.assign({},options,{
69 | rows: self.terminal().height(),
70 | columns: self.terminal().width(),
71 | cols: self.terminal().width(),
72 | cwd: self._cwd || options.cwd || process.env.cwd,
73 | env: process.env
74 | });
75 |
76 | // console.log "start pty",shell,command
77 | self._pty = pty.spawn(command,args,ptyOptions);
78 |
79 | (self._pty || self._child.stdout).on('data',function(data) {
80 | if (process.env.DEBUG) {
81 | fs.appendFileSync(__dirname + '/../../logs/terminal.log',JSON.stringify(data.toString('utf8')) + '\n');
82 | };
83 | self._vt.interpret(data.toString('utf8'));
84 | return self.ontick();
85 | });
86 |
87 | false && self._child.stdout.once('end',function() {
88 | self._child = null;
89 | // TODO: reset?
90 | return self.onend();
91 | });
92 |
93 | return self;
94 | };
95 |
96 | function Runner(owner,options){
97 | var self = this;
98 | self._owner = owner;
99 | self._options = options;
100 | self._cwd = options.cwd;
101 |
102 | self._terminal = new Terminal(options);
103 | self._model = new TerminalModel(self._terminal.width(),self._terminal.height());
104 |
105 | self._index = 0;
106 | self._receiver = null;
107 |
108 | if (pty) {
109 | self._cmdRunner = new CommandRunner(self._terminal,{cwd: self._cwd});
110 | self._cmdRunner.ontick = function() { return self.didChange(); };
111 | self._cmdRunner.onend = function() { return self.didEnd(); };
112 | };
113 | self;
114 | };
115 |
116 | exports.Runner = Runner; // export class
117 | Runner.prototype.options = function(v){ return this._options; }
118 | Runner.prototype.setOptions = function(v){ this._options = v; return this; };
119 |
120 | Runner.prototype.isSupported = function (){
121 | return !!pty;
122 | };
123 |
124 | Runner.prototype.run = function (command,args,options){
125 | if(options === undefined) options = {};
126 | return this._cmdRunner && this._cmdRunner.run && this._cmdRunner.run(command,args,{cwd: this._cwd});
127 | };
128 |
129 | Runner.prototype.write = function (string){
130 | return this._cmdRunner && this._cmdRunner.sendString && this._cmdRunner.sendString(string);
131 | };
132 |
133 | Runner.prototype.kill = function (){
134 | return this._cmdRunner && this._cmdRunner.kill && this._cmdRunner.kill();
135 | };
136 |
137 | Runner.prototype.send = function (obj){
138 | if(obj === undefined) obj = {};
139 | obj.index = this._index++;
140 | Imba.emit(this,'message',[obj]);
141 |
142 | if (this._receiver) {
143 | var binary = this.msgpack().pack(obj);
144 | return this._receiver.send(binary);
145 | };
146 | };
147 |
148 | Runner.prototype.sendState = function (){
149 | return this.send(this.state());
150 | };
151 |
152 | Runner.prototype.state = function (){
153 | return {
154 | type: 'state',
155 | connectUrl: this.options().connectUrl,
156 | isRunning: this._cmdRunner ? this._cmdRunner.isRunning() : false,
157 | screenIndex: this._terminal.screenIndex()
158 | };
159 | };
160 |
161 | Runner.prototype.didChange = function (){
162 | var self = this;
163 | clearTimeout(self._flushTimeout);
164 | self._flushTimeout = setTimeout(function() { return self.flushChanges(); },5);
165 | // flushChanges
166 | return self;
167 | };
168 |
169 | Runner.prototype.flushChanges = function (){
170 | var patch = this._model.createPatch(this._terminal.primaryScreen(),this._terminal.alternateScreen());
171 | this._model.applyPatch(patch);
172 | // console.log "flushChanges",JSON.stringify(patch)
173 | return this.send(
174 | {type: 'update',
175 | patch: patch,
176 | row: this._terminal.screen().row(),
177 | column: this._terminal.screen().column(),
178 | screenIndex: this._terminal.screenIndex()}
179 | );
180 | };
181 |
182 | Runner.prototype.didEnd = function (){
183 | return this.sendState();
184 | };
185 |
186 | Runner.prototype.receive = function (msg){
187 | if (!this._cmdRunner) { return };
188 |
189 | switch (msg.type) {
190 | case "cmd.run": {
191 | this._cmdRunner.run(msg.cmd,msg.args,{cwd: this._cwd});
192 | return this.sendState();
193 | break;
194 | }
195 | case "stdin.write": {
196 | return this._cmdRunner.sendString(msg.content);
197 | break;
198 | }
199 | default:
200 |
201 | return console.log(("Unknown command: " + msg));
202 |
203 | };
204 | };
205 |
206 | Runner.prototype.bind = function (ws){
207 | var self = this;
208 | self._receiver = ws;
209 |
210 | ws.on("message",function(binary) {
211 | if (self._receiver !== ws) {
212 | // Err! Received message on old connection
213 | ws.close();
214 | return;
215 | };
216 |
217 | var buffer = Buffer.from(binary);
218 | var msg = self.msgpack().unpack(buffer);
219 | return self.receive(msg);
220 | });
221 |
222 | ws.on("close",function() {
223 | if (self._receiver !== ws) {
224 | return;
225 | };
226 | return self._receiver = null;
227 | });
228 |
229 | return self.sendState();
230 | };
231 |
232 | exports.uuid = self.uuid = function (a,b){
233 | b = a = '';
234 | while (a++ < 36){
235 | b += (a * 51 & 52) ? ((a ^ 15) ? (8 ^ Math.random() * ((a ^ 20) ? 16 : 4)) : 4).toString(16) : '-';
236 | };
237 | return b;
238 | };
239 |
240 | function MultiRunner(options){
241 | this._options = options;
242 | this._tmpdir = options.tmpdir;
243 | this._baseurl = options.baseurl || "";
244 | if (!this._tmpdir) {
245 | throw new Error("tmpdir is required");
246 | };
247 | this._runners = {};
248 | };
249 |
250 | exports.MultiRunner = MultiRunner; // export class
251 | MultiRunner.prototype.findRunner = function (id){
252 | return this._runners[id];
253 | };
254 |
255 | MultiRunner.prototype.createRunner = function (){
256 | var id = this.uuid();
257 | var runnerDir = path.join(this._tmpdir,id);
258 | fs.mkdirSync(runnerDir);
259 | var cwd = path.join(runnerDir,"gitspeak");
260 | fs.mkdirSync(cwd);
261 | this._runners[id] = new Runner(
262 | {width: this._options.width,
263 | height: this._options.height,
264 | cwd: cwd,
265 | connectUrl: ("" + (this._baseurl) + id)}
266 | );
267 | return id;
268 | };
269 |
--------------------------------------------------------------------------------
/lib/terminal/model.js:
--------------------------------------------------------------------------------
1 | function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; };
2 | var buffer$ = require("./buffer"), Line = buffer$.Line, ScreenBuffer = buffer$.ScreenBuffer, TextStyling = buffer$.TextStyling;
3 |
4 | var REPLACE_LINE = 0;
5 | var PUSH_LINE = 1;
6 |
7 | function Palette(startAt){
8 | if(startAt === undefined) startAt = 1;
9 | this._startAt = startAt;
10 | this._index = startAt;
11 | this._mapping = {};
12 | this._values = [];
13 | };
14 |
15 | exports.Palette = Palette; // export class
16 | Palette.prototype.index = function(v){ return this._index; }
17 | Palette.prototype.setIndex = function(v){ this._index = v; return this; };
18 | Palette.prototype.values = function(v){ return this._values; }
19 | Palette.prototype.setValues = function(v){ this._values = v; return this; };
20 |
21 | Palette.prototype.findIndex = function (value){
22 | return this._mapping[value.toString()];
23 | };
24 |
25 | Palette.prototype.insert = function (value){
26 | Object.freeze(value);
27 | var idx = this._index++;
28 | this._mapping[value.toString()] = idx;
29 | this._values.push(value);
30 | return idx;
31 | };
32 |
33 | Palette.prototype.append = function (values){
34 | for (let i = 0, items = iter$(values), len = items.length; i < len; i++) {
35 | this.insert(items[i]);
36 | };
37 | return this;
38 | };
39 |
40 | Palette.prototype.findIndexOrInsert = function (value){
41 | return this.findIndex(value) || this.insert(value);
42 | };
43 |
44 | Palette.prototype.lookup = function (idx){
45 | var value = this._values[idx - this._startAt];
46 | if (!value) {
47 | throw new Error(("No value with index=" + idx));
48 | };
49 | return value;
50 | };
51 |
52 |
53 | function TerminalModel(width,height){
54 | this._width = width;
55 | this._primaryScreen = new ScreenBuffer(width,height);
56 | this._alternateScreen = new ScreenBuffer(width,height);
57 | this._stylingPalette = new Palette();
58 |
59 | this._primaryScreen.clear(TextStyling.DEFAULT);
60 | this._alternateScreen.clear(TextStyling.DEFAULT);
61 |
62 | this._screens = [this._primaryScreen,this._alternateScreen];
63 | };
64 |
65 | exports.TerminalModel = TerminalModel; // export class
66 | TerminalModel.prototype.primaryScreen = function(v){ return this._primaryScreen; }
67 | TerminalModel.prototype.setPrimaryScreen = function(v){ this._primaryScreen = v; return this; };
68 | TerminalModel.prototype.alternateScreen = function(v){ return this._alternateScreen; }
69 | TerminalModel.prototype.setAlternateScreen = function(v){ this._alternateScreen = v; return this; };
70 | TerminalModel.prototype.screens = function(v){ return this._screens; }
71 | TerminalModel.prototype.setScreens = function(v){ this._screens = v; return this; };
72 |
73 | TerminalModel.prototype.width = function(v){ return this._width; }
74 | TerminalModel.prototype.setWidth = function(v){ this._width = v; return this; };
75 | TerminalModel.prototype.height = function(v){ return this._height; }
76 | TerminalModel.prototype.setHeight = function(v){ this._height = v; return this; };
77 |
78 | TerminalModel.prototype.encodeRunLength = function (data){
79 | var result = [];
80 | var count = 1;
81 | var value = data[0];
82 |
83 | for (let len = data.length, idx = 1, rd = len - idx; (rd > 0) ? (idx < len) : (idx > len); (rd > 0) ? (idx++) : (idx--)) {
84 | if (value == data[idx]) {
85 | count += 1;
86 | } else {
87 | result.push(count);
88 | result.push(value);
89 | count = 1;
90 | value = data[idx];
91 | };
92 | };
93 | result.push(count);
94 | result.push(value);
95 | return result;
96 | };
97 |
98 | TerminalModel.prototype.decodeRunLength = function (data){
99 | var result = [];
100 | var idx = 0;
101 | while (idx < data.length){
102 | var count = data[idx++];
103 | var value = data[idx++];
104 | while (count--){
105 | result.push(value);
106 | };
107 | };
108 | return result;
109 | };
110 |
111 | TerminalModel.prototype.encodeStyles = function (styles,newPalette){
112 | var self = this;
113 | styles = styles.map(function(styling) {
114 | return self._stylingPalette.findIndex(styling) || newPalette.findIndexOrInsert(styling);
115 | });
116 | styles = self.encodeRunLength(styles);
117 |
118 | if (styles.length == 2 && styles[1] == 1) {
119 | return "";
120 | };
121 |
122 | return styles;
123 | };
124 |
125 | TerminalModel.prototype.decodeStyles = function (styles){
126 | var self = this;
127 | if (styles == null) {
128 | styles = [self._width,1];
129 | };
130 | styles = self.decodeRunLength(styles);
131 | return styles = styles.map(function(idx) {
132 | return self._stylingPalette.lookup(idx);
133 | });
134 | };
135 |
136 | TerminalModel.prototype.diffText = function (old,new$){
137 | var startIdx = 0;
138 | var endNewIdx = new$.length - 1;
139 | var endOldIdx = old.length - 1;
140 |
141 | while (old[startIdx] == new$[startIdx]){
142 | startIdx++;
143 | };
144 |
145 | while (startIdx < endOldIdx && startIdx < endNewIdx && old[endOldIdx] == new$[endNewIdx]){
146 | endNewIdx--;
147 | endOldIdx--;
148 | };
149 |
150 | if (startIdx > 0 || (endNewIdx < new$.length - 1)) {
151 | return [startIdx,endOldIdx + 1,new$.slice(startIdx,endNewIdx + 1)];
152 | } else {
153 | return new$;
154 | };
155 | };
156 |
157 | TerminalModel.prototype.patchText = function (old,patch){
158 | if (typeof patch == 'string') {
159 | return patch;
160 | };
161 |
162 | var start = patch[0];
163 | var stop = patch[1];
164 | return old.slice(0,start) + patch[2] + old.slice(stop);
165 | };
166 |
167 | TerminalModel.prototype.actionsBetween = function (old,new$,newPalette){
168 | var actions = [];
169 |
170 | for (let idx = 0, items = iter$(old.lines()), len = items.length, line; idx < len; idx++) {
171 | line = items[idx];
172 | var newLine = new$.lines()[idx];
173 | if (!newLine) {
174 | // should not happen?
175 | break;
176 | };
177 |
178 | var styles = this.encodeStyles(line.styles(),newPalette);
179 | var newStyles = this.encodeStyles(newLine.styles(),newPalette);
180 |
181 | var sameText = (line.text() == newLine.text());
182 | var sameStyles = (styles.toString() == newStyles.toString());
183 |
184 |
185 | if (sameText && sameStyles) {
186 | // Nothing to do!
187 | continue;
188 | };
189 |
190 | actions.push([
191 | REPLACE_LINE,idx,
192 | sameText ? null : this.diffText(line.text(),newLine.text()),
193 | sameStyles ? null : newStyles
194 | ]);
195 | };
196 |
197 | for (let len = new$.lines().length, idx = old.lines().length, rd = len - idx; (rd > 0) ? (idx < len) : (idx > len); (rd > 0) ? (idx++) : (idx--)) {
198 | var line = new$.lines()[idx];
199 | styles = this.encodeStyles(line.styles(),newPalette);
200 |
201 | var action = [PUSH_LINE,line.text()];
202 | if (styles) {
203 | action.push(styles);
204 | };
205 | actions.push(action);
206 | };
207 |
208 | return actions;
209 | };
210 |
211 | TerminalModel.prototype.applyActions = function (screen,actions){
212 | var v_;
213 | for (let i = 0, items = iter$(actions), len = items.length, action; i < len; i++) {
214 | action = items[i];
215 | if (action[0] == PUSH_LINE) {
216 | var line = new Line(this._width);
217 | line.setText(action[1]);
218 | line.setStyles((action.STYLES || (action.STYLES = this.decodeStyles(action[2]))));
219 | screen.lines().push(line);
220 | } else if (action[0] == REPLACE_LINE) {
221 | var lineIdx = action[1];
222 | line = screen.lines()[lineIdx];
223 | if (action[2] != null) {
224 | action.OLDTEXT = line.text();
225 | line.setText(this.patchText(line.text(),action[2]));
226 | };
227 | if (action[3] != null) {
228 | action.OLDSTYLES = line.styles();
229 | line.setStyles((action.STYLES || (action.STYLES = this.decodeStyles(action[3]))));
230 | };
231 | };
232 | };
233 | ((screen.setVersion(v_ = screen.version() + 1),v_)) - 1;
234 | return this;
235 | };
236 |
237 | TerminalModel.prototype.revertActions = function (screen,actions){
238 | // NOTE: Now the order of actions is not significent.
239 | // In the future we might want to loop backwards
240 | var v_;
241 | for (let i = 0, items = iter$(actions), len = items.length, action; i < len; i++) {
242 | action = items[i];
243 | if (action[0] == PUSH_LINE) {
244 | screen.lines().pop();
245 | } else if (action[0] == REPLACE_LINE) {
246 | var lineIdx = action[1];
247 | var line = screen.lines()[lineIdx];
248 | if (action.OLDTEXT != null) {
249 | line.setText(action.OLDTEXT);
250 | };
251 | if (action.OLDSTYLES != null) {
252 | line.setStyles(action.OLDSTYLES);
253 | };
254 | };
255 | };
256 | ((screen.setVersion(v_ = screen.version() + 1),v_)) - 1;
257 | return this;
258 | };
259 |
260 | TerminalModel.prototype.createPatch = function (newPrimary,newAlternate){
261 | var newPalette = new Palette(this._stylingPalette.index());
262 | var primaryActions = this.actionsBetween(this._primaryScreen,newPrimary,newPalette);
263 | var alternateActions = this.actionsBetween(this._alternateScreen,newAlternate,newPalette);
264 |
265 | if (primaryActions.length == 0 && alternateActions.length == 0) {
266 | return null;
267 | };
268 |
269 | return [newPalette.values(),primaryActions,alternateActions];
270 | };
271 |
272 | TerminalModel.prototype.applyPatch = function (patch){
273 | if (!patch) {
274 | return;
275 | };
276 |
277 | this._stylingPalette.append(patch[0]);
278 | this.applyActions(this._primaryScreen,patch[1]);
279 | return this.applyActions(this._alternateScreen,patch[2]);
280 | };
281 |
282 | TerminalModel.prototype.revertPatch = function (patch){
283 | if (!patch) {
284 | return;
285 | };
286 |
287 | this.revertActions(this._primaryScreen,patch[1]);
288 | return this.revertActions(this._alternateScreen,patch[2]);
289 | };
290 |
--------------------------------------------------------------------------------
/lib/helpers.js:
--------------------------------------------------------------------------------
1 | function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; };
2 | var self = {};
3 |
4 | var ansiMap = {
5 | reset: [0,0],
6 | bold: [1,22],
7 | dim: [2,22],
8 | italic: [3,23],
9 | underline: [4,24],
10 | inverse: [7,27],
11 | hidden: [8,28],
12 | strikethrough: [9,29],
13 |
14 | black: [30,39],
15 | red: [31,39],
16 | green: [32,39],
17 | yellow: [33,39],
18 | blue: [34,39],
19 | magenta: [35,39],
20 | cyan: [36,39],
21 | white: [37,39],
22 | gray: [90,39],
23 |
24 | redBright: [91,39],
25 | greenBright: [92,39],
26 | yellowBright: [93,39],
27 | blueBright: [94,39],
28 | magentaBright: [95,39],
29 | cyanBright: [96,39],
30 | whiteBright: [97,39]
31 | };
32 |
33 | var ansi = exports.ansi = {
34 | bold: function(text) { return '\u001b[1m' + text + '\u001b[22m'; },
35 | red: function(text) { return '\u001b[31m' + text + '\u001b[39m'; },
36 | green: function(text) { return '\u001b[32m' + text + '\u001b[39m'; },
37 | yellow: function(text) { return '\u001b[33m' + text + '\u001b[39m'; },
38 | gray: function(text) { return '\u001b[90m' + text + '\u001b[39m'; },
39 | white: function(text) { return '\u001b[37m' + text + '\u001b[39m'; },
40 | f: function(name,text) {
41 | let pair = ansiMap[name];
42 | return '\u001b[' + pair[0] + 'm' + text + '\u001b[' + pair[1] + 'm';
43 | }
44 | };
45 |
46 | ansi.warn = ansi.yellow;
47 | ansi.error = ansi.red;
48 |
49 | exports.brace = self.brace = function (str){
50 | var lines = str.match(/\n/);
51 | // what about indentation?
52 |
53 | if (lines) {
54 | return '{' + str + '\n}';
55 | } else {
56 | return '{\n' + str + '\n}';
57 | };
58 | };
59 |
60 | exports.normalizeIndentation = self.normalizeIndentation = function (str){
61 | var m;
62 | var reg = /\n+([^\n\S]*)/g;
63 | var ind = null;
64 |
65 | var length_;while (m = reg.exec(str)){
66 | var attempt = m[1];
67 | if (ind === null || 0 < (length_ = attempt.length) && length_ < ind.length) {
68 | ind = attempt;
69 | };
70 | };
71 |
72 | if (ind) { str = str.replace(RegExp(("\\n" + ind),"g"),'\n') };
73 | return str;
74 | };
75 |
76 |
77 | exports.flatten = self.flatten = function (arr){
78 | var out = [];
79 | arr.forEach(function(v) { return (v instanceof Array) ? out.push.apply(out,self.flatten(v)) : out.push(v); });
80 | return out;
81 | };
82 |
83 |
84 | exports.pascalCase = self.pascalCase = function (str){
85 | return str.replace(/(^|[\-\_\s])(\w)/g,function(m,v,l) { return l.toUpperCase(); });
86 | };
87 |
88 | exports.camelCase = self.camelCase = function (str){
89 | str = String(str);
90 | // should add shortcut out
91 | return str.replace(/([\-\_\s])(\w)/g,function(m,v,l) { return l.toUpperCase(); });
92 | };
93 |
94 | exports.dashToCamelCase = self.dashToCamelCase = function (str){
95 | str = String(str);
96 | if (str.indexOf('-') >= 0) {
97 | // should add shortcut out
98 | str = str.replace(/([\-\s])(\w)/g,function(m,v,l) { return l.toUpperCase(); });
99 | };
100 | return str;
101 | };
102 |
103 | exports.snakeCase = self.snakeCase = function (str){
104 | var str = str.replace(/([\-\s])(\w)/g,'_');
105 | return str.replace(/()([A-Z])/g,"_$1",function(m,v,l) { return l.toUpperCase(); });
106 | };
107 |
108 | exports.setterSym = self.setterSym = function (sym){
109 | return self.dashToCamelCase(("set-" + sym));
110 | };
111 |
112 | exports.quote = self.quote = function (str){
113 | return '"' + str + '"';
114 | };
115 |
116 | exports.singlequote = self.singlequote = function (str){
117 | return "'" + str + "'";
118 | };
119 |
120 | exports.symbolize = self.symbolize = function (str){
121 | str = String(str);
122 | var end = str.charAt(str.length - 1);
123 |
124 | if (end == '=') {
125 | str = 'set' + str[0].toUpperCase() + str.slice(1,-1);
126 | };
127 |
128 | if (str.indexOf("-") >= 0) {
129 | str = str.replace(/([\-\s])(\w)/g,function(m,v,l) { return l.toUpperCase(); });
130 | };
131 |
132 | return str;
133 | };
134 |
135 |
136 | exports.indent = self.indent = function (str){
137 | return String(str).replace(/^/g,"\t").replace(/\n/g,"\n\t").replace(/\n\t$/g,"\n");
138 | };
139 |
140 | exports.bracketize = self.bracketize = function (str,ind){
141 | if(ind === undefined) ind = true;
142 | if (ind) { str = "\n" + self.indent(str) + "\n" };
143 | return '{' + str + '}';
144 | };
145 |
146 | exports.parenthesize = self.parenthesize = function (str){
147 | return '(' + String(str) + ')';
148 | };
149 |
150 | exports.unionOfLocations = self.unionOfLocations = function (){
151 | var $0 = arguments, i = $0.length;
152 | var locs = new Array(i>0 ? i : 0);
153 | while(i>0) locs[i-1] = $0[--i];
154 | var a = Infinity;
155 | var b = -Infinity;
156 |
157 | for (let i = 0, items = iter$(locs), len = items.length, loc; i < len; i++) {
158 | loc = items[i];
159 | if (loc && loc._loc != undefined) {
160 | loc = loc._loc;
161 | };
162 |
163 | if (loc && (loc.loc instanceof Function)) {
164 | loc = loc.loc();
165 | };
166 |
167 | if (loc instanceof Array) {
168 | if (a > loc[0]) { a = loc[0] };
169 | if (b < loc[0]) { b = loc[1] };
170 | } else if ((typeof loc=='number'||loc instanceof Number)) {
171 | if (a > loc) { a = loc };
172 | if (b < loc) { b = loc };
173 | };
174 | };
175 |
176 | return [a,b];
177 | };
178 |
179 |
180 |
181 | exports.locationToLineColMap = self.locationToLineColMap = function (code){
182 | var lines = code.split(/\n/g);
183 | var map = [];
184 |
185 | var chr;
186 | var loc = 0;
187 | var col = 0;
188 | var line = 0;
189 |
190 | while (chr = code[loc]){
191 | map[loc] = [line,col];
192 |
193 | if (chr == '\n') {
194 | line++;
195 | col = 0;
196 | } else {
197 | col++;
198 | };
199 |
200 | loc++;
201 | };
202 |
203 | return map;
204 | };
205 |
206 | exports.markLineColForTokens = self.markLineColForTokens = function (tokens,code){
207 | return self;
208 | };
209 |
210 | exports.parseArgs = self.parseArgs = function (argv,o){
211 | var env_;
212 | if(o === undefined) o = {};
213 | var aliases = o.alias || (o.alias = {});
214 | var groups = o.groups || (o.groups = []);
215 | var schema = o.schema || {};
216 |
217 | schema.main = {};
218 |
219 | var options = {};
220 | var explicit = {};
221 | argv = argv || process.argv.slice(2);
222 | var curr = null;
223 | var i = 0;
224 | var m;
225 |
226 | while ((i < argv.length)){
227 | var arg = argv[i];
228 | i++;
229 |
230 | if (m = arg.match(/^\-([a-zA-Z]+)$/)) {
231 | curr = null;
232 | let chars = m[1].split('');
233 |
234 | for (let i = 0, items = iter$(chars), len = items.length, item; i < len; i++) {
235 | // console.log "parsing {item} at {i}",aliases
236 | item = items[i];
237 | var key = aliases[item] || item;
238 | chars[i] = key;
239 | options[key] = true;
240 | };
241 |
242 | if (chars.length == 1) {
243 | curr = chars;
244 | };
245 | } else if (m = arg.match(/^\-\-([a-z0-9\-\_A-Z]+)$/)) {
246 | var val = true;
247 | key = m[1];
248 |
249 | if (key.indexOf('no-') == 0) {
250 | key = key.substr(3);
251 | val = false;
252 | };
253 |
254 | for (let j = 0, items = iter$(groups), len = items.length, g; j < len; j++) {
255 | g = items[j];
256 | if (key.substr(0,g.length) == g) {
257 | console.log('should be part of group');
258 | };
259 | };
260 |
261 | key = self.dashToCamelCase(key);
262 |
263 | options[key] = val;
264 | curr = key;
265 | } else {
266 | var desc = schema[curr];
267 |
268 | if (!(curr && schema[curr])) {
269 | curr = 'main';
270 | };
271 |
272 | if (arg.match(/^\d+$/)) {
273 | arg = parseInt(arg);
274 | };
275 |
276 | val = options[curr];
277 | if (val == true || val == false) {
278 | options[curr] = arg;
279 | } else if ((typeof val=='string'||val instanceof String) || (typeof val=='number'||val instanceof Number)) {
280 | options[curr] = [val].concat(arg);
281 | } else if (val instanceof Array) {
282 | val.push(arg);
283 | } else {
284 | options[curr] = arg;
285 | };
286 |
287 | if (!(desc && desc.multi)) {
288 | curr = 'main';
289 | };
290 | };
291 | };
292 |
293 | if ((typeof (env_ = options.env)=='string'||env_ instanceof String)) {
294 | options[("ENV_" + (options.env))] = true;
295 | };
296 |
297 | return options;
298 | };
299 |
300 | exports.printExcerpt = self.printExcerpt = function (code,loc,pars){
301 | if(!pars||pars.constructor !== Object) pars = {};
302 | var hl = pars.hl !== undefined ? pars.hl : false;
303 | var gutter = pars.gutter !== undefined ? pars.gutter : true;
304 | var type = pars.type !== undefined ? pars.type : 'warn';
305 | var pad = pars.pad !== undefined ? pars.pad : 2;
306 | var lines = code.split(/\n/g);
307 | var locmap = self.locationToLineColMap(code);
308 | var lc = locmap[loc[0]] || [0,0];
309 | var ln = lc[0];
310 | var col = lc[1];
311 | var line = lines[ln];
312 |
313 | var ln0 = Math.max(0,ln - pad);
314 | var ln1 = Math.min(ln0 + pad + 1 + pad,lines.length);
315 | let lni = ln - ln0;
316 | var l = ln0;
317 |
318 | var res1 = [];while (l < ln1){
319 | res1.push(lines[l++]);
320 | };var out = res1;
321 |
322 | if (gutter) {
323 | out = out.map(function(line,i) {
324 | let prefix = ("" + (ln0 + i + 1));
325 | let str;
326 | while (prefix.length < String(ln1).length){
327 | prefix = (" " + prefix);
328 | };
329 | if (i == lni) {
330 | str = (" -> " + prefix + " | " + line);
331 | if (hl) { str = ansi.f(hl,str) };
332 | } else {
333 | str = (" " + prefix + " | " + line);
334 | if (hl) { str = ansi.f('gray',str) };
335 | };
336 | return str;
337 | });
338 | };
339 |
340 | // if colors isa String
341 | // out[lni] = ansi.f(colors,out[lni])
342 | // elif colors
343 | // let color = ansi[type] or ansi:red
344 | // out[lni] = color(out[lni])
345 |
346 | let res = out.join('\n');
347 | return res;
348 | };
349 |
350 | exports.printWarning = self.printWarning = function (code,warn){
351 | let msg = warn.message; // b("{yellow('warn: ')}") + yellow(warn:message)
352 | let excerpt = self.printExcerpt(code,warn.loc,{hl: 'whiteBright',type: 'warn',pad: 1});
353 | return msg + '\n' + excerpt;
354 | };
355 |
356 |
357 |
358 |
359 |
--------------------------------------------------------------------------------
/src/fs.imba:
--------------------------------------------------------------------------------
1 | import {Component} from './component'
2 | import {Git,getGitInfo} from './git'
3 |
4 | var fs = require 'original-fs'
5 | var path = require 'path'
6 | var fspath = path
7 | var ibn = require 'isbinaryfile'
8 | var posix = path:posix
9 |
10 | var watch = require 'node-watch'
11 |
12 | var SKIP_LIST = ['node_modules','.git','.DS_Store',/\.swp$/,/\w\~$/]
13 |
14 | var FLAGS =
15 | UNSAVED: 1
16 | UNTRACKED: 2
17 | IGNORED: 4
18 | LAZY: 8
19 | BINARY: 16
20 | SYMLINK: 32
21 | MODIFIED: 64
22 | ADDED: 128
23 | RENAMED: 256
24 | COPIED: 512
25 | DELETED: 1024
26 |
27 | ###
28 | M Modified,
29 | A Added,
30 | D Deleted,
31 | R Renamed,
32 | C Copied,
33 | . Unchanged
34 | ? Untracked
35 | ! Ignored
36 | ###
37 |
38 | ###
39 | We should first include files high up in the hierarchy - and when we hit a
40 | certain limit start to lazy-load files, by including references to them, but not
41 | the content (until user requests the content)
42 |
43 | The most important files to include are:
44 | 1. nodes at top level
45 | 2. files with unstaged changes - and their directories
46 |
47 | If there are files with unstaged changes deeply nested in directories
48 | we don't need to prefetch their siblings, but rather mark their outer
49 | directories as lazy-loaded.
50 |
51 | ###
52 |
53 | export def fstat dir
54 |
55 | let node = {
56 | cwd: dir
57 | name: path.basename(dir)
58 | type: 'mdir'
59 | }
60 | if let git = getGitInfo(dir)
61 | node:git = git
62 | node:type = 'repo'
63 |
64 | return node
65 |
66 | export class FileSystem < Component
67 | prop root
68 | prop watcher
69 | prop git
70 | prop cwd
71 | prop extfs
72 | prop baseRef
73 |
74 | def initialize owner,options,extfs
75 |
76 | @owner = owner
77 | @options = options
78 | @extfs = extfs or fs
79 | # @cwd = path.resolve(cwd)
80 | @cwd = options:cwd
81 | @baseRef = options:baseRef || 'HEAD'
82 |
83 | console.log "FS mount {@cwd} {@baseRef}"
84 | # difference between fully ignored files, and just eagerly loaded ones?
85 | @folders = {}
86 | @entries = {}
87 | @state = {}
88 | @watchers = {}
89 | @added = []
90 | @contents = {}
91 |
92 | @root = {
93 | cwd: @cwd
94 | name: path.basename(@cwd)
95 | type: 'mdir'
96 | expanded: yes
97 | ref: ref
98 | }
99 |
100 | log "fs root",@root
101 | setup
102 | self
103 |
104 | def setup
105 | # check if this is a repository
106 | @git = Git.new(self,cwd,baseRef: @baseRef)
107 | unless @git.isRepository
108 | return @git = null
109 |
110 | @root:git = @git.summary
111 | @root:type = 'repo'
112 |
113 | # load the list of uncommitted files
114 | # excluding those ignored by git
115 | try
116 | @diff = git.diff(baseRef)
117 | @diff:map ||= {}
118 | catch e
119 | log "error from git diff",e:message
120 |
121 | @git.on('oread') do |oid,body|
122 | # console.log "oread"
123 | emit('oread',oid,body)
124 | self
125 |
126 | def absPath src
127 | path.resolve(cwd,src)
128 |
129 | def relPath src
130 | path.relative(cwd,absPath(src))
131 |
132 | def basename src
133 | path.basename(src)
134 |
135 | def get src, recursive: no, force: no, read: no
136 | let abspath = absPath(src)
137 | let relpath = relPath(src)
138 |
139 | if abspath == cwd
140 | return @root
141 |
142 | let node = @entries[relpath]
143 | let gitobj = git?.tree[relpath]
144 |
145 | if node
146 | log "already added",node
147 | return node
148 |
149 | # check if file exists
150 | unless extfs.existsSync(abspath)
151 | log "src does not exist",abspath
152 | return null
153 |
154 | # possibly add outer directories
155 | # get statistics for this item
156 | let stat = extfs.lstatSync(abspath)
157 |
158 | node = {
159 | type: 'file'
160 | name: basename(abspath)
161 | path: relpath
162 | par: fspath.dirname(relpath)
163 | lazy: yes
164 | flags: FLAGS.LAZY
165 | mask: 0 # FLAGS.LAZY
166 | }
167 |
168 | # do we want to do this immediately, or always through send-diff?
169 | # @diff is not updated if we change baseRef?
170 | if git
171 | # let gitobj = git.tree[relpath]
172 | let gitdiff = @diff:map and @diff:map[relpath]
173 |
174 | if gitobj
175 | if gitobj:oid
176 | # what if it has changed since then?
177 | node:oid = gitobj:oid
178 | node:status = gitobj:status if gitobj:status
179 | # node:mask = gitobj:mask if gitobj:mask
180 |
181 | if gitdiff
182 | node:status = gitdiff:status
183 | # node:mask |= gitdiff:mask
184 | node:baseOid = gitdiff:baseOid
185 | node:oldPath = gitdiff:oldPath
186 |
187 | if !gitobj and !gitdiff
188 | # we can safely assume that this node is ignored in git
189 | # node:mask |= FLAGS.IGNORED
190 | node:status = '!'
191 |
192 |
193 | if stat.isDirectory
194 | node:type = 'dir'
195 |
196 | # go through git-diff to see if this should be marked
197 | # if there are changes inside, make this lookup recursive
198 | let changes = for own cpath,change of @diff:map
199 | unless cpath.indexOf(relpath + path:sep) == 0
200 | continue
201 | recursive = yes
202 | change
203 |
204 | for change in changes
205 | node:status = 'M'
206 | if change:status == '?'
207 | node:status = '?'
208 | break
209 | # or possibly A if there are added files?
210 | # node:imask |= change:mask
211 |
212 |
213 | if stat.isFile
214 | node:size = stat:size
215 | # include body immediately if changed file
216 | if node:status # why fetch this instantly?
217 | await loadNodeBody(node)
218 |
219 | if stat.isSymbolicLink
220 | node:symlink = yes
221 |
222 | register(relpath,node)
223 |
224 | if recursive and node:type == 'dir'
225 | watchDir(node)
226 |
227 | return node
228 |
229 | def loadNodeBody node, emitRead = no
230 | let path = node:path
231 | if node:type == 'file'
232 | let prev = node:body
233 | if !isBinary(node:path) and node:size < 200000
234 | return Promise.new do |resolve,reject|
235 | extfs.readFile(absPath(path),'utf-8') do |err,data|
236 | log "found file content",data,emitRead
237 | let prev = node:body
238 | node:body = @contents[path] = data
239 | node:lazy = no
240 | node:flags = node:flags & (~FLAGS.LAZY)
241 | if node:body != prev and emitRead
242 | emit('read',path,node:body)
243 | resolve(node)
244 | return Promise.resolve(node)
245 |
246 | # node:body = @contents[path] = extfs.readFileSync(absPath(path),'utf-8') or ""
247 | # node:lazy = no
248 | # node:flags = node:flags & (~FLAGS.LAZY)
249 | # # if emitRead
250 | # # console.log "emit nodeBody",path,node:body
251 | # if node:body != prev and emitRead
252 | # emit('read',path,node:body)
253 |
254 | def register path, node
255 | @entries[path] = node
256 | emit('add',path,node)
257 |
258 | def deregister path, node
259 | if let prev = @entries[path]
260 | delete @entries[path]
261 | # also remove items inside path if dir?
262 | emit('rm',path,prev)
263 |
264 | def read path
265 | path = relPath(path)
266 | return if absPath(path) == cwd
267 |
268 | let node = await get(path, recursive: yes)
269 | log "reading",node
270 | if node:lazy
271 | node:lazy = no
272 |
273 | if node:type == 'file'
274 | await loadNodeBody(node,yes)
275 |
276 | elif node:type == 'dir'
277 | watchDir(node)
278 |
279 | return node:body
280 |
281 | def isIgnored fpath
282 | let name = path.basename(fpath)
283 | for item in SKIP_LIST
284 | if item isa RegExp
285 | return yes if item.test(name) or item.test(fpath)
286 | return yes if item == fpath or item == name
287 | return no
288 |
289 | def isBinary path
290 | ibn.sync( absPath(path) )
291 |
292 | def onfsevent dir, event, src
293 | let rel = relPath(src)
294 | console.log "fsevent",event,src
295 | let node = @entries[rel]
296 |
297 | if node and event == 'remove'
298 | # git might still show the file - as deleted?
299 | return deregister(rel)
300 |
301 | if node
302 | # possibly update the status according to git
303 | # refreshModifiedFiles
304 | git?.refreshUntrackedFiles(relPath(dir:path))
305 |
306 | # send update event?!
307 | if !node:lazy
308 | read(rel)
309 | # emit('read',path,body)
310 |
311 | log "updated content!"
312 |
313 | elif !node
314 | # make git refresh the list of untracked files in directory
315 | git?.refreshUntrackedFiles(relPath(dir:path))
316 | get(rel)
317 |
318 | # what if it was deleted??
319 | delay('refreshChanges',100)
320 | return
321 |
322 | def watchDir node
323 | log 'watching dir',node,absPath(node:path)
324 | return self if @watchers[node:path]
325 | @watchers[node:path] = watch(absPath(node:path),self:onfsevent.bind(self,node))
326 |
327 | let abs = absPath(node:path)
328 | for file in extfs.readdirSync(abs)
329 | await get(path.join(abs,file))
330 |
331 | self
332 |
333 | def refreshChanges
334 |
335 | return unless git
336 | # should move into git module instead?
337 |
338 | var changed = {}
339 | var prev = @changes or {}
340 | var prevClone = Object.assign({},prev)
341 | var curr = git.diff(git.baseRef,yes)[:map]
342 |
343 | log "refreshChanges"
344 |
345 | for own src,node of curr
346 | # also include if any oldOid changed?
347 | if !prev[src] or prev[src][:status] != node:status
348 | changed[src] = node
349 | delete prevClone[src]
350 |
351 | # items that went from having changes to not having any?
352 | for src,node of prevClone
353 | changed[src] = {status: '', oid: node:oldOid}
354 |
355 | @changes = curr
356 |
357 | if Object.keys(changed).len > 0
358 | log "diff",changed
359 | for own src,node of changed
360 | # read the new oid as well?
361 | git.read(node:oldOid)
362 |
363 | emit('diff',changed)
364 | return changed
365 |
366 | def start
367 | watchDir(path: '.')
368 | refreshChanges
369 | emit('mounted')
370 |
371 | def write src, body
372 | let abs = absPath(src)
373 | let node = get(src)
374 |
375 | log "fs.write",src,abs
376 |
377 | if node and node:body == body
378 | return self
379 | else
380 | node:ts = Date.now if node
381 | # @contents[relPath(src)] = body
382 | node:body = body
383 | extfs.writeFileSync(abs,body,'utf-8')
384 | return self
385 |
386 |
387 | # path should be relative to root / cwd
388 | def mkdir entry
389 | throw "not implemented"
390 |
391 | def mkfile entry
392 | throw "not implemented"
393 |
394 | def mv src, dest
395 | throw "not implemented"
396 |
397 | def dispose
398 | self
399 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const remoteMain = require('@electron/remote/main');
2 | // remoteMain.enable(window.webContents)
3 | remoteMain.initialize();
4 |
5 | // Modules to control application life and create native browser window
6 | const path = require('path')
7 | var fp = require("find-free-port")
8 | const {app, BrowserWindow,Tray,Menu,session, protocol,ipcMain, clipboard, shell, Notification, dialog} = require('electron')
9 | const fs = require('fs');
10 | const cp = require('child_process');
11 | const origFs = require('original-fs');
12 | const fixPath = require('fix-path');
13 | const log = require('electron-log');
14 | const { autoUpdater } = require("electron-updater");
15 | fixPath();
16 |
17 | const {fstat} = require('./lib/fs');
18 | const menuTemplate = require('./menu')
19 |
20 | const HOST = process.env.GSHOST || 'gitspeak.com';
21 | const DEBUG = process.env.DEBUG;
22 | // const AFFINITY = DEBUG ? 'debug' : 'myAffinity';
23 | // const PARTITION = DEBUG ? 'persist:debug' : 'persist:main';
24 | const PARTITION = 'persist:' + HOST;
25 | const AFFINITY = 'affinity:' + HOST;
26 | // process.noAsar = true;
27 |
28 | const gotTheLock = app.requestSingleInstanceLock();
29 |
30 | // Keep a global reference of the window object, if you don't, the window will
31 | // be closed automatically when the JavaScript object is garbage collected.
32 | let main
33 | let splash
34 | let tunnel
35 | let initialUrl = process.env.INITIAL_URL || '/';
36 | let state = {
37 | tunnelPort: null
38 | };
39 |
40 | var logQueue = [];
41 |
42 | // Start tunnel in separate process to avoid blocking main thread.
43 | // Pages will communicate with this via local websocket server
44 |
45 | // Auto updating stuff
46 | autoUpdater.logger = log;
47 | autoUpdater.logger.transports.file.level = 'info';
48 | log.info('App starting...');
49 |
50 | autoUpdater.on('checking-for-update', () => {
51 | devToolsLog('Checking for update...');
52 | })
53 | autoUpdater.on('update-available', (info) => {
54 | devToolsLog('Update available.');
55 | })
56 | autoUpdater.on('update-not-available', (info) => {
57 | devToolsLog('Update not available.');
58 | })
59 | autoUpdater.on('error', (err) => {
60 | devToolsLog('Error in auto-updater. ' + err);
61 | })
62 | autoUpdater.on('download-progress', (progressObj) => {
63 | let log_message = "Download speed: " + progressObj.bytesPerSecond;
64 | log_message = log_message + ' - Downloaded ' + progressObj.percent + '%';
65 | log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')';
66 | devToolsLog(log_message);
67 | })
68 |
69 | autoUpdater.on('update-downloaded', (info) => {
70 | devToolsLog('Update downloaded');
71 | dialog.showMessageBox({
72 | type: 'info',
73 | title: 'Update downloaded',
74 | message: "We've downloaded a new version of GitSpeak for you, do you want to relaunch the app?",
75 | buttons: ['Sure', 'No']
76 | }, (buttonIndex) => {
77 | if (buttonIndex === 0) {
78 | if(main) main.forceClose = true;
79 | autoUpdater.quitAndInstall();
80 | }
81 | else {
82 | devToolsLog('Do not quit and install');
83 | }
84 | })
85 | });
86 |
87 | function rpc(name,...args){
88 | var doc = main.webContents;
89 | var res = doc.send('message',{type: 'rpc', data: [name,args]});
90 | }
91 |
92 | function send(name,args){
93 | if(main && main.webContents) main.webContents.send('message',{type: name, data: args});
94 | }
95 |
96 | async function setupTunnel(){
97 | state.ports = await fp(48000, 49000, '127.0.0.1', 1);
98 | state.tunnelPort = state.ports[0];
99 |
100 | process.env.TUNNEL_PORT = state.tunnelPort;
101 |
102 | let env = {
103 | PATH: process.env.PATH,
104 | TUNNEL_PORT: state.tunnelPort
105 | };
106 |
107 | ['HOME','TMPDIR','USER'].forEach(function(k){
108 | if(process.env[k]) env[k] = process.env[k];
109 | })
110 |
111 | tunnel = cp.fork('./lib/wss', [], {
112 | env: env,
113 | cwd: __dirname,
114 | silent: true
115 | })
116 |
117 | tunnel.stdout.on('data', (data) => {
118 | devToolsLog(String(data));
119 | });
120 |
121 | tunnel.stderr.on('data', (data) => {
122 | devToolsLog(String("error from tunnel: " + String(data)));
123 | })
124 | }
125 |
126 | function openIDE(params){
127 | console.log('open ide',params);
128 | params.port = state.tunnelPort;
129 | main.webContents.send('message',{type: 'openSession', data: params});
130 | }
131 |
132 |
133 | function devToolsLog(s) {
134 | log.info(s);
135 |
136 | if (main && main.webContents && DEBUG) {
137 | main.webContents.send('message',{type: 'log', data: s});
138 | } else {
139 | logQueue.push(s);
140 | }
141 | }
142 |
143 | var url_scheme = "gitspeak";
144 | protocol.registerSchemesAsPrivileged([
145 | { scheme: url_scheme }
146 | ])
147 | app.setAsDefaultProtocolClient(url_scheme);
148 |
149 | async function setupApplication () {
150 |
151 | let opts = {
152 | width: 420,
153 | height: 280,
154 | title: "GitSpeak",
155 | titleBarStyle: 'hidden',
156 | hasShadow: true,
157 | vibrancy: null,
158 | center: true,
159 | movable: false,
160 | resizable: false,
161 | minimizable: false,
162 | maximizable: false,
163 | closable: false,
164 | alwaysOnTop: true,
165 | show: false
166 | }
167 |
168 | // Create loading-screen that will show until we have loaded GitSpeak
169 | splash = new BrowserWindow(Object.assign({
170 | autoHideMenuBar: true
171 | },opts));
172 |
173 | splash.once('ready-to-show', function(event, url) {
174 | splash.show();
175 | return this;
176 | });
177 |
178 | splash.loadURL(`file://${__dirname}/splash.html`);
179 | await setupTunnel();
180 |
181 | // Create the browser window.
182 | main = new BrowserWindow({
183 | width: 1280,
184 | height: 900,
185 | title: "GitSpeak",
186 | titleBarStyle: 'hidden',
187 | vibrancy: null,
188 | show: false,
189 | webPreferences: {
190 | partition: PARTITION,
191 | preload: path.join(__dirname, 'preload.js'),
192 | nodeIntegration: false,
193 | contextIsolation: false,
194 | nativeWindowOpen: true,
195 | allowRunningInsecureContent: false,
196 | backgroundThrottling: false
197 | }
198 | })
199 |
200 | var menu = Menu.buildFromTemplate(menuTemplate.template);
201 | Menu.setApplicationMenu(menu);
202 | main.loadURL("https://" + HOST + initialUrl);
203 | devToolsLog(logQueue);
204 |
205 | main.setAutoHideMenuBar(true);
206 | main.on('show',function(event){
207 | if(splash){
208 | splash.hide();
209 | splash.destroy();
210 | splash = null;
211 | }
212 | });
213 |
214 | var doc = main.webContents;
215 | remoteMain.enable(doc);
216 | doc.on('will-navigate', function(event, url) {
217 | console.log("will-navigate somewhere?!?",url);
218 | event.preventDefault();
219 | shell.openExternal(url);
220 | return this;
221 | });
222 |
223 | doc.on('new-window', (event, url, frameName, disposition, options, additionalFeatures) => {
224 | console.log('new-window',frameName);
225 | shell.openExternal(url);
226 | return;
227 |
228 | var outerPos = main.getPosition();
229 | var outerSize = main.getSize();
230 |
231 | var defaults = {}
232 |
233 | if(frameName == 'ghlogin'){
234 | shell.openExternal(url);
235 | // Always open externally now
236 | return;
237 | }
238 |
239 | if (true) {
240 | event.preventDefault()
241 | var frameDefaults = defaults[frameName] || {};
242 | Object.assign(options, {
243 | titleBarStyle: 'default',
244 | modal: false,
245 | parent: main,
246 | width: 1020,
247 | height: 790,
248 | resizable: false
249 | },frameDefaults);
250 |
251 | // center over parent window
252 | options.x = outerPos[0] + Math.round((outerSize[0] - options.width) * 0.5);
253 | options.y = outerPos[1] + Math.round((outerSize[1] - options.height) * 0.5);
254 |
255 | options.webPreferences = Object.assign({
256 | preload: path.join(__dirname, 'extwindow.js'),
257 | partition: PARTITION,
258 | affinity: AFFINITY
259 | },
260 | options.webPreferences,
261 | frameDefaults.webPreferences || {}
262 | );
263 |
264 | event.newGuest = new BrowserWindow(options)
265 | }
266 | })
267 |
268 | if (process.platform === 'darwin') {
269 | // when user tries to close main window on mac
270 | // we simply hide it instead, to be able to quicky
271 | // show it again when app is reactivated.
272 | main.on('close', (e) => {
273 | // if main window is closing because the user
274 | // is quitting the app, we don't want to preventDefault
275 | if(main.forceClose) return;
276 | e.preventDefault();
277 | main.hide();
278 | })
279 | }
280 |
281 | // Emitted when the window is closed.
282 | main.on('closed', function () {
283 | main = null;
284 | })
285 | }
286 |
287 |
288 | ipcMain.on("client", function(event, arg) {
289 | console.log("ipcmain app",arg);
290 |
291 | if(arg == 'focus'){
292 | app.focus();
293 | }
294 | });
295 |
296 | ipcMain.on("app_release", function(event, arg) {
297 | autoUpdater.checkForUpdatesAndNotify();
298 | })
299 |
300 | ipcMain.on("state.get", function(event, arg) {
301 | console.log('state.get',arg);
302 | event.returnValue = state[arg];
303 | })
304 |
305 | ipcMain.on("fstat", function(event, dir) {
306 | console.log('fstat',dir);
307 | event.returnValue = fstat(dir);
308 | })
309 |
310 | function setupRequestInterceptor(){
311 | // try to intercept http requests
312 | const filter = {
313 | urls: ['https://sindre.gitspeak.com:8443/*', '*://electron.github.io']
314 | }
315 | main.webContents.session.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
316 | console.log('intercept webRequest!!');
317 | // details.requestHeaders['User-Agent'] = 'MyAgent'
318 | callback({cancel: false, requestHeaders: details.requestHeaders})
319 | })
320 | }
321 |
322 | // This method will be called when Electron has finished
323 | // initialization and is ready to create browser windows.
324 | // Some APIs can only be used after this event occurs.
325 | app.on('ready', setupApplication)
326 | // app.on('ready', setupRequestInterceptor)
327 |
328 | app.on('ready', () => {
329 | autoUpdater.checkForUpdatesAndNotify();
330 | })
331 |
332 | app.on('open-url', (event,url) => {
333 | if(main && main.webContents){
334 | console.log("trying to open url through application! " + url);
335 | send('openUrl',url);
336 | } else {
337 | initialUrl = url.slice(10);
338 | }
339 | })
340 |
341 |
342 | app.on('before-quit-for-update', () => {
343 | if(main) main.forceClose = true;
344 | });
345 |
346 | app.on('before-quit', () => {
347 | if(main) main.forceClose = true;
348 | tunnel.send({type: 'kill'});
349 | tunnel.kill('SIGINT')
350 | });
351 | app.on('will-quit', () => {
352 | console.log('will-quit')
353 | });
354 |
355 | app.on('quit', () => {
356 | console.log('quit')
357 | });
358 |
359 | // Quit when all windows are closed.
360 | app.on('window-all-closed', function () {
361 | console.log('window-all-closed triggered')
362 | // On OS X it is common for applications and their menu bar
363 | // to stay active until the user quits explicitly with Cmd + Q
364 | if (process.platform !== 'darwin') {
365 | app.quit()
366 | }
367 | })
368 |
369 | app.on('activate', function () {
370 |
371 | // On OS X it's common to re-create a window in the app when the
372 | // dock icon is clicked and there are no other windows open.
373 | if (main == null) {
374 | // should not be possible(!)
375 | // createWindow()
376 | } else {
377 | console.log("showing main window");
378 | main.show();
379 | app.focus();
380 | }
381 | })
382 |
383 | // In this file you can include the rest of your app's specific main process
384 | // code. You can also put them in separate files and require them here.
385 |
--------------------------------------------------------------------------------
/src/terminal/terminal.imba:
--------------------------------------------------------------------------------
1 | import lib, hterm from "../../vendor/hterm_vt"
2 | import wc from "../../vendor/lib_wc"
3 |
4 | import ScreenBuffer, TextStyling, getWhitespace, getFill from "./buffer"
5 |
6 | hterm.Terminal ||= {}
7 | hterm.Terminal:cursorShape ||= {
8 | BLOCK: 'BLOCK'
9 | BEAM: 'BEAM'
10 | UNDERLINE: 'UNDERLINE'
11 | }
12 |
13 | class CursorState
14 | def initialize screen
15 | @screen = screen
16 |
17 | def save vt
18 | @cursor = @screen.saveCursor
19 | @textAttributes = @screen.textAttributes.clone
20 | @GL = vt:GL
21 | @GR = vt:GR
22 | @G0 = vt:G0
23 | @G1 = vt:G1
24 | @G2 = vt:G2
25 | @G3 = vt:G3
26 | self
27 |
28 | def restore vt
29 | @screen.restoreCursor(@cursor)
30 | @screen.textAttributes = @textAttributes
31 | vt:GL = @GL
32 | vt:GR = @GR
33 | vt:G0 = @G0
34 | vt:G1 = @G1
35 | vt:G2 = @G2
36 | vt:G3 = @G3
37 | self
38 |
39 |
40 | export class Screen
41 | prop textAttributes
42 | prop width
43 | prop height
44 | prop buffer
45 |
46 | def initialize terminal
47 | @terminal = terminal
48 | @width = terminal.width
49 | @height = terminal.height
50 | @textAttributes = hterm.TextAttributes.new(null)
51 | @cursorState = CursorState.new(self)
52 |
53 | @buffer = ScreenBuffer.new(@width, @height)
54 | @buffer.clear(getStyling)
55 |
56 | def getStyling
57 | TextStyling.fromAttributes(@textAttributes)
58 |
59 | def row
60 | @buffer.row
61 |
62 | def column
63 | @buffer.column
64 |
65 | def setRow row
66 | @buffer.row = row
67 |
68 | def setColumn col
69 | @buffer.column = col
70 |
71 | def lines
72 | @buffer.lines
73 |
74 | def createLine
75 | @buffer.createLine(getStyling)
76 |
77 | def indexForRow row
78 | @buffer.indexForRow(row)
79 |
80 | def lineAtRow row
81 | @buffer.lineAtRow(row)
82 |
83 | def currentLine
84 | lineAtRow(row)
85 |
86 | def pushLine
87 | lines.push(createLine)
88 |
89 | def popLine
90 | lines.pop
91 |
92 | def setScrollRegion top, bottom
93 | @scrollTop = top
94 | @scrollBottom = bottom
95 | self
96 |
97 | def scrollTop
98 | @scrollTop or 0
99 |
100 | def scrollBottom
101 | @scrollBottom or (@height - 1)
102 |
103 | def atTop
104 | row == scrollTop
105 |
106 | def atBottom
107 | row == scrollBottom
108 |
109 | def scrollLines insertRow, deleteRow, count
110 | var insertIdx = indexForRow(insertRow)
111 | var deleteIdx = indexForRow(deleteRow)
112 |
113 | for i in [0 ... count]
114 | lines.splice(insertIdx, 0, createLine)
115 |
116 | if insertIdx < deleteIdx
117 | # Take into account that we have shifted lines
118 | deleteIdx += count
119 |
120 | lines.splice(deleteIdx, count)
121 |
122 | def scrollUp count
123 | scrollLines(scrollBottom + 1, scrollTop, count)
124 |
125 | def scrollDown count
126 | scrollLines(scrollTop, scrollBottom - count + 1, count)
127 |
128 | def insertLines count
129 | scrollLines(row, scrollBottom - count + 1, count)
130 |
131 | def deleteLines count
132 | scrollLines(scrollBottom + 1, row, count);
133 |
134 | def visibleLines
135 | var startIdx = indexForRow(0)
136 | lines.slice(startIdx, startIdx + height)
137 |
138 | def saveCursor
139 | hterm.RowCol.new(row, column)
140 |
141 | def restoreCursor cursor
142 | row = cursor:row
143 | column = cursor:column
144 |
145 | def saveCursorAndState vt
146 | @cursorState.save(vt)
147 |
148 | def restoreCursorAndState vt
149 | @cursorState.restore(vt)
150 |
151 | def cursorUp count = 1
152 | row -= count
153 |
154 | def cursorDown count = 1
155 | row += 1
156 |
157 | def writeText text, insert = no
158 | var styling = getStyling
159 |
160 | while text:length
161 | var availableSpace = width - column
162 | if availableSpace == 0
163 | formFeed
164 |
165 | var fittedText = wc:substr(text, 0, availableSpace)
166 | var textWidth = wc:strWidth(fittedText)
167 | if insert
168 | fittedText += currentLine.substr(column, width - textWidth)
169 | currentLine.replace(column, fittedText, styling)
170 | column += textWidth
171 | text = wc:substr(text, textWidth)
172 | self
173 |
174 | def insertText text
175 | writeText(text, yes)
176 |
177 | def clear
178 | @buffer.clear(getStyling)
179 | self
180 |
181 | def fill char
182 | var styling = getStyling
183 | var text = getFill(width, char)
184 | for i in [0 ... height]
185 | var line = lineAtRow(i)
186 | line.replace(0, text, styling)
187 | self
188 |
189 | def deleteChars count
190 | var line = currentLine
191 | var styling = line.styles[column + count]
192 | line.shiftLeft(column, count, styling)
193 |
194 | def eraseToLeft count = column
195 | currentLine.erase(0, count, getStyling)
196 |
197 | def eraseToRight count = width - column
198 | currentLine.erase(column, count, getStyling)
199 |
200 | def eraseLine
201 | currentLine.clear(getStyling)
202 |
203 | def eraseAbove
204 | var styling = getStyling
205 | eraseToLeft(column + 1)
206 | for i in [0 ... row]
207 | var line = lineAtRow(i)
208 | line.erase(0, width, styling)
209 |
210 | def eraseBelow
211 | var styling = getStyling
212 | eraseToRight
213 | for i in [(row + 1) ... @height]
214 | var line = lineAtRow(i)
215 | line.erase(0, width, styling)
216 |
217 | def formFeed
218 | column = 0
219 | newLine
220 |
221 | def newLine
222 | if @scrollTop != null or @scrollBottom != null
223 | if atBottom
224 | # We're at the bottom in the scroll view
225 | scrollUp(1)
226 | return
227 |
228 | if atBottom
229 | pushLine
230 | else
231 | row += 1
232 |
233 | def lineFeed
234 | newLine
235 |
236 | def reverseLineFeed
237 | if atTop
238 | scrollDown(1)
239 | else
240 | row -= 1
241 |
242 | def reset
243 | # Remove scroll region
244 | setScrollRegion(null, null)
245 |
246 | # Clear screen
247 | clear
248 |
249 | # Reset cursor
250 | row = 0
251 | column = 0
252 | self
253 |
254 |
255 | var toScreen = do |name|
256 | do
257 | var screen = this.@screen
258 | unless screen and screen[name]
259 | console.log "error in terminal ({name})"
260 | return
261 |
262 | screen[name].apply(screen, arguments)
263 |
264 | var toScreenBuffer = do |name|
265 | do
266 | var screen = this.@screen
267 | var buffer = screen.@buffer
268 | buffer[name].apply(buffer, arguments)
269 |
270 | export class Terminal
271 | prop width
272 | prop height
273 | prop screen
274 | prop screens
275 | prop insertMode
276 | prop wraparound
277 | prop cursorBlink
278 | prop cursorShape
279 | prop windowTitle
280 |
281 | prop primaryScreen
282 | prop alternateScreen
283 |
284 | def initialize options
285 | @width = options:width
286 | @height = options:height
287 |
288 | @primaryScreen = Screen.new(self)
289 | @alternateScreen = Screen.new(self)
290 | @screen = @primaryScreen
291 | @screens = [@primaryScreen, @alternateScreen]
292 |
293 | @insertMode = no
294 | @wraparound = yes
295 | @cursorBlink = no
296 | @cursorShape = hterm.Terminal:cursorShape.BLOCK
297 |
298 | @tabWidth = 8
299 | setDefaultTabStops
300 |
301 | self:keyboard = {}
302 | self:io = {
303 | sendString: do # nothing
304 | }
305 | self:screenSize = {
306 | width: @width
307 | height: @height
308 | }
309 |
310 | def screenIndex
311 | @screens.indexOf(@screen)
312 |
313 | def createVT
314 | hterm.VT.new(self)
315 |
316 | def vt
317 | @vt ||= createVT
318 |
319 | def cursorState
320 | [screen.row, screen.column]
321 |
322 | def saveCursorAndState both
323 | if both
324 | @primaryScreen.saveCursorAndState(vt)
325 | @alternateScreen.saveCursorAndState(vt)
326 | else
327 | screen.saveCursorAndState(vt)
328 |
329 | def restoreCursorAndState both
330 | if both
331 | @primaryScreen.restoreCursorAndState(vt)
332 | @alternateScreen.restoreCursorAndState(vt)
333 | else
334 | screen.restoreCursorAndState(vt)
335 |
336 | def setAlternateMode state
337 | var newScreen = state ? @alternateScreen : @primaryScreen
338 | @screen = newScreen
339 |
340 | def getTextAttributes
341 | @screen.textAttributes
342 |
343 | def setTextAttributes attrs
344 | @screen.textAttributes = attrs
345 |
346 | def getForegroundColor
347 | "white"
348 |
349 | def getBackgroundColor
350 | "black"
351 |
352 | # These are not supported by iTerm2. Then I won't bother supporting them.
353 | def setForegroundColor
354 | # noop
355 |
356 | def setBackgroundColor
357 | # noop
358 |
359 | def syncMouseStyle
360 | # noop
361 |
362 | def setBracketedPaste state
363 | # noop
364 |
365 | def ringBell
366 | # noop
367 |
368 | def setOriginMode state
369 | # not supported at the moment
370 | screen.row = 0
371 | screen.column = 0
372 |
373 | def setTabStop column
374 | for stop, idx in @tabStops
375 | if stop > column
376 | @tabStops.splice(0, idx, column)
377 | return
378 |
379 | @tabStops.push(column)
380 | self
381 |
382 | def setDefaultTabStops
383 | @tabStops = []
384 | var column = @tabWidth
385 | while column < screen.width
386 | @tabStops.push(column)
387 | column += @tabWidth
388 | self
389 |
390 | def clearAllTabStops
391 | @tabStops = []
392 | self
393 |
394 | def clearTabStopAtCursor
395 | for stop, idx in @tabStops
396 | if stop == screen.column
397 | @tabStops.splice(idx, 1)
398 | return
399 | self
400 |
401 | def forwardTabStop
402 | for stop in @tabStops
403 | if stop > screen.column
404 | screen.column = stop
405 | return
406 |
407 | screen.column = screen.width - 1
408 |
409 | def backwardTabStop
410 | var lastStop = 0
411 | for stop in @tabStops
412 | if stop > screen.column
413 | break
414 | lastStop = stop
415 |
416 | screen.column = lastStop
417 |
418 | def fill char
419 | screen.fill(char)
420 | # Currently this method is only used by DECALN, but technically
421 | # I want this method to *just* fill the screen, not reset the cursor.
422 | # That requires a fix in hterm.VT: https://bugs.chromium.org/p/chromium/issues/detail?id=811718
423 | screen.column = 0
424 | screen.row = 0
425 | self
426 |
427 | def reset
428 | clearAllTabStops
429 | softReset
430 | self
431 |
432 | def softReset
433 | primaryScreen.reset
434 | alternateScreen.reset
435 | @vt?.reset
436 | self
437 |
438 | def print text
439 | if @insertMode
440 | screen.insertText(text)
441 | else
442 | screen.writeText(text)
443 |
444 | Object.assign(self:prototype,
445 | getCursorRow: toScreenBuffer(:row)
446 | getCursorColumn: toScreenBuffer(:column)
447 | deleteLines: toScreen(:deleteLines)
448 | insertLines: toScreen(:insertLines)
449 | lines: toScreen(:lines)
450 | cursorUp: toScreen(:cursorUp)
451 | cursorDown: toScreen(:cursorDown)
452 |
453 | setCursorColumn: toScreenBuffer(:setColumn)
454 | setAbsoluteCursorRow: toScreen(:setRow)
455 | setCursorVisible: toScreenBuffer(:setCursorVisible)
456 |
457 | setVTScrollRegion: toScreen(:setScrollRegion)
458 | vtScrollUp: toScreen(:scrollUp)
459 |
460 | formFeed: toScreen(:formFeed)
461 | lineFeed: toScreen(:lineFeed)
462 | reverseLineFeed: toScreen(:reverseLineFeed)
463 |
464 | insertSpace: toScreen(:insertSpace)
465 | deleteChars: toScreen(:deleteChars)
466 | clear: toScreen(:clear)
467 | eraseToLeft: toScreen(:eraseToLeft)
468 | eraseToRight: toScreen(:eraseToRight)
469 | eraseLine: toScreen(:eraseLine)
470 | eraseAbove: toScreen(:eraseAbove)
471 | eraseBelow: toScreen(:eraseBelow)
472 | )
473 |
474 | def cursorLeft amount
475 | screen.column -= amount
476 |
477 | def cursorRight amount
478 | screen.column += amount
479 |
480 | def setCursorPosition row, column
481 | screen.row = row
482 | screen.column = column
483 |
484 | def visibleLines
485 | screen.visibleLines
486 |
487 |
488 |
489 | # At the moment we need to support the following functions:
490 |
491 | # [x] backwardTabStop
492 | # [x] clear
493 | # [x] clearAllTabStops
494 | # [ ] clearHome
495 | # [x] clearTabStopAtCursor
496 | # [ ] copyStringToClipboard
497 | # [x] cursorDown
498 | # [x] cursorLeft
499 | # [x] cursorRight
500 | # [x] cursorUp
501 | # [x] deleteChars
502 | # [x] deleteLines
503 | # [ ] displayImage
504 | # [x] eraseAbove
505 | # [x] eraseBelow
506 | # [x] eraseLine
507 | # [x] eraseToLeft
508 | # [x] eraseToRight
509 | # [x] fill
510 | # [x] formFeed
511 | # [x] forwardTabStop
512 | # [x] getBackgroundColor
513 | # [x] getCursorRow
514 | # [x] getForegroundColor
515 | # [x] getTextAttributes
516 | # [x] insertLines
517 | # [x] insertSpace
518 | # [x] lineFeed
519 | # [x] print
520 | # [x] reset
521 | # [x] restoreCursorAndState
522 | # [x] reverseLineFeed
523 | # [x] ringBell
524 | # [x] saveCursorAndState
525 | # [x] setAbsoluteCursorRow
526 | # [x] setAlternateMode
527 | # [ ] setAutoCarriageReturn
528 | # [x] setBackgroundColor
529 | # [x] setBracketedPaste
530 | # [x] setCursorBlink
531 | # [ ] setCursorColor
532 | # [x] setCursorPosition
533 | # [x] setCursorShape
534 | # [x] setCursorVisible
535 | # [x] setForegroundColor
536 | # [x] setInsertMode
537 | # [x] setOriginMode
538 | # [ ] setReverseVideo
539 | # [ ] setReverseWraparound
540 | # [ ] setScrollbarVisible
541 | # [x] setTabStop
542 | # [ ] setTextAttributes
543 | # [x] setVTScrollRegion
544 | # [ ] setWindowTitle
545 | # [ ] setWraparound
546 | # [x] softReset
547 | # [x] syncMouseStyle
548 | # [ ] vtScrollDown
549 | # [x] vtScrollUp
550 |
551 |
--------------------------------------------------------------------------------
/lib/fs.js:
--------------------------------------------------------------------------------
1 | function len$(a){
2 | return a && (a.len instanceof Function ? a.len() : a.length) || 0;
3 | };
4 | function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; };
5 | var self = {}, Imba = require('imba');
6 | var Component = require('./component').Component;
7 | var git$ = require('./git'), Git = git$.Git, getGitInfo = git$.getGitInfo;
8 |
9 | var fs = require('original-fs');
10 | var path = require('path');
11 | var fspath = path;
12 | var ibn = require('isbinaryfile');
13 | var posix = path.posix;
14 |
15 | var watch = require('node-watch');
16 |
17 | var SKIP_LIST = ['node_modules','.git','.DS_Store',/\.swp$/,/\w\~$/];
18 |
19 | var FLAGS = {
20 | UNSAVED: 1,
21 | UNTRACKED: 2,
22 | IGNORED: 4,
23 | LAZY: 8,
24 | BINARY: 16,
25 | SYMLINK: 32,
26 | MODIFIED: 64,
27 | ADDED: 128,
28 | RENAMED: 256,
29 | COPIED: 512,
30 | DELETED: 1024
31 | };
32 |
33 | /*
34 | M Modified,
35 | A Added,
36 | D Deleted,
37 | R Renamed,
38 | C Copied,
39 | . Unchanged
40 | ? Untracked
41 | ! Ignored
42 | */
43 |
44 |
45 | /*
46 | We should first include files high up in the hierarchy - and when we hit a
47 | certain limit start to lazy-load files, by including references to them, but not
48 | the content (until user requests the content)
49 |
50 | The most important files to include are:
51 | 1. nodes at top level
52 | 2. files with unstaged changes - and their directories
53 |
54 | If there are files with unstaged changes deeply nested in directories
55 | we don't need to prefetch their siblings, but rather mark their outer
56 | directories as lazy-loaded.
57 |
58 | */
59 |
60 |
61 | exports.fstat = self.fstat = function (dir){
62 |
63 | var git;
64 | let node = {
65 | cwd: dir,
66 | name: path.basename(dir),
67 | type: 'mdir'
68 | };
69 | if (git = getGitInfo(dir)) {
70 | node.git = git;
71 | node.type = 'repo';
72 | };
73 |
74 | return node;
75 | };
76 |
77 | function FileSystem(owner,options,extfs){
78 |
79 | this._owner = owner;
80 | this._options = options;
81 | this._extfs = extfs || fs;
82 | // @cwd = path.resolve(cwd)
83 | this._cwd = options.cwd;
84 | this._baseRef = options.baseRef || 'HEAD';
85 |
86 | console.log(("FS mount " + (this._cwd) + " " + (this._baseRef)));
87 | // difference between fully ignored files, and just eagerly loaded ones?
88 | this._folders = {};
89 | this._entries = {};
90 | this._state = {};
91 | this._watchers = {};
92 | this._added = [];
93 | this._contents = {};
94 |
95 | this._root = {
96 | cwd: this._cwd,
97 | name: path.basename(this._cwd),
98 | type: 'mdir',
99 | expanded: true,
100 | ref: this.ref()
101 | };
102 |
103 | this.log("fs root",this._root);
104 | this.setup();
105 | this;
106 | };
107 |
108 | Imba.subclass(FileSystem,Component);
109 | exports.FileSystem = FileSystem; // export class
110 | FileSystem.prototype.root = function(v){ return this._root; }
111 | FileSystem.prototype.setRoot = function(v){ this._root = v; return this; };
112 | FileSystem.prototype.watcher = function(v){ return this._watcher; }
113 | FileSystem.prototype.setWatcher = function(v){ this._watcher = v; return this; };
114 | FileSystem.prototype.git = function(v){ return this._git; }
115 | FileSystem.prototype.setGit = function(v){ this._git = v; return this; };
116 | FileSystem.prototype.cwd = function(v){ return this._cwd; }
117 | FileSystem.prototype.setCwd = function(v){ this._cwd = v; return this; };
118 | FileSystem.prototype.extfs = function(v){ return this._extfs; }
119 | FileSystem.prototype.setExtfs = function(v){ this._extfs = v; return this; };
120 | FileSystem.prototype.baseRef = function(v){ return this._baseRef; }
121 | FileSystem.prototype.setBaseRef = function(v){ this._baseRef = v; return this; };
122 |
123 | FileSystem.prototype.setup = function (){
124 | // check if this is a repository
125 | var self = this;
126 | self._git = new Git(self,self.cwd(),{baseRef: self._baseRef});
127 | if (!self._git.isRepository()) {
128 | return self._git = null;
129 | };
130 |
131 | self._root.git = self._git.summary();
132 | self._root.type = 'repo';
133 |
134 | // load the list of uncommitted files
135 | // excluding those ignored by git
136 | try {
137 | self._diff = self.git().diff(self.baseRef());
138 | self._diff.map || (self._diff.map = {});
139 | } catch (e) {
140 | self.log("error from git diff",e.message);
141 | };
142 |
143 | self._git.on('oread',function(oid,body) {
144 | // console.log "oread"
145 | return self.emit('oread',oid,body);
146 | });
147 | return self;
148 | };
149 |
150 | FileSystem.prototype.absPath = function (src){
151 | return path.resolve(this.cwd(),src);
152 | };
153 |
154 | FileSystem.prototype.relPath = function (src){
155 | return path.relative(this.cwd(),this.absPath(src));
156 | };
157 |
158 | FileSystem.prototype.basename = function (src){
159 | return path.basename(src);
160 | };
161 |
162 | FileSystem.prototype.get = async function (src,pars){
163 | var git_;
164 | if(!pars||pars.constructor !== Object) pars = {};
165 | var recursive = pars.recursive !== undefined ? pars.recursive : false;
166 | var force = pars.force !== undefined ? pars.force : false;
167 | var read = pars.read !== undefined ? pars.read : false;
168 | let abspath = this.absPath(src);
169 | let relpath = this.relPath(src);
170 |
171 | if (abspath == this.cwd()) {
172 | return this._root;
173 | };
174 |
175 | let node = this._entries[relpath];
176 | let gitobj = (git_ = this.git()) && git_.tree && git_.tree()[relpath];
177 |
178 | if (node) {
179 | this.log("already added",node);
180 | return node;
181 | };
182 |
183 | // check if file exists
184 | if (!this.extfs().existsSync(abspath)) {
185 | this.log("src does not exist",abspath);
186 | return null;
187 | };
188 |
189 | // possibly add outer directories
190 | // get statistics for this item
191 | let stat = this.extfs().lstatSync(abspath);
192 |
193 | node = {
194 | type: 'file',
195 | name: this.basename(abspath),
196 | path: relpath,
197 | par: fspath.dirname(relpath),
198 | lazy: true,
199 | flags: FLAGS.LAZY,
200 | mask: 0 // FLAGS.LAZY
201 | };
202 |
203 | // do we want to do this immediately, or always through send-diff?
204 | // @diff is not updated if we change baseRef?
205 | if (this.git()) {
206 | // let gitobj = git.tree[relpath]
207 | let gitdiff = this._diff.map && this._diff.map[relpath];
208 |
209 | if (gitobj) {
210 | if (gitobj.oid) {
211 | // what if it has changed since then?
212 | node.oid = gitobj.oid;
213 | };
214 | if (gitobj.status) { node.status = gitobj.status };
215 | // node:mask = gitobj:mask if gitobj:mask
216 | };
217 |
218 | if (gitdiff) {
219 | node.status = gitdiff.status;
220 | // node:mask |= gitdiff:mask
221 | node.baseOid = gitdiff.baseOid;
222 | node.oldPath = gitdiff.oldPath;
223 | };
224 |
225 | if (!gitobj && !gitdiff) {
226 | // we can safely assume that this node is ignored in git
227 | // node:mask |= FLAGS.IGNORED
228 | node.status = '!';
229 | };
230 | };
231 |
232 |
233 | if (stat.isDirectory()) {
234 | node.type = 'dir';
235 |
236 | // go through git-diff to see if this should be marked
237 | // if there are changes inside, make this lookup recursive
238 | let changes;
239 | let res = [];
240 | for (let o = this._diff.map, change, i = 0, keys = Object.keys(o), l = keys.length, cpath; i < l; i++){
241 | cpath = keys[i];change = o[cpath];if (cpath.indexOf(relpath + path.sep) != 0) {
242 | continue;
243 | };
244 | recursive = true;
245 | res.push(change);
246 | };
247 | changes = res;
248 |
249 | for (let i = 0, items = iter$(changes), len = items.length, change; i < len; i++) {
250 | change = items[i];
251 | node.status = 'M';
252 | if (change.status == '?') {
253 | node.status = '?';
254 | break;
255 | };
256 | // or possibly A if there are added files?
257 | // node:imask |= change:mask
258 | };
259 | };
260 |
261 |
262 | if (stat.isFile()) {
263 | node.size = stat.size;
264 | // include body immediately if changed file
265 | if (node.status) { // why fetch this instantly?
266 | await this.loadNodeBody(node);
267 | };
268 | };
269 |
270 | if (stat.isSymbolicLink()) {
271 | node.symlink = true;
272 | };
273 |
274 | this.register(relpath,node);
275 |
276 | if (recursive && node.type == 'dir') {
277 | this.watchDir(node);
278 | };
279 |
280 | return node;
281 | };
282 |
283 | FileSystem.prototype.loadNodeBody = function (node,emitRead){
284 | var self = this;
285 | if(emitRead === undefined) emitRead = false;
286 | let path = node.path;
287 | if (node.type == 'file') {
288 | let prev = node.body;
289 | if (!self.isBinary(node.path) && node.size < 200000) {
290 | return new Promise(function(resolve,reject) {
291 | return self.extfs().readFile(self.absPath(path),'utf-8',function(err,data) {
292 | self.log("found file content",data,emitRead);
293 | let prev = node.body;
294 | node.body = self._contents[path] = data;
295 | node.lazy = false;
296 | node.flags = node.flags & (~FLAGS.LAZY);
297 | if (node.body != prev && emitRead) {
298 | self.emit('read',path,node.body);
299 | };
300 | return resolve(node);
301 | });
302 | });
303 | };
304 | };
305 | return Promise.resolve(node);
306 |
307 | // node:body = @contents[path] = extfs.readFileSync(absPath(path),'utf-8') or ""
308 | // node:lazy = no
309 | // node:flags = node:flags & (~FLAGS.LAZY)
310 | // # if emitRead
311 | // # console.log "emit nodeBody",path,node:body
312 | // if node:body != prev and emitRead
313 | // emit('read',path,node:body)
314 | };
315 |
316 | FileSystem.prototype.register = function (path,node){
317 | this._entries[path] = node;
318 | return this.emit('add',path,node);
319 | };
320 |
321 | FileSystem.prototype.deregister = function (path,node){
322 | var prev, v_;
323 | if (prev = this._entries[path]) {
324 | (((v_ = this._entries[path]),delete this._entries[path], v_));
325 | // also remove items inside path if dir?
326 | return this.emit('rm',path,prev);
327 | };
328 | };
329 |
330 | FileSystem.prototype.read = async function (path){
331 | path = this.relPath(path);
332 | if (this.absPath(path) == this.cwd()) { return };
333 |
334 | let node = await this.get(path,{recursive: true});
335 | this.log("reading",node);
336 | if (node.lazy) {
337 | node.lazy = false;
338 | };
339 |
340 | if (node.type == 'file') {
341 | await this.loadNodeBody(node,true);
342 | } else if (node.type == 'dir') {
343 | this.watchDir(node);
344 | };
345 |
346 | return node.body;
347 | };
348 |
349 | FileSystem.prototype.isIgnored = function (fpath){
350 | let name = path.basename(fpath);
351 | for (let i = 0, len = SKIP_LIST.length, item; i < len; i++) {
352 | item = SKIP_LIST[i];
353 | if (item instanceof RegExp) {
354 | if (item.test(name) || item.test(fpath)) { return true };
355 | };
356 | if (item == fpath || item == name) { return true };
357 | };
358 | return false;
359 | };
360 |
361 | FileSystem.prototype.isBinary = function (path){
362 | return ibn.sync(this.absPath(path));
363 | };
364 |
365 | FileSystem.prototype.onfsevent = function (dir,event,src){
366 | var git_, $1;
367 | let rel = this.relPath(src);
368 | console.log("fsevent",event,src);
369 | let node = this._entries[rel];
370 |
371 | if (node && event == 'remove') {
372 | // git might still show the file - as deleted?
373 | return this.deregister(rel);
374 | };
375 |
376 | if (node) {
377 | // possibly update the status according to git
378 | // refreshModifiedFiles
379 | (git_ = this.git()) && git_.refreshUntrackedFiles && git_.refreshUntrackedFiles(this.relPath(dir.path));
380 |
381 | // send update event?!
382 | if (!node.lazy) {
383 | this.read(rel);
384 | // emit('read',path,body)
385 | };
386 |
387 | this.log("updated content!");
388 | } else if (!node) {
389 | // make git refresh the list of untracked files in directory
390 | ($1 = this.git()) && $1.refreshUntrackedFiles && $1.refreshUntrackedFiles(this.relPath(dir.path));
391 | this.get(rel);
392 | };
393 |
394 | // what if it was deleted??
395 | this.delay('refreshChanges',100);
396 | return;
397 | };
398 |
399 | FileSystem.prototype.watchDir = async function (node){
400 | this.log('watching dir',node,this.absPath(node.path));
401 | if (this._watchers[node.path]) { return this };
402 | this._watchers[node.path] = watch(this.absPath(node.path),this.onfsevent.bind(this,node));
403 |
404 | let abs = this.absPath(node.path);
405 | for (let i = 0, items = iter$(this.extfs().readdirSync(abs)), len = items.length; i < len; i++) {
406 | await this.get(path.join(abs,items[i]));
407 | };
408 |
409 | return this;
410 | };
411 |
412 | FileSystem.prototype.refreshChanges = function (){
413 |
414 | var v_;
415 | if (!(this.git())) { return };
416 | // should move into git module instead?
417 |
418 | var changed = {};
419 | var prev = this._changes || {};
420 | var prevClone = Object.assign({},prev);
421 | var curr = this.git().diff(this.git().baseRef(),true).map;
422 |
423 | this.log("refreshChanges");
424 |
425 | for (let node, i = 0, keys = Object.keys(curr), l = keys.length, src; i < l; i++){
426 | // also include if any oldOid changed?
427 | src = keys[i];node = curr[src];if (!prev[src] || prev[src].status != node.status) {
428 | changed[src] = node;
429 | };
430 | (((v_ = prevClone[src]),delete prevClone[src], v_));
431 | };
432 |
433 | // items that went from having changes to not having any?
434 | for (let src in prevClone){
435 | let node;
436 | node = prevClone[src];changed[src] = {status: '',oid: node.oldOid};
437 | };
438 |
439 | this._changes = curr;
440 |
441 | if (len$(Object.keys(changed)) > 0) {
442 | this.log("diff",changed);
443 | for (let node, i = 0, keys = Object.keys(changed), l = keys.length, src; i < l; i++){
444 | // read the new oid as well?
445 | src = keys[i];node = changed[src];this.git().read(node.oldOid);
446 | };
447 |
448 | this.emit('diff',changed);
449 | };
450 | return changed;
451 | };
452 |
453 | FileSystem.prototype.start = function (){
454 | this.watchDir({path: '.'});
455 | this.refreshChanges();
456 | return this.emit('mounted');
457 | };
458 |
459 | FileSystem.prototype.write = function (src,body){
460 | let abs = this.absPath(src);
461 | let node = this.get(src);
462 |
463 | this.log("fs.write",src,abs);
464 |
465 | if (node && node.body == body) {
466 | return this;
467 | } else {
468 | if (node) { node.ts = Date.now() };
469 | // @contents[relPath(src)] = body
470 | node.body = body;
471 | this.extfs().writeFileSync(abs,body,'utf-8');
472 | };
473 | return this;
474 | };
475 |
476 |
477 | // path should be relative to root / cwd
478 | FileSystem.prototype.mkdir = function (entry){
479 | throw "not implemented";
480 | };
481 |
482 | FileSystem.prototype.mkfile = function (entry){
483 | throw "not implemented";
484 | };
485 |
486 | FileSystem.prototype.mv = function (src,dest){
487 | throw "not implemented";
488 | };
489 |
490 | FileSystem.prototype.dispose = function (){
491 | return this;
492 | };
493 |
--------------------------------------------------------------------------------
/src/git.imba:
--------------------------------------------------------------------------------
1 | import {Component} from './component'
2 |
3 | var simpleGit = require 'simple-git/promise'
4 | var parseGitConfig = require 'parse-git-config'
5 | var hostedGitInfo = require 'hosted-git-info'
6 | var gitRepoInfo = require 'git-repo-info'
7 | var cp = require 'child_process'
8 | var ibn = require 'isbinaryfile'
9 | var util = require './util'
10 |
11 | var LINESEP = '\n'
12 | var FLAGS =
13 | UNSAVED: 1
14 | UNTRACKED: 2
15 | IGNORED: 4
16 | LAZY: 8
17 | BINARY: 16
18 | SYMLINK: 32
19 | MODIFIED: 64
20 | ADDED: 128
21 | RENAMED: 256
22 | COPIED: 512
23 | DELETED: 1024
24 |
25 | "M": 64
26 | "A": 128
27 | "D": 1024
28 | "R": 256
29 | "?": 2
30 |
31 |
32 | var validate =
33 | ref: do |val| (/^[\:\/\-\.\w]+$/).test(val)
34 | sha: do |val| (/^[\:\/\-\.\w]+$/).test(val)
35 |
36 | export def exec command, cwd
37 | cp.execSync(command, cwd: cwd, env: process:env)
38 |
39 | export def execSync command, cwd
40 | cp.execSync(command, cwd: cwd, env: process:env)
41 |
42 | export def shaExists cwd, treeish
43 | try
44 | execSync("git cat-file -t {treeish}",cwd)
45 | return yes
46 | catch e
47 | return no
48 |
49 | export def fetchSha cwd, sha, ref
50 | return yes if shaExists(cwd,sha)
51 | console.log("fetchSha",cwd,sha,ref)
52 |
53 | let cmd = ref ? "git fetch origin {ref}" : "git fetch"
54 | let res = execSync(cmd,cwd)
55 |
56 | return yes
57 |
58 | export def isValidTreeish value
59 | return value.match(/^[\:\/\-\.\w]+$/)
60 |
61 | ###
62 | --raw --numstat
63 | :100644 100644 06f59bf... 98ad458... M README.md
64 | :100644 100644 5474c93... 801afcc... M server.js
65 | :000000 100644 0000000... 3760da4... A src/api.imba
66 | :100644 100644 b116b25... cfee64d... M src/main.imba
67 | :000000 100644 0000000... 698007b... A www/playground.js
68 | 7 1 README.md
69 | 1 1 server.js
70 | 4 0 src/api.imba
71 | 9 1 src/main.imba
72 | 1 0 www/playground.js
73 |
74 | Should be able to call this asynchronously from socket
75 | WARN this doesnt show actual diff between the two, but rather
76 | the changes in head relative to the branch of base
77 | ###
78 | export def getGitDiff cwd, base, head, includePatch = no
79 | console.log "getGitDiff",cwd,base,head
80 |
81 |
82 | let baseSha = execSync("git merge-base {head} {base}",cwd).toString.trim
83 | let result = {
84 | head: head
85 | base: baseSha
86 | diff: []
87 | }
88 |
89 | let raw = execSync("git diff --raw --numstat {baseSha}..{head}",cwd).toString
90 | let lines = raw.split('\n')
91 | let len = Math.floor(lines:length * 0.5)
92 | let numstat = lines.splice(len,len + 2).map do |ln|
93 | ln.split(/\s+/).map do |item| (/^\d+$/).test(item) ? parseInt(item) : item
94 |
95 | for entry,i in lines
96 | let mode = entry.split(/[\s\t]/)[4]
97 | let file = entry.slice(entry.indexOf('\t') + 1)
98 | let node = {
99 | name: file,
100 | mode: mode,
101 | ins: numstat[i][0]
102 | rem: numstat[i][1]
103 | }
104 |
105 | if includePatch
106 | if node:ins == '-'
107 | continue
108 |
109 | if mode == 'A' or mode == 'M'
110 | # Should also check size and if binary
111 |
112 | let body = execSync("git cat-file -p {head}:{file}",cwd).toString
113 | node:body = body
114 | if mode == 'M'
115 | let patch = execSync("git diff {baseSha}..{head} -- {file}",cwd).toString
116 | node:patch = patch
117 | elif mode == 'D'
118 | # possibly include the previous value?
119 | yes
120 |
121 | result:diff.push node
122 | return result
123 |
124 |
125 | export def getGitBlob cwd, sha, refToFetch
126 | console.log "getGitBlob",cwd,sha,refToFetch
127 | unless isValidTreeish(sha)
128 | console.log "blob did not exist??",cwd,sha
129 | return null
130 | # make sure we've fetched latest from remote
131 | fetchSha(cwd,sha,refToFetch)
132 |
133 | try
134 | let buffer = execSync('git cat-file -p ' + sha, cwd)
135 | # not certain that we have the oid?
136 | let obj = {
137 | oid: sha
138 | body: null
139 | size: buffer:length
140 | type: 'blob'
141 | }
142 | if !ibn.sync(buffer,obj:size) and obj:size < 200000
143 | obj:body = buffer.toString
144 | return obj
145 | catch error
146 | console.log "error from getGitBlob"
147 | return null
148 |
149 | export def getGitTree cwd, sha, refToFetch
150 | console.log "getGitTree",cwd,sha,refToFetch
151 | return null unless isValidTreeish(sha)
152 |
153 | fetchSha(cwd,sha,refToFetch)
154 |
155 | try
156 | let buffer = execSync('git ls-tree -z -l ' + sha, cwd)
157 | let tree = []
158 | for line in buffer.toString.split('\0')
159 | let [mode,type,sha,osize] = line.split(/(?:\ |\t)+/g)
160 | let name = line.substr(line.indexOf('\t') + 1)
161 | continue unless name
162 | tree.push({sha: sha,size: osize, mode: mode, path: name, type: type})
163 | return {
164 | oid: sha
165 | type: 'tree'
166 | data: { nodes: tree }
167 | }
168 | catch error
169 | return null
170 |
171 | export def getGitInfo cwd
172 | var data = {}
173 | if var repo = gitRepoInfo._findRepo(cwd)
174 | var info = gitRepoInfo(cwd)
175 | data:branch = info:branch
176 | data:sha = info:sha
177 | data:tag = info:tag
178 | data:commit = info:commitMessage
179 | data:root = info:root
180 | else
181 | return data
182 |
183 | if var conf = parseGitConfig.sync(cwd: cwd, path: cwd + '/.git/config')
184 | let branchInfo = execSync("git branch -vv --no-abbrev --no-color", cwd).toString
185 | let [m,name,head,remote] = branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/)
186 | data:remote = remote
187 |
188 | if let origin = conf['remote "origin"']
189 | data:origin = origin:url
190 |
191 | return data
192 |
193 | export class Git < Component
194 |
195 | prop origin
196 | prop info
197 | prop status
198 | prop baseRef
199 |
200 | def cwd
201 | @root
202 |
203 | def repoRoot
204 | isRepository and @summary:root
205 |
206 | def initialize owner, root, options = {}
207 | @owner = owner
208 | @root = root
209 | @summary = {}
210 | @diffs = {}
211 | @untracked = {}
212 | @changed = {}
213 | @objects = {}
214 | @baseRef = options:baseRef or 'HEAD'
215 |
216 | @trees = {}
217 | @trees:base = {}
218 | @trees:modified = Object.create(@trees:base)
219 | @trees:untracked = Object.create(@trees:modified)
220 |
221 | if var repo = gitRepoInfo._findRepo(cwd)
222 | var info = gitRepoInfo(cwd)
223 | @summary:branch = info:branch
224 | @summary:sha = info:sha
225 | @summary:tag = info:tag
226 | @summary:commit = info:commitMessage
227 | @summary:root = info:root
228 | log "GIT",info
229 |
230 |
231 | if var conf = parseGitConfig.sync(cwd: cwd, path: cwd + '/.git/config')
232 | log "GITCONF",cwd,conf
233 | let branchInfo = exec("branch -vv --no-abbrev --no-color").toString
234 | let [m,name,head,remote] = branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/)
235 | @summary:remote = remote
236 |
237 | if let origin = conf['remote "origin"']
238 | self.origin = @summary:origin = origin:url
239 |
240 | self
241 |
242 | def exec cmd
243 | # todo: add check
244 | if cmd isa Array
245 | cp.execFileSync('git',cmd, cwd: cwd, env: process:env)
246 | else
247 | cp.execSync('git ' + cmd, cwd: cwd, env: process:env)
248 |
249 | def execAsync cmd
250 | Promise.new do |resolve,reject|
251 | var o = {cwd: cwd, env: process:env, maxBuffer: 1024 * 500}
252 | var handler = do |err,stdout,stderr|
253 | if err
254 | log "error from exec"
255 | log err and err:message
256 | log stderr.toString
257 | return reject(err)
258 |
259 | let str = stdout.toString
260 | if str[str:length - 1] == '\n'
261 | str = str.slice(0,-1)
262 | resolve(str)
263 |
264 | if cmd isa Array
265 | log "cmd is array"
266 | o:encoding = 'utf-8'
267 | cp.execFile('git',cmd,o,handler)
268 | else
269 | cp.exec('git ' + cmd, o,handler)
270 |
271 | def isRepository
272 | !!@summary:branch
273 |
274 | def gitRepoRef
275 | return null unless isRepository
276 | let m = origin.match(/github\.com[\/\:](.*?)(\.git)?$/)
277 | m ? m[1] : null
278 |
279 | def parseGitTree text
280 | var tree = {}
281 | # SP SP