├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | GitSpeak 10 | 11 | 12 | 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 SP TAB 282 | for line in text.split('\0') 283 | let [mode,type,oid,osize] = line.split(/(?:\ |\t)+/g) 284 | let name = line.substr(line.indexOf('\t') + 1) 285 | continue unless name 286 | tree[name] = {oid: oid,size: osize, mode: mode} 287 | tree[name.toLowerCase] = tree[name] 288 | return tree 289 | 290 | def load 291 | Promise.resolve(self) 292 | # status = await @git.status 293 | 294 | def git 295 | @git ||= simpleGit(cwd) 296 | 297 | def summary 298 | @summary 299 | 300 | def config 301 | @config ||= {} 302 | 303 | def tree 304 | unless isRepository 305 | return @tree = {} 306 | 307 | if @tree 308 | return @tree 309 | 310 | var raw = exec('ls-tree -rltz HEAD').toString 311 | @tree = parseGitTree(raw) 312 | refreshUntrackedFiles() 313 | return @tree 314 | 315 | def cat oid 316 | # todo: add check 317 | var res = exec('cat-file -p ' + oid) 318 | return res 319 | 320 | def read oid 321 | let existing = @objects[oid] 322 | if !existing and oid 323 | if let buf = cat(oid) 324 | let obj = @objects[oid] = { 325 | oid: oid 326 | body: null 327 | size: buf:length 328 | } 329 | if !ibn.sync(buf,buf:length) and obj:size < 200000 330 | obj:body = buf.toString 331 | 332 | emit('oread',oid,obj) 333 | return 334 | 335 | 336 | # returns a formatted list of changes in the repo, relative 337 | # to the supplied baseRef (the default is HEAD, which means 338 | # current uncommitted changes) 339 | def diff treeish = @baseRef, hard = no 340 | return {map: {}} unless isRepository 341 | var key = treeish # + includeBlobs 342 | return @diffs[key] if @diffs[key] and !hard 343 | log "git diff {treeish} {cwd}" 344 | # add these to the actual tree maybe? 345 | var prevChanges = @changed 346 | 347 | var changes = [] 348 | var map = {} 349 | # to ensure git recalculates before diff-index 350 | exec('status') 351 | 352 | # todo: add check 353 | var raw = exec('diff-index -z ' + treeish).toString 354 | var nullOid = "0000000000000000000000000000000000000000" 355 | for line in raw.split('\0:') 356 | let parts = line.split('\0') 357 | var [mode0,mode1,oid0,oid1,status] = parts[0].split(' ') 358 | var change = { 359 | oldPath: parts[1] 360 | newPath: parts[2] or parts[1] 361 | oldOid: oid0 362 | newOid: oid1 363 | status: status 364 | } 365 | let path = change:newPath or change:oldPath 366 | delete change:newPath if change:newPath == change:oldPath 367 | delete change:newOid if change:newOid == nullOid 368 | delete change:oldOid if change:oldOid == nullOid 369 | 370 | # if includeBlobs and change:oldOid 371 | # let buf = cat(change:oldOid) 372 | # if !ibn.sync(buf,buf:length) 373 | # change:oldBody = buf.toString 374 | 375 | changes.push(change) 376 | 377 | if treeish == 'HEAD' 378 | # if we are asking for the uncommitted changes we also want the added files 379 | # that are not yet staged, and that are not ignored via .gitignore) 380 | var toAdd = exec('ls-files --others --exclude-standard -z').toString.split('\0') # windows? 381 | 382 | for name in toAdd when name 383 | changes.push({newPath: name,status: '?'}) 384 | 385 | 386 | for change in changes 387 | if change:newPath 388 | map[change:newPath] = change 389 | if change:oldPath 390 | map[change:oldPath] = change 391 | 392 | return @diffs[key] = { 393 | baseRef: treeish 394 | changes: changes 395 | map: map 396 | } 397 | 398 | # def isIgnored src 399 | def refreshUntrackedFiles dir = '' 400 | # todo: add check 401 | var paths = exec('ls-files --others --exclude-standard -z ' + dir).toString.split('\0') 402 | var tree = tree 403 | for path in paths when path 404 | unless tree[path] 405 | tree[path] = {status: '?'} 406 | 407 | return self 408 | 409 | def oidForPath path 410 | tree[path] 411 | 412 | def commit options 413 | var msg = options:message 414 | # console.log "commit",msg,options 415 | # TODO escape git message 416 | var cmd = "git commit --allow-empty -m \"{msg}\"" 417 | # var res = await git.commit(['--allow-empty','-m',msg]) 418 | # console.log "did commit?",res,cmd 419 | if options:push 420 | cmd += " && git push" 421 | 422 | if var term = @owner.@terminals[0] 423 | # make sure we are in the right directory 424 | term.write("cd {cwd}\n") 425 | term.write(cmd + '\n') 426 | self 427 | 428 | export class GitRepo < Git 429 | 430 | def initialize owner, root, options = {} 431 | @owner = owner 432 | @root = root 433 | @summary = {} 434 | @intervals = {} 435 | @refs = {} 436 | 437 | if var repo = gitRepoInfo._findRepo(cwd) 438 | var info = gitRepoInfo(cwd) 439 | @summary:branch = info:branch 440 | @summary:sha = info:sha 441 | @summary:tag = info:tag 442 | @summary:commit = info:commitMessage 443 | @summary:root = info:root 444 | log "GIT",info 445 | 446 | 447 | if var conf = parseGitConfig.sync(cwd: cwd, path: cwd + '/.git/config') 448 | log "GITCONF",cwd,conf 449 | let branchInfo = exec("branch -vv --no-abbrev --no-color").toString 450 | let [m,name,head,remote] = branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/) 451 | @summary:remote = remote 452 | 453 | if let origin = conf['remote "origin"'] 454 | self.origin = @summary:origin = origin:url 455 | self 456 | 457 | def start 458 | emit('start',@summary) 459 | # dont fetch all the time 460 | # @intervals:fetch = setInterval(self:fetch.bind(self),10000) 461 | updateRefs 462 | sendRefs 463 | self 464 | 465 | def updateRefs 466 | let refs = {} 467 | let rawRefs = exec('show-ref').toString 468 | for line in rawRefs.split(LINESEP) 469 | var [sha,ref] = line.split(" ") 470 | refs[ref] = sha 471 | @refs = refs 472 | self 473 | 474 | def sendRefs 475 | emit('refs',@refs) 476 | self 477 | 478 | def checkTest 479 | log "called checkTest" 480 | return {a: 1, b: 2} 481 | 482 | def grep text 483 | # Trim the text so trailing newlines will not prevent a match. 484 | text = text.trim() 485 | 486 | # find the last commit that is shared between local branch and upstream 487 | # FIXME now always looking for the location relative to remote HEAD 488 | 489 | # find last commit in history that we know is synced to the remote 490 | var rootSha = null # await execAsync('merge-base HEAD @{u}') 491 | let lastOriginCommit = await execAsync('log --remotes=origin -1 --pretty=oneline') 492 | if lastOriginCommit 493 | rootSha = lastOriginCommit.split(" ")[0] 494 | 495 | # let rootSha = await execAsync('merge-base HEAD @{u}') 496 | # refs/remotes/origin/HEAD 497 | 498 | 499 | let lines = text.split('\n') 500 | let cmd = [ 501 | "grep" 502 | "--files-with-matches" 503 | "--all-match" 504 | "-n" 505 | "-F" 506 | '-e' 507 | text 508 | ] 509 | 510 | let matches = [] 511 | let res 512 | log "grep",JSON.stringify(text),lines:length,cmd,text.indexOf('\t') 513 | try 514 | res = await execAsync(cmd) 515 | catch e 516 | return [] 517 | 518 | if res 519 | let files = res.split("\n") 520 | if files.len > 20 521 | # too many hits 522 | return matches 523 | 524 | for file in files 525 | log "git cat-file -p {rootSha}:{file}" 526 | var sha = rootSha 527 | # if the file does not exist 528 | try 529 | # if the file does not exist in HEAD - or the position is different 530 | let body 531 | try 532 | body = await execAsync("cat-file -p {sha}:{file}") 533 | 534 | if !body or body.indexOf(text) == -1 535 | sha = await execAsync("rev-parse HEAD") 536 | body = await execAsync("cat-file -p {sha}:{file}") 537 | log "could not find from remote head, use local head instead {sha}" 538 | 539 | let start = 0 540 | let idx 541 | 542 | while (idx = body.indexOf(text,start)) >= 0 543 | let match = { 544 | commit: rootSha, 545 | file: file, 546 | loc: idx, 547 | line: util.countLines(body.slice(0,idx)) 548 | } 549 | 550 | # include full lines? 551 | let url = "https://github.com/{gitRepoRef}/blob/{rootSha}/{file}#L{match:line}" 552 | if lines.len > 1 553 | url += "-L{match:line + lines.len - 1}" 554 | let lang = file.split(".").pop 555 | match:permalink = url 556 | match:code = text 557 | match:language = lang 558 | match:markdown = "```{lang}\n{text}\n```\n[↳ {file}]({url})" 559 | start = idx + text:length 560 | matches.push(match) 561 | catch e 562 | log "error grepping {file}" 563 | 564 | return matches 565 | 566 | def fetch 567 | return if @fetching 568 | emit('fetching',@fetching = yes) 569 | try 570 | var res = await execAsync('fetch -a -f origin "refs/pull/*:refs/pull/*"') 571 | # console.log "result is",res 572 | # if there is a result - some branches may have updated -- send new refs 573 | if res and String(res):length > 2 574 | updateRefs 575 | sendRefs 576 | 577 | catch e 578 | self 579 | 580 | emit('fetched',@fetching = no) 581 | return 582 | 583 | def fetch_ref ref, expectedSha 584 | # console.log "fetch_ref",ref,expectedSha 585 | unless validate.ref(ref) 586 | return null 587 | 588 | try 589 | let curr = @refs[ref] 590 | 591 | if expectedSha and curr == expectedSha 592 | return curr 593 | var cmd = "fetch -a -f origin \"{ref}:{ref}\"" 594 | # console.log "call cmd",cmd 595 | var res = await execAsync(cmd) 596 | # console.log "result from cmd",res 597 | updateRefs 598 | return @refs[ref] 599 | return 10 600 | 601 | 602 | def dispose 603 | clearInterval(@intervals:fetch) 604 | self -------------------------------------------------------------------------------- /lib/terminal/terminal.js: -------------------------------------------------------------------------------- 1 | function iter$(a){ return a ? (a.toArray ? a.toArray() : a) : []; }; 2 | var Terminal_; 3 | var hterm_vt$ = require("../../vendor/hterm_vt"), lib = hterm_vt$.lib, hterm = hterm_vt$.hterm; 4 | var wc = require("../../vendor/lib_wc").wc; 5 | 6 | var buffer$ = require("./buffer"), ScreenBuffer = buffer$.ScreenBuffer, TextStyling = buffer$.TextStyling, getWhitespace = buffer$.getWhitespace, getFill = buffer$.getFill; 7 | 8 | hterm.Terminal || (hterm.Terminal = {}); 9 | (Terminal_ = hterm.Terminal).cursorShape || (Terminal_.cursorShape = { 10 | BLOCK: 'BLOCK', 11 | BEAM: 'BEAM', 12 | UNDERLINE: 'UNDERLINE' 13 | }); 14 | 15 | function CursorState(screen){ 16 | this._screen = screen; 17 | }; 18 | 19 | CursorState.prototype.save = function (vt){ 20 | this._cursor = this._screen.saveCursor(); 21 | this._textAttributes = this._screen.textAttributes().clone(); 22 | this._GL = vt.GL; 23 | this._GR = vt.GR; 24 | this._G0 = vt.G0; 25 | this._G1 = vt.G1; 26 | this._G2 = vt.G2; 27 | this._G3 = vt.G3; 28 | return this; 29 | }; 30 | 31 | CursorState.prototype.restore = function (vt){ 32 | this._screen.restoreCursor(this._cursor); 33 | this._screen.setTextAttributes(this._textAttributes); 34 | vt.GL = this._GL; 35 | vt.GR = this._GR; 36 | vt.G0 = this._G0; 37 | vt.G1 = this._G1; 38 | vt.G2 = this._G2; 39 | vt.G3 = this._G3; 40 | return this; 41 | }; 42 | 43 | 44 | function Screen(terminal){ 45 | this._terminal = terminal; 46 | this._width = terminal.width(); 47 | this._height = terminal.height(); 48 | this._textAttributes = new (hterm.TextAttributes)(null); 49 | this._cursorState = new CursorState(this); 50 | 51 | this._buffer = new ScreenBuffer(this._width,this._height); 52 | this._buffer.clear(this.getStyling()); 53 | }; 54 | 55 | exports.Screen = Screen; // export class 56 | Screen.prototype.textAttributes = function(v){ return this._textAttributes; } 57 | Screen.prototype.setTextAttributes = function(v){ this._textAttributes = v; return this; }; 58 | Screen.prototype.width = function(v){ return this._width; } 59 | Screen.prototype.setWidth = function(v){ this._width = v; return this; }; 60 | Screen.prototype.height = function(v){ return this._height; } 61 | Screen.prototype.setHeight = function(v){ this._height = v; return this; }; 62 | Screen.prototype.buffer = function(v){ return this._buffer; } 63 | Screen.prototype.setBuffer = function(v){ this._buffer = v; return this; }; 64 | 65 | Screen.prototype.getStyling = function (){ 66 | return TextStyling.fromAttributes(this._textAttributes); 67 | }; 68 | 69 | Screen.prototype.row = function (){ 70 | return this._buffer.row(); 71 | }; 72 | 73 | Screen.prototype.column = function (){ 74 | return this._buffer.column(); 75 | }; 76 | 77 | Screen.prototype.setRow = function (row){ 78 | return (this._buffer.setRow(row),row); 79 | }; 80 | 81 | Screen.prototype.setColumn = function (col){ 82 | return (this._buffer.setColumn(col),col); 83 | }; 84 | 85 | Screen.prototype.lines = function (){ 86 | return this._buffer.lines(); 87 | }; 88 | 89 | Screen.prototype.createLine = function (){ 90 | return this._buffer.createLine(this.getStyling()); 91 | }; 92 | 93 | Screen.prototype.indexForRow = function (row){ 94 | return this._buffer.indexForRow(row); 95 | }; 96 | 97 | Screen.prototype.lineAtRow = function (row){ 98 | return this._buffer.lineAtRow(row); 99 | }; 100 | 101 | Screen.prototype.currentLine = function (){ 102 | return this.lineAtRow(this.row()); 103 | }; 104 | 105 | Screen.prototype.pushLine = function (){ 106 | return this.lines().push(this.createLine()); 107 | }; 108 | 109 | Screen.prototype.popLine = function (){ 110 | return this.lines().pop(); 111 | }; 112 | 113 | Screen.prototype.setScrollRegion = function (top,bottom){ 114 | this._scrollTop = top; 115 | this._scrollBottom = bottom; 116 | return this; 117 | }; 118 | 119 | Screen.prototype.scrollTop = function (){ 120 | return this._scrollTop || 0; 121 | }; 122 | 123 | Screen.prototype.scrollBottom = function (){ 124 | return this._scrollBottom || (this._height - 1); 125 | }; 126 | 127 | Screen.prototype.atTop = function (){ 128 | return this.row() == this.scrollTop(); 129 | }; 130 | 131 | Screen.prototype.atBottom = function (){ 132 | return this.row() == this.scrollBottom(); 133 | }; 134 | 135 | Screen.prototype.scrollLines = function (insertRow,deleteRow,count){ 136 | var insertIdx = this.indexForRow(insertRow); 137 | var deleteIdx = this.indexForRow(deleteRow); 138 | 139 | for (let len = count, i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) { 140 | this.lines().splice(insertIdx,0,this.createLine()); 141 | }; 142 | 143 | if (insertIdx < deleteIdx) { 144 | // Take into account that we have shifted lines 145 | deleteIdx += count; 146 | }; 147 | 148 | return this.lines().splice(deleteIdx,count); 149 | }; 150 | 151 | Screen.prototype.scrollUp = function (count){ 152 | return this.scrollLines(this.scrollBottom() + 1,this.scrollTop(),count); 153 | }; 154 | 155 | Screen.prototype.scrollDown = function (count){ 156 | return this.scrollLines(this.scrollTop(),this.scrollBottom() - count + 1,count); 157 | }; 158 | 159 | Screen.prototype.insertLines = function (count){ 160 | return this.scrollLines(this.row(),this.scrollBottom() - count + 1,count); 161 | }; 162 | 163 | Screen.prototype.deleteLines = function (count){ 164 | return this.scrollLines(this.scrollBottom() + 1,this.row(),count);; 165 | }; 166 | 167 | Screen.prototype.visibleLines = function (){ 168 | var startIdx = this.indexForRow(0); 169 | return this.lines().slice(startIdx,startIdx + this.height()); 170 | }; 171 | 172 | Screen.prototype.saveCursor = function (){ 173 | return new (hterm.RowCol)(this.row(),this.column()); 174 | }; 175 | 176 | Screen.prototype.restoreCursor = function (cursor){ 177 | var v_; 178 | this.setRow(cursor.row); 179 | return (this.setColumn(v_ = cursor.column),v_); 180 | }; 181 | 182 | Screen.prototype.saveCursorAndState = function (vt){ 183 | return this._cursorState.save(vt); 184 | }; 185 | 186 | Screen.prototype.restoreCursorAndState = function (vt){ 187 | return this._cursorState.restore(vt); 188 | }; 189 | 190 | Screen.prototype.cursorUp = function (count){ 191 | var v_; 192 | if(count === undefined) count = 1; 193 | return (this.setRow(v_ = this.row() - count),v_); 194 | }; 195 | 196 | Screen.prototype.cursorDown = function (count){ 197 | var v_; 198 | if(count === undefined) count = 1; 199 | return (this.setRow(v_ = this.row() + 1),v_); 200 | }; 201 | 202 | Screen.prototype.writeText = function (text,insert){ 203 | if(insert === undefined) insert = false; 204 | var styling = this.getStyling(); 205 | 206 | while (text.length){ 207 | var availableSpace = this.width() - this.column(); 208 | if (availableSpace == 0) { 209 | this.formFeed(); 210 | }; 211 | 212 | var fittedText = wc.substr(text,0,availableSpace); 213 | var textWidth = wc.strWidth(fittedText); 214 | if (insert) { 215 | fittedText += this.currentLine().substr(this.column(),this.width() - textWidth); 216 | }; 217 | this.currentLine().replace(this.column(),fittedText,styling); 218 | this.setColumn(this.column() + textWidth); 219 | text = wc.substr(text,textWidth); 220 | }; 221 | return this; 222 | }; 223 | 224 | Screen.prototype.insertText = function (text){ 225 | return this.writeText(text,true); 226 | }; 227 | 228 | Screen.prototype.clear = function (){ 229 | this._buffer.clear(this.getStyling()); 230 | return this; 231 | }; 232 | 233 | Screen.prototype.fill = function (char$){ 234 | var styling = this.getStyling(); 235 | var text = getFill(this.width(),char$); 236 | for (let len = this.height(), i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) { 237 | var line = this.lineAtRow(i); 238 | line.replace(0,text,styling); 239 | }; 240 | return this; 241 | }; 242 | 243 | Screen.prototype.deleteChars = function (count){ 244 | var line = this.currentLine(); 245 | var styling = line.styles()[this.column() + count]; 246 | return line.shiftLeft(this.column(),count,styling); 247 | }; 248 | 249 | Screen.prototype.eraseToLeft = function (count){ 250 | if(count === undefined) count = this.column(); 251 | return this.currentLine().erase(0,count,this.getStyling()); 252 | }; 253 | 254 | Screen.prototype.eraseToRight = function (count){ 255 | if(count === undefined) count = this.width() - this.column(); 256 | return this.currentLine().erase(this.column(),count,this.getStyling()); 257 | }; 258 | 259 | Screen.prototype.eraseLine = function (){ 260 | return this.currentLine().clear(this.getStyling()); 261 | }; 262 | 263 | Screen.prototype.eraseAbove = function (){ 264 | var styling = this.getStyling(); 265 | this.eraseToLeft(this.column() + 1); 266 | let res = []; 267 | for (let len = this.row(), i = 0, rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) { 268 | var line = this.lineAtRow(i); 269 | res.push(line.erase(0,this.width(),styling)); 270 | }; 271 | return res; 272 | }; 273 | 274 | Screen.prototype.eraseBelow = function (){ 275 | var styling = this.getStyling(); 276 | this.eraseToRight(); 277 | let res = []; 278 | for (let len = this._height, i = (this.row() + 1), rd = len - i; (rd > 0) ? (i < len) : (i > len); (rd > 0) ? (i++) : (i--)) { 279 | var line = this.lineAtRow(i); 280 | res.push(line.erase(0,this.width(),styling)); 281 | }; 282 | return res; 283 | }; 284 | 285 | Screen.prototype.formFeed = function (){ 286 | this.setColumn(0); 287 | return this.newLine(); 288 | }; 289 | 290 | Screen.prototype.newLine = function (){ 291 | var v_; 292 | if (this._scrollTop != null || this._scrollBottom != null) { 293 | if (this.atBottom()) { 294 | // We're at the bottom in the scroll view 295 | this.scrollUp(1); 296 | return; 297 | }; 298 | }; 299 | 300 | if (this.atBottom()) { 301 | return this.pushLine(); 302 | } else { 303 | return (this.setRow(v_ = this.row() + 1),v_); 304 | }; 305 | }; 306 | 307 | Screen.prototype.lineFeed = function (){ 308 | return this.newLine(); 309 | }; 310 | 311 | Screen.prototype.reverseLineFeed = function (){ 312 | var v_; 313 | if (this.atTop()) { 314 | return this.scrollDown(1); 315 | } else { 316 | return (this.setRow(v_ = this.row() - 1),v_); 317 | }; 318 | }; 319 | 320 | Screen.prototype.reset = function (){ 321 | // Remove scroll region 322 | this.setScrollRegion(null,null); 323 | 324 | // Clear screen 325 | this.clear(); 326 | 327 | // Reset cursor 328 | this.setRow(0); 329 | this.setColumn(0); 330 | return this; 331 | }; 332 | 333 | 334 | var toScreen = function(name) { 335 | return function() { 336 | var screen = this._screen; 337 | if (!(screen && screen[name])) { 338 | console.log(("error in terminal (" + name + ")")); 339 | return; 340 | }; 341 | 342 | return screen[name].apply(screen,arguments); 343 | }; 344 | }; 345 | 346 | var toScreenBuffer = function(name) { 347 | return function() { 348 | var screen = this._screen; 349 | var buffer = screen._buffer; 350 | return buffer[name].apply(buffer,arguments); 351 | }; 352 | }; 353 | 354 | function Terminal(options){ 355 | this._width = options.width; 356 | this._height = options.height; 357 | 358 | this._primaryScreen = new Screen(this); 359 | this._alternateScreen = new Screen(this); 360 | this._screen = this._primaryScreen; 361 | this._screens = [this._primaryScreen,this._alternateScreen]; 362 | 363 | this._insertMode = false; 364 | this._wraparound = true; 365 | this._cursorBlink = false; 366 | this._cursorShape = hterm.Terminal.cursorShape.BLOCK; 367 | 368 | this._tabWidth = 8; 369 | this.setDefaultTabStops(); 370 | 371 | this.keyboard = {}; 372 | this.io = { 373 | sendString: function() { } // nothing 374 | }; 375 | this.screenSize = { 376 | width: this._width, 377 | height: this._height 378 | }; 379 | }; 380 | 381 | exports.Terminal = Terminal; // export class 382 | Terminal.prototype.width = function(v){ return this._width; } 383 | Terminal.prototype.setWidth = function(v){ this._width = v; return this; }; 384 | Terminal.prototype.height = function(v){ return this._height; } 385 | Terminal.prototype.setHeight = function(v){ this._height = v; return this; }; 386 | Terminal.prototype.screen = function(v){ return this._screen; } 387 | Terminal.prototype.setScreen = function(v){ this._screen = v; return this; }; 388 | Terminal.prototype.screens = function(v){ return this._screens; } 389 | Terminal.prototype.setScreens = function(v){ this._screens = v; return this; }; 390 | Terminal.prototype.insertMode = function(v){ return this._insertMode; } 391 | Terminal.prototype.setInsertMode = function(v){ this._insertMode = v; return this; }; 392 | Terminal.prototype.wraparound = function(v){ return this._wraparound; } 393 | Terminal.prototype.setWraparound = function(v){ this._wraparound = v; return this; }; 394 | Terminal.prototype.cursorBlink = function(v){ return this._cursorBlink; } 395 | Terminal.prototype.setCursorBlink = function(v){ this._cursorBlink = v; return this; }; 396 | Terminal.prototype.cursorShape = function(v){ return this._cursorShape; } 397 | Terminal.prototype.setCursorShape = function(v){ this._cursorShape = v; return this; }; 398 | Terminal.prototype.windowTitle = function(v){ return this._windowTitle; } 399 | Terminal.prototype.setWindowTitle = function(v){ this._windowTitle = v; return this; }; 400 | 401 | Terminal.prototype.primaryScreen = function(v){ return this._primaryScreen; } 402 | Terminal.prototype.setPrimaryScreen = function(v){ this._primaryScreen = v; return this; }; 403 | Terminal.prototype.alternateScreen = function(v){ return this._alternateScreen; } 404 | Terminal.prototype.setAlternateScreen = function(v){ this._alternateScreen = v; return this; }; 405 | 406 | Terminal.prototype.screenIndex = function (){ 407 | return this._screens.indexOf(this._screen); 408 | }; 409 | 410 | Terminal.prototype.createVT = function (){ 411 | return new (hterm.VT)(this); 412 | }; 413 | 414 | Terminal.prototype.vt = function (){ 415 | return this._vt || (this._vt = this.createVT()); 416 | }; 417 | 418 | Terminal.prototype.cursorState = function (){ 419 | return [this.screen().row(),this.screen().column()]; 420 | }; 421 | 422 | Terminal.prototype.saveCursorAndState = function (both){ 423 | if (both) { 424 | this._primaryScreen.saveCursorAndState(this.vt()); 425 | return this._alternateScreen.saveCursorAndState(this.vt()); 426 | } else { 427 | return this.screen().saveCursorAndState(this.vt()); 428 | }; 429 | }; 430 | 431 | Terminal.prototype.restoreCursorAndState = function (both){ 432 | if (both) { 433 | this._primaryScreen.restoreCursorAndState(this.vt()); 434 | return this._alternateScreen.restoreCursorAndState(this.vt()); 435 | } else { 436 | return this.screen().restoreCursorAndState(this.vt()); 437 | }; 438 | }; 439 | 440 | Terminal.prototype.setAlternateMode = function (state){ 441 | var newScreen = state ? this._alternateScreen : this._primaryScreen; 442 | return this._screen = newScreen; 443 | }; 444 | 445 | Terminal.prototype.getTextAttributes = function (){ 446 | return this._screen.textAttributes(); 447 | }; 448 | 449 | Terminal.prototype.setTextAttributes = function (attrs){ 450 | return (this._screen.setTextAttributes(attrs),attrs); 451 | }; 452 | 453 | Terminal.prototype.getForegroundColor = function (){ 454 | return "white"; 455 | }; 456 | 457 | Terminal.prototype.getBackgroundColor = function (){ 458 | return "black"; 459 | }; 460 | 461 | // These are not supported by iTerm2. Then I won't bother supporting them. 462 | Terminal.prototype.setForegroundColor = function (){ 463 | // noop 464 | }; 465 | 466 | Terminal.prototype.setBackgroundColor = function (){ 467 | // noop 468 | }; 469 | 470 | Terminal.prototype.syncMouseStyle = function (){ 471 | // noop 472 | }; 473 | 474 | Terminal.prototype.setBracketedPaste = function (state){ 475 | // noop 476 | }; 477 | 478 | Terminal.prototype.ringBell = function (){ 479 | // noop 480 | }; 481 | 482 | Terminal.prototype.setOriginMode = function (state){ 483 | // not supported at the moment 484 | var v_; 485 | this.screen().setRow(0); 486 | return (this.screen().setColumn(v_ = 0),v_); 487 | }; 488 | 489 | Terminal.prototype.setTabStop = function (column){ 490 | for (let idx = 0, items = iter$(this._tabStops), len = items.length, stop; idx < len; idx++) { 491 | stop = items[idx]; 492 | if (stop > column) { 493 | this._tabStops.splice(0,idx,column); 494 | return; 495 | }; 496 | }; 497 | 498 | this._tabStops.push(column); 499 | return this; 500 | }; 501 | 502 | Terminal.prototype.setDefaultTabStops = function (){ 503 | this._tabStops = []; 504 | var column = this._tabWidth; 505 | while (column < this.screen().width()){ 506 | this._tabStops.push(column); 507 | column += this._tabWidth; 508 | }; 509 | return this; 510 | }; 511 | 512 | Terminal.prototype.clearAllTabStops = function (){ 513 | this._tabStops = []; 514 | return this; 515 | }; 516 | 517 | Terminal.prototype.clearTabStopAtCursor = function (){ 518 | for (let idx = 0, items = iter$(this._tabStops), len = items.length, stop; idx < len; idx++) { 519 | stop = items[idx]; 520 | if (stop == this.screen().column()) { 521 | this._tabStops.splice(idx,1); 522 | return; 523 | }; 524 | }; 525 | return this; 526 | }; 527 | 528 | Terminal.prototype.forwardTabStop = function (){ 529 | var v_; 530 | for (let i = 0, items = iter$(this._tabStops), len = items.length, stop; i < len; i++) { 531 | stop = items[i]; 532 | if (stop > this.screen().column()) { 533 | this.screen().setColumn(stop); 534 | return; 535 | }; 536 | }; 537 | 538 | return (this.screen().setColumn(v_ = this.screen().width() - 1),v_); 539 | }; 540 | 541 | Terminal.prototype.backwardTabStop = function (){ 542 | var lastStop = 0; 543 | for (let i = 0, items = iter$(this._tabStops), len = items.length, stop; i < len; i++) { 544 | stop = items[i]; 545 | if (stop > this.screen().column()) { 546 | break; 547 | }; 548 | lastStop = stop; 549 | }; 550 | 551 | return (this.screen().setColumn(lastStop),lastStop); 552 | }; 553 | 554 | Terminal.prototype.fill = function (char$){ 555 | this.screen().fill(char$); 556 | // Currently this method is only used by DECALN, but technically 557 | // I want this method to *just* fill the screen, not reset the cursor. 558 | // That requires a fix in hterm.VT: https://bugs.chromium.org/p/chromium/issues/detail?id=811718 559 | this.screen().setColumn(0); 560 | this.screen().setRow(0); 561 | return this; 562 | }; 563 | 564 | Terminal.prototype.reset = function (){ 565 | this.clearAllTabStops(); 566 | this.softReset(); 567 | return this; 568 | }; 569 | 570 | Terminal.prototype.softReset = function (){ 571 | this.primaryScreen().reset(); 572 | this.alternateScreen().reset(); 573 | this._vt && this._vt.reset && this._vt.reset(); 574 | return this; 575 | }; 576 | 577 | Terminal.prototype.print = function (text){ 578 | if (this._insertMode) { 579 | return this.screen().insertText(text); 580 | } else { 581 | return this.screen().writeText(text); 582 | }; 583 | }; 584 | 585 | Object.assign(Terminal.prototype,{getCursorRow: toScreenBuffer('row'), 586 | getCursorColumn: toScreenBuffer('column'), 587 | deleteLines: toScreen('deleteLines'), 588 | insertLines: toScreen('insertLines'), 589 | lines: toScreen('lines'), 590 | cursorUp: toScreen('cursorUp'), 591 | cursorDown: toScreen('cursorDown'), 592 | 593 | setCursorColumn: toScreenBuffer('setColumn'), 594 | setAbsoluteCursorRow: toScreen('setRow'), 595 | setCursorVisible: toScreenBuffer('setCursorVisible'), 596 | 597 | setVTScrollRegion: toScreen('setScrollRegion'), 598 | vtScrollUp: toScreen('scrollUp'), 599 | 600 | formFeed: toScreen('formFeed'), 601 | lineFeed: toScreen('lineFeed'), 602 | reverseLineFeed: toScreen('reverseLineFeed'), 603 | 604 | insertSpace: toScreen('insertSpace'), 605 | deleteChars: toScreen('deleteChars'), 606 | clear: toScreen('clear'), 607 | eraseToLeft: toScreen('eraseToLeft'), 608 | eraseToRight: toScreen('eraseToRight'), 609 | eraseLine: toScreen('eraseLine'), 610 | eraseAbove: toScreen('eraseAbove'), 611 | eraseBelow: toScreen('eraseBelow')}); 612 | 613 | Terminal.prototype.cursorLeft = function (amount){ 614 | var screen_, v_; 615 | return ((screen_ = this.screen()).setColumn(v_ = screen_.column() - amount),v_); 616 | }; 617 | 618 | Terminal.prototype.cursorRight = function (amount){ 619 | var screen_, v_; 620 | return ((screen_ = this.screen()).setColumn(v_ = screen_.column() + amount),v_); 621 | }; 622 | 623 | Terminal.prototype.setCursorPosition = function (row,column){ 624 | this.screen().setRow(row); 625 | return (this.screen().setColumn(column),column); 626 | }; 627 | 628 | Terminal.prototype.visibleLines = function (){ 629 | return this.screen().visibleLines(); 630 | }; 631 | 632 | 633 | 634 | // At the moment we need to support the following functions: 635 | 636 | // [x] backwardTabStop 637 | // [x] clear 638 | // [x] clearAllTabStops 639 | // [ ] clearHome 640 | // [x] clearTabStopAtCursor 641 | // [ ] copyStringToClipboard 642 | // [x] cursorDown 643 | // [x] cursorLeft 644 | // [x] cursorRight 645 | // [x] cursorUp 646 | // [x] deleteChars 647 | // [x] deleteLines 648 | // [ ] displayImage 649 | // [x] eraseAbove 650 | // [x] eraseBelow 651 | // [x] eraseLine 652 | // [x] eraseToLeft 653 | // [x] eraseToRight 654 | // [x] fill 655 | // [x] formFeed 656 | // [x] forwardTabStop 657 | // [x] getBackgroundColor 658 | // [x] getCursorRow 659 | // [x] getForegroundColor 660 | // [x] getTextAttributes 661 | // [x] insertLines 662 | // [x] insertSpace 663 | // [x] lineFeed 664 | // [x] print 665 | // [x] reset 666 | // [x] restoreCursorAndState 667 | // [x] reverseLineFeed 668 | // [x] ringBell 669 | // [x] saveCursorAndState 670 | // [x] setAbsoluteCursorRow 671 | // [x] setAlternateMode 672 | // [ ] setAutoCarriageReturn 673 | // [x] setBackgroundColor 674 | // [x] setBracketedPaste 675 | // [x] setCursorBlink 676 | // [ ] setCursorColor 677 | // [x] setCursorPosition 678 | // [x] setCursorShape 679 | // [x] setCursorVisible 680 | // [x] setForegroundColor 681 | // [x] setInsertMode 682 | // [x] setOriginMode 683 | // [ ] setReverseVideo 684 | // [ ] setReverseWraparound 685 | // [ ] setScrollbarVisible 686 | // [x] setTabStop 687 | // [ ] setTextAttributes 688 | // [x] setVTScrollRegion 689 | // [ ] setWindowTitle 690 | // [ ] setWraparound 691 | // [x] softReset 692 | // [x] syncMouseStyle 693 | // [ ] vtScrollDown 694 | // [x] vtScrollUp 695 | 696 | -------------------------------------------------------------------------------- /lib/git.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 | 8 | var simpleGit = require('simple-git/promise'); 9 | var parseGitConfig = require('parse-git-config'); 10 | var hostedGitInfo = require('hosted-git-info'); 11 | var gitRepoInfo = require('git-repo-info'); 12 | var cp = require('child_process'); 13 | var ibn = require('isbinaryfile'); 14 | var util = require('./util'); 15 | 16 | var LINESEP = '\n'; 17 | var FLAGS = { 18 | UNSAVED: 1, 19 | UNTRACKED: 2, 20 | IGNORED: 4, 21 | LAZY: 8, 22 | BINARY: 16, 23 | SYMLINK: 32, 24 | MODIFIED: 64, 25 | ADDED: 128, 26 | RENAMED: 256, 27 | COPIED: 512, 28 | DELETED: 1024, 29 | 30 | "M": 64, 31 | "A": 128, 32 | "D": 1024, 33 | "R": 256, 34 | "?": 2 35 | }; 36 | 37 | 38 | var validate = { 39 | ref: function(val) { return (/^[\:\/\-\.\w]+$/).test(val); }, 40 | sha: function(val) { return (/^[\:\/\-\.\w]+$/).test(val); } 41 | }; 42 | 43 | exports.exec = self.exec = function (command,cwd){ 44 | return cp.execSync(command,{cwd: cwd,env: process.env}); 45 | }; 46 | 47 | exports.execSync = self.execSync = function (command,cwd){ 48 | return cp.execSync(command,{cwd: cwd,env: process.env}); 49 | }; 50 | 51 | exports.shaExists = self.shaExists = function (cwd,treeish){ 52 | try { 53 | self.execSync(("git cat-file -t " + treeish),cwd); 54 | return true; 55 | } catch (e) { 56 | return false; 57 | }; 58 | }; 59 | 60 | exports.fetchSha = self.fetchSha = function (cwd,sha,ref){ 61 | if (self.shaExists(cwd,sha)) { return true }; 62 | console.log("fetchSha",cwd,sha,ref); 63 | 64 | let cmd = ref ? (("git fetch origin " + ref)) : "git fetch"; 65 | let res = self.execSync(cmd,cwd); 66 | 67 | return true; 68 | }; 69 | 70 | exports.isValidTreeish = self.isValidTreeish = function (value){ 71 | return value.match(/^[\:\/\-\.\w]+$/); 72 | }; 73 | 74 | /* 75 | --raw --numstat 76 | :100644 100644 06f59bf... 98ad458... M README.md 77 | :100644 100644 5474c93... 801afcc... M server.js 78 | :000000 100644 0000000... 3760da4... A src/api.imba 79 | :100644 100644 b116b25... cfee64d... M src/main.imba 80 | :000000 100644 0000000... 698007b... A www/playground.js 81 | 7 1 README.md 82 | 1 1 server.js 83 | 4 0 src/api.imba 84 | 9 1 src/main.imba 85 | 1 0 www/playground.js 86 | 87 | Should be able to call this asynchronously from socket 88 | WARN this doesnt show actual diff between the two, but rather 89 | the changes in head relative to the branch of base 90 | */ 91 | 92 | exports.getGitDiff = self.getGitDiff = function (cwd,base,head,includePatch){ 93 | if(includePatch === undefined) includePatch = false; 94 | console.log("getGitDiff",cwd,base,head); 95 | 96 | 97 | let baseSha = self.execSync(("git merge-base " + head + " " + base),cwd).toString().trim(); 98 | let result = { 99 | head: head, 100 | base: baseSha, 101 | diff: [] 102 | }; 103 | 104 | let raw = self.execSync(("git diff --raw --numstat " + baseSha + ".." + head),cwd).toString(); 105 | let lines = raw.split('\n'); 106 | let len = Math.floor(lines.length * 0.5); 107 | let numstat = lines.splice(len,len + 2).map(function(ln) { 108 | return ln.split(/\s+/).map(function(item) { return (/^\d+$/).test(item) ? parseInt(item) : item; }); 109 | }); 110 | 111 | for (let i = 0, items = iter$(lines), len_ = items.length, entry; i < len_; i++) { 112 | entry = items[i]; 113 | let mode = entry.split(/[\s\t]/)[4]; 114 | let file = entry.slice(entry.indexOf('\t') + 1); 115 | let node = { 116 | name: file, 117 | mode: mode, 118 | ins: numstat[i][0], 119 | rem: numstat[i][1] 120 | }; 121 | 122 | if (includePatch) { 123 | if (node.ins == '-') { 124 | continue; 125 | }; 126 | 127 | if (mode == 'A' || mode == 'M') { 128 | // Should also check size and if binary 129 | 130 | let body = self.execSync(("git cat-file -p " + head + ":" + file),cwd).toString(); 131 | node.body = body; 132 | if (mode == 'M') { 133 | let patch = self.execSync(("git diff " + baseSha + ".." + head + " -- " + file),cwd).toString(); 134 | node.patch = patch; 135 | }; 136 | } else if (mode == 'D') { 137 | // possibly include the previous value? 138 | true; 139 | }; 140 | }; 141 | 142 | result.diff.push(node); 143 | }; 144 | return result; 145 | }; 146 | 147 | 148 | exports.getGitBlob = self.getGitBlob = function (cwd,sha,refToFetch){ 149 | console.log("getGitBlob",cwd,sha,refToFetch); 150 | if (!self.isValidTreeish(sha)) { 151 | console.log("blob did not exist??",cwd,sha); 152 | return null; 153 | }; 154 | // make sure we've fetched latest from remote 155 | self.fetchSha(cwd,sha,refToFetch); 156 | 157 | try { 158 | let buffer = self.execSync('git cat-file -p ' + sha,cwd); 159 | // not certain that we have the oid? 160 | let obj = { 161 | oid: sha, 162 | body: null, 163 | size: buffer.length, 164 | type: 'blob' 165 | }; 166 | if (!ibn.sync(buffer,obj.size) && obj.size < 200000) { 167 | obj.body = buffer.toString(); 168 | }; 169 | return obj; 170 | } catch (error) { 171 | console.log("error from getGitBlob"); 172 | return null; 173 | }; 174 | }; 175 | 176 | exports.getGitTree = self.getGitTree = function (cwd,sha,refToFetch){ 177 | var ary; 178 | console.log("getGitTree",cwd,sha,refToFetch); 179 | if (!self.isValidTreeish(sha)) { return null }; 180 | 181 | self.fetchSha(cwd,sha,refToFetch); 182 | 183 | try { 184 | let buffer = self.execSync('git ls-tree -z -l ' + sha,cwd); 185 | let tree = []; 186 | for (let i = 0, items = iter$(buffer.toString().split('\0')), len = items.length, line; i < len; i++) { 187 | line = items[i]; 188 | var ary = iter$(line.split(/(?:\ |\t)+/g));let mode = ary[0],type = ary[1],sha = ary[2],osize = ary[3]; 189 | let name = line.substr(line.indexOf('\t') + 1); 190 | if (!name) { continue; }; 191 | tree.push({sha: sha,size: osize,mode: mode,path: name,type: type}); 192 | }; 193 | return { 194 | oid: sha, 195 | type: 'tree', 196 | data: {nodes: tree} 197 | }; 198 | } catch (error) { 199 | return null; 200 | }; 201 | }; 202 | 203 | exports.getGitInfo = self.getGitInfo = function (cwd){ 204 | var repo, conf, ary, origin; 205 | var data = {}; 206 | if (repo = gitRepoInfo._findRepo(cwd)) { 207 | var info = gitRepoInfo(cwd); 208 | data.branch = info.branch; 209 | data.sha = info.sha; 210 | data.tag = info.tag; 211 | data.commit = info.commitMessage; 212 | data.root = info.root; 213 | } else { 214 | return data; 215 | }; 216 | 217 | if (conf = parseGitConfig.sync({cwd: cwd,path: cwd + '/.git/config'})) { 218 | let branchInfo = self.execSync("git branch -vv --no-abbrev --no-color",cwd).toString(); 219 | var ary = iter$(branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/));let m = ary[0],name = ary[1],head = ary[2],remote = ary[3]; 220 | data.remote = remote; 221 | 222 | if (origin = conf['remote "origin"']) { 223 | data.origin = origin.url; 224 | }; 225 | }; 226 | 227 | return data; 228 | }; 229 | 230 | function Git(owner,root,options){ 231 | var repo, conf, ary, origin; 232 | if(options === undefined) options = {}; 233 | this._owner = owner; 234 | this._root = root; 235 | this._summary = {}; 236 | this._diffs = {}; 237 | this._untracked = {}; 238 | this._changed = {}; 239 | this._objects = {}; 240 | this._baseRef = options.baseRef || 'HEAD'; 241 | 242 | this._trees = {}; 243 | this._trees.base = {}; 244 | this._trees.modified = Object.create(this._trees.base); 245 | this._trees.untracked = Object.create(this._trees.modified); 246 | 247 | if (repo = gitRepoInfo._findRepo(this.cwd())) { 248 | var info = gitRepoInfo(this.cwd()); 249 | this._summary.branch = info.branch; 250 | this._summary.sha = info.sha; 251 | this._summary.tag = info.tag; 252 | this._summary.commit = info.commitMessage; 253 | this._summary.root = info.root; 254 | this.log("GIT",info); 255 | }; 256 | 257 | 258 | if (conf = parseGitConfig.sync({cwd: this.cwd(),path: this.cwd() + '/.git/config'})) { 259 | this.log("GITCONF",this.cwd(),conf); 260 | let branchInfo = this.exec("branch -vv --no-abbrev --no-color").toString(); 261 | var ary = iter$(branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/));let m = ary[0],name = ary[1],head = ary[2],remote = ary[3]; 262 | this._summary.remote = remote; 263 | 264 | if (origin = conf['remote "origin"']) { 265 | this.setOrigin(this._summary.origin = origin.url); 266 | }; 267 | }; 268 | 269 | this; 270 | }; 271 | 272 | Imba.subclass(Git,Component); 273 | exports.Git = Git; // export class 274 | Git.prototype.origin = function(v){ return this._origin; } 275 | Git.prototype.setOrigin = function(v){ this._origin = v; return this; }; 276 | Git.prototype.info = function(v){ return this._info; } 277 | Git.prototype.setInfo = function(v){ this._info = v; return this; }; 278 | Git.prototype.status = function(v){ return this._status; } 279 | Git.prototype.setStatus = function(v){ this._status = v; return this; }; 280 | Git.prototype.baseRef = function(v){ return this._baseRef; } 281 | Git.prototype.setBaseRef = function(v){ this._baseRef = v; return this; }; 282 | 283 | Git.prototype.cwd = function (){ 284 | return this._root; 285 | }; 286 | 287 | Git.prototype.repoRoot = function (){ 288 | return this.isRepository() && this._summary.root; 289 | }; 290 | 291 | Git.prototype.exec = function (cmd){ 292 | // todo: add check 293 | if (cmd instanceof Array) { 294 | return cp.execFileSync('git',cmd,{cwd: this.cwd(),env: process.env}); 295 | } else { 296 | return cp.execSync('git ' + cmd,{cwd: this.cwd(),env: process.env}); 297 | }; 298 | }; 299 | 300 | Git.prototype.execAsync = function (cmd){ 301 | var self = this; 302 | return new Promise(function(resolve,reject) { 303 | var o = {cwd: self.cwd(),env: process.env,maxBuffer: 1024 * 500}; 304 | var handler = function(err,stdout,stderr) { 305 | if (err) { 306 | self.log("error from exec"); 307 | self.log(err && err.message); 308 | self.log(stderr.toString()); 309 | return reject(err); 310 | }; 311 | 312 | let str = stdout.toString(); 313 | if (str[str.length - 1] == '\n') { 314 | str = str.slice(0,-1); 315 | }; 316 | return resolve(str); 317 | }; 318 | 319 | if (cmd instanceof Array) { 320 | self.log("cmd is array"); 321 | o.encoding = 'utf-8'; 322 | return cp.execFile('git',cmd,o,handler); 323 | } else { 324 | return cp.exec('git ' + cmd,o,handler); 325 | }; 326 | }); 327 | }; 328 | 329 | Git.prototype.isRepository = function (){ 330 | return !!this._summary.branch; 331 | }; 332 | 333 | Git.prototype.gitRepoRef = function (){ 334 | if (!(this.isRepository())) { return null }; 335 | let m = this.origin().match(/github\.com[\/\:](.*?)(\.git)?$/); 336 | return m ? m[1] : null; 337 | }; 338 | 339 | Git.prototype.parseGitTree = function (text){ 340 | var ary; 341 | var tree = {}; 342 | // SP SP SP TAB 343 | for (let i = 0, items = iter$(text.split('\0')), len = items.length, line; i < len; i++) { 344 | line = items[i]; 345 | var ary = iter$(line.split(/(?:\ |\t)+/g));let mode = ary[0],type = ary[1],oid = ary[2],osize = ary[3]; 346 | let name = line.substr(line.indexOf('\t') + 1); 347 | if (!name) { continue; }; 348 | tree[name] = {oid: oid,size: osize,mode: mode}; 349 | tree[name.toLowerCase()] = tree[name]; 350 | }; 351 | return tree; 352 | }; 353 | 354 | Git.prototype.load = function (){ 355 | return Promise.resolve(this); 356 | // status = await @git.status 357 | }; 358 | 359 | Git.prototype.git = function (){ 360 | return this._git || (this._git = simpleGit(this.cwd())); 361 | }; 362 | 363 | Git.prototype.summary = function (){ 364 | return this._summary; 365 | }; 366 | 367 | Git.prototype.config = function (){ 368 | return this._config || (this._config = {}); 369 | }; 370 | 371 | Git.prototype.tree = function (){ 372 | if (!(this.isRepository())) { 373 | return this._tree = {}; 374 | }; 375 | 376 | if (this._tree) { 377 | return this._tree; 378 | }; 379 | 380 | var raw = this.exec('ls-tree -rltz HEAD').toString(); 381 | this._tree = this.parseGitTree(raw); 382 | this.refreshUntrackedFiles(); 383 | return this._tree; 384 | }; 385 | 386 | Git.prototype.cat = function (oid){ 387 | // todo: add check 388 | var res = this.exec('cat-file -p ' + oid); 389 | return res; 390 | }; 391 | 392 | Git.prototype.read = function (oid){ 393 | var buf; 394 | let existing = this._objects[oid]; 395 | if (!existing && oid) { 396 | if (buf = this.cat(oid)) { 397 | let obj = this._objects[oid] = { 398 | oid: oid, 399 | body: null, 400 | size: buf.length 401 | }; 402 | if (!ibn.sync(buf,buf.length) && obj.size < 200000) { 403 | obj.body = buf.toString(); 404 | }; 405 | 406 | this.emit('oread',oid,obj); 407 | }; 408 | }; 409 | return; 410 | }; 411 | 412 | 413 | // returns a formatted list of changes in the repo, relative 414 | // to the supplied baseRef (the default is HEAD, which means 415 | // current uncommitted changes) 416 | Git.prototype.diff = function (treeish,hard){ 417 | var ary, v_, $1, $2; 418 | if(treeish === undefined) treeish = this._baseRef; 419 | if(hard === undefined) hard = false; 420 | if (!(this.isRepository())) { return {map: {}} }; 421 | var key = treeish; // + includeBlobs 422 | if (this._diffs[key] && !hard) { return this._diffs[key] }; 423 | this.log(("git diff " + treeish + " " + this.cwd())); 424 | // add these to the actual tree maybe? 425 | var prevChanges = this._changed; 426 | 427 | var changes = []; 428 | var map = {}; 429 | // to ensure git recalculates before diff-index 430 | this.exec('status'); 431 | 432 | // todo: add check 433 | var raw = this.exec('diff-index -z ' + treeish).toString(); 434 | var nullOid = "0000000000000000000000000000000000000000"; 435 | for (let i = 0, items = iter$(raw.split('\0:')), len = items.length; i < len; i++) { 436 | let parts = items[i].split('\0'); 437 | var ary = iter$(parts[0].split(' '));var mode0 = ary[0],mode1 = ary[1],oid0 = ary[2],oid1 = ary[3],status = ary[4]; 438 | var change = { 439 | oldPath: parts[1], 440 | newPath: parts[2] || parts[1], 441 | oldOid: oid0, 442 | newOid: oid1, 443 | status: status 444 | }; 445 | let path = change.newPath || change.oldPath; 446 | if (change.newPath == change.oldPath) { (((v_ = change.newPath),delete change.newPath, v_)) }; 447 | if (change.newOid == nullOid) { ((($1 = change.newOid),delete change.newOid, $1)) }; 448 | if (change.oldOid == nullOid) { ((($2 = change.oldOid),delete change.oldOid, $2)) }; 449 | 450 | // if includeBlobs and change:oldOid 451 | // let buf = cat(change:oldOid) 452 | // if !ibn.sync(buf,buf:length) 453 | // change:oldBody = buf.toString 454 | 455 | changes.push(change); 456 | }; 457 | 458 | if (treeish == 'HEAD') { 459 | // if we are asking for the uncommitted changes we also want the added files 460 | // that are not yet staged, and that are not ignored via .gitignore) 461 | var toAdd = this.exec('ls-files --others --exclude-standard -z').toString().split('\0'); // windows? 462 | 463 | for (let i = 0, items = iter$(toAdd), len = items.length, name; i < len; i++) { 464 | name = items[i]; 465 | if (!name) { continue; }; 466 | changes.push({newPath: name,status: '?'}); 467 | }; 468 | }; 469 | 470 | 471 | for (let i = 0, len = changes.length, change; i < len; i++) { 472 | change = changes[i]; 473 | if (change.newPath) { 474 | map[change.newPath] = change; 475 | }; 476 | if (change.oldPath) { 477 | map[change.oldPath] = change; 478 | }; 479 | }; 480 | 481 | return this._diffs[key] = { 482 | baseRef: treeish, 483 | changes: changes, 484 | map: map 485 | }; 486 | }; 487 | 488 | // def isIgnored src 489 | Git.prototype.refreshUntrackedFiles = function (dir){ 490 | // todo: add check 491 | if(dir === undefined) dir = ''; 492 | var paths = this.exec('ls-files --others --exclude-standard -z ' + dir).toString().split('\0'); 493 | var tree = this.tree(); 494 | for (let i = 0, items = iter$(paths), len = items.length, path; i < len; i++) { 495 | path = items[i]; 496 | if (!path) { continue; }; 497 | if (!tree[path]) { 498 | tree[path] = {status: '?'}; 499 | }; 500 | }; 501 | 502 | return this; 503 | }; 504 | 505 | Git.prototype.oidForPath = function (path){ 506 | return this.tree()[path]; 507 | }; 508 | 509 | Git.prototype.commit = function (options){ 510 | var term; 511 | var msg = options.message; 512 | // console.log "commit",msg,options 513 | // TODO escape git message 514 | var cmd = ("git commit --allow-empty -m \"" + msg + "\""); 515 | // var res = await git.commit(['--allow-empty','-m',msg]) 516 | // console.log "did commit?",res,cmd 517 | if (options.push) { 518 | cmd += " && git push"; 519 | }; 520 | 521 | if (term = this._owner._terminals[0]) { 522 | // make sure we are in the right directory 523 | term.write(("cd " + this.cwd() + "\n")); 524 | term.write(cmd + '\n'); 525 | }; 526 | return this; 527 | }; 528 | 529 | function GitRepo(owner,root,options){ 530 | var repo, conf, ary, origin; 531 | if(options === undefined) options = {}; 532 | this._owner = owner; 533 | this._root = root; 534 | this._summary = {}; 535 | this._intervals = {}; 536 | this._refs = {}; 537 | 538 | if (repo = gitRepoInfo._findRepo(this.cwd())) { 539 | var info = gitRepoInfo(this.cwd()); 540 | this._summary.branch = info.branch; 541 | this._summary.sha = info.sha; 542 | this._summary.tag = info.tag; 543 | this._summary.commit = info.commitMessage; 544 | this._summary.root = info.root; 545 | this.log("GIT",info); 546 | }; 547 | 548 | 549 | if (conf = parseGitConfig.sync({cwd: this.cwd(),path: this.cwd() + '/.git/config'})) { 550 | this.log("GITCONF",this.cwd(),conf); 551 | let branchInfo = this.exec("branch -vv --no-abbrev --no-color").toString(); 552 | var ary = iter$(branchInfo.match(/\* (\w+)\s+([a-f\d]+)\s(?:\[([^\]]+)\])?/));let m = ary[0],name = ary[1],head = ary[2],remote = ary[3]; 553 | this._summary.remote = remote; 554 | 555 | if (origin = conf['remote "origin"']) { 556 | this.setOrigin(this._summary.origin = origin.url); 557 | }; 558 | }; 559 | this; 560 | }; 561 | 562 | Imba.subclass(GitRepo,Git); 563 | exports.GitRepo = GitRepo; // export class 564 | GitRepo.prototype.start = function (){ 565 | this.emit('start',this._summary); 566 | // dont fetch all the time 567 | // @intervals:fetch = setInterval(self:fetch.bind(self),10000) 568 | this.updateRefs(); 569 | this.sendRefs(); 570 | return this; 571 | }; 572 | 573 | GitRepo.prototype.updateRefs = function (){ 574 | var ary; 575 | let refs = {}; 576 | let rawRefs = this.exec('show-ref').toString(); 577 | for (let i = 0, items = iter$(rawRefs.split(LINESEP)), len = items.length; i < len; i++) { 578 | var ary = iter$(items[i].split(" "));var sha = ary[0],ref = ary[1]; 579 | refs[ref] = sha; 580 | }; 581 | this._refs = refs; 582 | return this; 583 | }; 584 | 585 | GitRepo.prototype.sendRefs = function (){ 586 | this.emit('refs',this._refs); 587 | return this; 588 | }; 589 | 590 | GitRepo.prototype.checkTest = function (){ 591 | this.log("called checkTest"); 592 | return {a: 1,b: 2}; 593 | }; 594 | 595 | GitRepo.prototype.grep = async function (text){ 596 | // Trim the text so trailing newlines will not prevent a match. 597 | text = text.trim(); 598 | 599 | // find the last commit that is shared between local branch and upstream 600 | // FIXME now always looking for the location relative to remote HEAD 601 | 602 | // find last commit in history that we know is synced to the remote 603 | var rootSha = null; // await execAsync('merge-base HEAD @{u}') 604 | let lastOriginCommit = await this.execAsync('log --remotes=origin -1 --pretty=oneline'); 605 | if (lastOriginCommit) { 606 | rootSha = lastOriginCommit.split(" ")[0]; 607 | }; 608 | 609 | // let rootSha = await execAsync('merge-base HEAD @{u}') 610 | // refs/remotes/origin/HEAD 611 | 612 | 613 | let lines = text.split('\n'); 614 | let cmd = [ 615 | "grep", 616 | "--files-with-matches", 617 | "--all-match", 618 | "-n", 619 | "-F", 620 | '-e', 621 | text 622 | ]; 623 | 624 | let matches = []; 625 | let res; 626 | this.log("grep",JSON.stringify(text),lines.length,cmd,text.indexOf('\t')); 627 | try { 628 | res = await this.execAsync(cmd); 629 | } catch (e) { 630 | return []; 631 | }; 632 | 633 | if (res) { 634 | let files = res.split("\n"); 635 | if (len$(files) > 20) { 636 | // too many hits 637 | return matches; 638 | }; 639 | 640 | for (let i = 0, items = iter$(files), len = items.length, file; i < len; i++) { 641 | file = items[i]; 642 | this.log(("git cat-file -p " + rootSha + ":" + file)); 643 | var sha = rootSha; 644 | // if the file does not exist 645 | try { 646 | // if the file does not exist in HEAD - or the position is different 647 | let body; 648 | try { 649 | body = await this.execAsync(("cat-file -p " + sha + ":" + file)); 650 | } catch (e) { }; 651 | 652 | if (!body || body.indexOf(text) == -1) { 653 | sha = await this.execAsync("rev-parse HEAD"); 654 | body = await this.execAsync(("cat-file -p " + sha + ":" + file)); 655 | this.log(("could not find from remote head, use local head instead " + sha)); 656 | }; 657 | 658 | let start = 0; 659 | let idx; 660 | 661 | while ((idx = body.indexOf(text,start)) >= 0){ 662 | let match = { 663 | commit: rootSha, 664 | file: file, 665 | loc: idx, 666 | line: util.countLines(body.slice(0,idx)) 667 | }; 668 | 669 | // include full lines? 670 | let url = ("https://github.com/" + this.gitRepoRef() + "/blob/" + rootSha + "/" + file + "#L" + (match.line)); 671 | if (len$(lines) > 1) { 672 | url += ("-L" + (match.line + len$(lines) - 1)); 673 | }; 674 | let lang = file.split(".").pop(); 675 | match.permalink = url; 676 | match.code = text; 677 | match.language = lang; 678 | match.markdown = ("```" + lang + "\n" + text + "\n```\n[↳ " + file + "](" + url + ")"); 679 | start = idx + text.length; 680 | matches.push(match); 681 | }; 682 | } catch (e) { 683 | this.log(("error grepping " + file)); 684 | }; 685 | }; 686 | }; 687 | 688 | return matches; 689 | }; 690 | 691 | GitRepo.prototype.fetch = async function (){ 692 | if (this._fetching) { return }; 693 | this.emit('fetching',this._fetching = true); 694 | try { 695 | var res = await this.execAsync('fetch -a -f origin "refs/pull/*:refs/pull/*"'); 696 | // console.log "result is",res 697 | // if there is a result - some branches may have updated -- send new refs 698 | if (res && String(res).length > 2) { 699 | this.updateRefs(); 700 | this.sendRefs(); 701 | }; 702 | } catch (e) { 703 | this; 704 | }; 705 | 706 | this.emit('fetched',this._fetching = false); 707 | return; 708 | }; 709 | 710 | GitRepo.prototype.fetch_ref = async function (ref,expectedSha){ 711 | // console.log "fetch_ref",ref,expectedSha 712 | if (!validate.ref(ref)) { 713 | return null; 714 | }; 715 | 716 | try { 717 | let curr = this._refs[ref]; 718 | 719 | if (expectedSha && curr == expectedSha) { 720 | return curr; 721 | }; 722 | var cmd = ("fetch -a -f origin \"" + ref + ":" + ref + "\""); 723 | // console.log "call cmd",cmd 724 | var res = await this.execAsync(cmd); 725 | // console.log "result from cmd",res 726 | this.updateRefs(); 727 | return this._refs[ref]; 728 | } catch (e) { }; 729 | return 10; 730 | }; 731 | 732 | 733 | GitRepo.prototype.dispose = function (){ 734 | clearInterval(this._intervals.fetch); 735 | return this; 736 | }; 737 | --------------------------------------------------------------------------------