├── .gitignore ├── README.md ├── bin └── nvi ├── config.json ├── docs └── screenshot.png ├── loop ├── package.json ├── precompile ├── controllers │ ├── application.coffee │ ├── client.coffee │ └── server.coffee ├── main.coffee ├── models │ ├── HydraBuffer.coffee │ ├── Logger.coffee │ ├── Socket.coffee │ └── User.coffee └── views │ ├── Bar.coffee │ ├── BufferView.coffee │ ├── BufferViewCursor.coffee │ ├── Cell.coffee │ ├── Tab.coffee │ ├── Terminal.coffee │ └── Window.coffee ├── static ├── controllers │ ├── application.js │ ├── client.js │ └── server.js ├── main.js ├── models │ ├── HydraBuffer.js │ ├── Logger.js │ ├── Socket.js │ └── User.js └── views │ ├── Bar.js │ ├── BufferView.js │ ├── BufferViewCursor.js │ ├── Cell.js │ ├── Tab.js │ ├── Terminal.js │ ├── View.js │ ├── ViewCursor.js │ └── Window.js └── test └── test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | nvi.log 3 | nvi-*.log 4 | sample*.txt 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Very opinionated Node.JS VI clone 2 | 3 | This will become my dream collaborative editor. 4 | 5 | **WARNING:** Its in a very ALPHA state right now. Contributions welcome. 6 | 7 | ![Screenshot](https://raw.github.com/mikesmullin/nvi/development/docs/screenshot.png) 8 | 9 | ## Vision: 10 | 11 | We're taking the best parts of Vim: 12 | * 256-color text-based user interface 13 | * works locally via terminal incl. tmux 14 | * works remotely via ssh 15 | * modes 16 | * buffers 17 | * block edit 18 | * macros 19 | * mouse support 20 | * plugins: 21 | * syntax highlighting 22 | * nerdtree 23 | * ctags 24 | * taglist 25 | 26 | and making them better: 27 | * collaborative editing 28 | * multiple writers w/ colored cursors 29 | * follow/professor mode 30 | * type inference and code completion (coffeescript will be first) 31 | * easily configured and modded via coffeescript and javascript 32 | * unforgiving bias toward modern systems 33 | 34 | ## Achieved Features: 35 | 36 | * 256-color terminal text-based user interface 37 | * tiled window management for buffers 38 | * modes: COMBO, NORMAL, REPLACE, BLOCK, LINE-BLOCK, COMMAND 39 | * connect multiple nvi sessions in host-guests configuration 40 | * local unix and remote tcp socket support for pairing 41 | 42 | ## Installation 43 | ```bash 44 | npm install nvi -g 45 | ``` 46 | 47 | ## Usage 48 | ```bash 49 | nvi # new file 50 | nvi # existing file 51 | ``` 52 | 53 | ## Getting Started 54 | 55 | Nvi modes are not Vim modes. 56 | Nvi NORMAL is Vim INSERT. 57 | Nvi COMBO is Vim NORMAL. 58 | These mode names are less confusing to new users. 59 | When you first run Nvi, you begin in Nvi NORMAL mode. 60 | This is intended to provide new users with a sense of familiarity as it is conventional to nano or Notepad on first impression. 61 | This is aided by default hotkey behaviors like: 62 | * Esc: enter Nvi COMBO mode 63 | * Ctrl+S: Save, enter Nvi COMBO mode 64 | * Ctrl+Q: Prompt to save if any changes, then quit 65 | * Ctrl+X: Cut selection to clipboard 66 | * Ctrl+C: Copy selection to clipboard 67 | * Ctrl+V: Paste clipboard 68 | 69 | ## Beginning a Collaborative Editing Session 70 | 71 | * Open `nvi` twice 72 | * In the first Nvi, press `` to enter COMBO mode, type: `:listen`, and hit `` 73 | * In the second Nvi, press `` to enter COMBO mode, type: `:connect`, and hit `` 74 | 75 | ## Rambling Specification 76 | 77 | only transmit files when they are needing to be opened, as they need to be opened, 78 | and only the parts of the file that need to be rendered 79 | 80 | detect changes to files on disk using mtime 81 | and auto-sync those to the group 82 | 83 | detect an out-of-sync state (what is hash of chars on line RAND characters RANDx - RANDY?) if its not the same then resend the document 84 | 85 | buffer is closely tied to the view 86 | it only fetches enough bytes to fill the view 87 | it grabs chunks equal to the current view's line in bytes 88 | 89 | buffer is aware of how many views are using it 90 | before it seeks the file for more data, 91 | it determines which stripes are being used across all views 92 | it considers overlapping views 93 | and only fetches each line it needs once, in order from first byte to last byte in file 94 | and and when it fetches an area of the buffer that is overlapping in the view render 95 | it updates both views not one then again for the other 96 | 97 | for my collaborative mode, 98 | the guests will not receive mirror copies of the entire directory 99 | the host will share the directory skeleton for the current directory 100 | and recursively but only upon a watch request 101 | watchers will receive push notification from the host (e.g. file added or removed under watchdir) 102 | 103 | any file a client requests to open, the client, and all other clients will be aware of 104 | client will request the file from host 105 | host will broadcast to other clients that the client has opened the file 106 | and when it has closed the file 107 | 108 | host will spoon-feed data to the clients like a buffer would 109 | so the clients only get the data they need to look at that moment 110 | 111 | also, diffs are only broadcasted within the context of the all clients collective views, as well 112 | 113 | if a file changes on the host system 114 | then we only broadcast that change if it is within the overlapped views of clients 115 | 116 | this is very similar to how ssh vim tmux session would go, except we now have multiple cursors 117 | 118 | cursor position updates will be notified (client->host) and rebroadcasted (others) with a throttle 119 | but diffs will not be based on cursor position and exact keypress matches 120 | they will be actual patch diffs in a minimalist format 121 | 122 | and what about binary files? 123 | these will not attempt to be diffed 124 | on modification, the latest (mtime) copy overrides everyone else's 125 | (clients will need to have a filesystem mtime watch too) 126 | the process for updating a binary file as a client would be: 127 | connect 128 | right-click > download the file or folder 129 | modify the file locally 130 | mtime inotify triggers guests to receive the bin file as well 131 | any new files created (atime) get auto-shared to the host 132 | client can be asked to resolve merge conflicts 133 | 134 | its basically like a rapid git session 135 | 136 | except i don't like all the assumptions made above re. bin files 137 | i think if the client wants to share a new file, it should be explicit, at least for now 138 | and let's see if it becomes a hassle 139 | or if a client wants to modify an existing binfile, it should also be explicit 140 | because in reality these will change a lot (e.g. during a photoshop editing session) 141 | and we really only care about the final result 142 | and most likely the group will want to talk it over and approve it before accepting it anyway 143 | 144 | it would be cool if it had git support so that team commits could be saved with attribution for the authors involved when the commit was made 145 | and so the commits only happened on the host side 146 | same with pushes 147 | 148 | NO that silly git diff is not applicable; it doesn't resolve changes on the same line 149 | so i need my own 150 | i think instead i will just translate the cursor/block edit operations to binary opcodes 151 | 152 | basically whatever you can do with a block, there's an opcode for that 153 | so shift selection one character right, shift selection one character left (unlikely use case) 154 | duplicate selection up/down 155 | delete selection 156 | insert before selection 157 | insert after selection 158 | move selection up/down 159 | backspace/delete x characters 160 | insert "literal" characters; assume we are working on an ascii file always 161 | pretty confident that's a safe assumption; collaborative editing binary files 162 | will have their own app and experience 163 | move cursor absolute x, y or relative -x, -y 164 | 165 | 166 | when syncing hydrabuffers / views 167 | clients will write changes to disk, without filler 168 | as the view scrolls up/down the file will be prepended/appended 169 | until the file is complete 170 | 171 | if a user changes a file outside of nvim which they don't have a complete dataset for 172 | hmm 173 | 174 | ok getting too fancy here / prematurely optimizing / overengineering 175 | let's K.I.S.S. 176 | 177 | we'll transfer the whole ascii file to clients as they request to view it 178 | and then we'll work to keep it in sync from there 179 | its really not that much data to transfer 180 | 181 | except am i solving anything the other way around? 182 | a lot of times sync errors happen while users are editing in the areas 183 | we aren't currently watching for changes in 184 | 185 | i kind of like my ssh-vim-like approach 186 | 187 | ok so in that vain, the client does not buffer or store ANY data locally 188 | this way it cannot possibly go out of sync 189 | if it does, they just close/reopen the file to get back in sync 190 | sync will happen on a per-view basis 191 | 192 | same as if you opened a file and someone changed it while you were reading it 193 | 194 | this is also good because it functionally reinforces the idea of 195 | requiring clients to check-in new files or binary file changes 196 | by issuing special commands to have them imported to the host 197 | 198 | and then with that i can refuse to render binary files altogether 199 | just display an error like 'binary files unsupported; utf8 text only' 200 | 201 | so if someone wanted to edit a binary file in this flow, they would 202 | have to issue a special command to fetch the binfile 203 | then edit it locally 204 | then issue a special command to upload the binfile 205 | thus greatly reducing incidental sync errors 206 | and unnecessary data transfer 207 | 208 | 209 | 210 | 211 | 212 | 213 | for a view statusbar just show: 214 | 215 | relative path, bold filename 216 | dont show line endings like 'unix | mac | win' thats pointless 217 | instead highlight whitespace aggressively and with favoritism for unix 218 | show percentage of file remaining 219 | and cursor x:y pos 220 | 221 | 222 | show treeview directory structure 223 | i'll have to implement this too because it needs remote support 224 | 225 | the modes i'll implement will be: 226 | NORMAL except i'll call it COMBO 227 | INSERT except i'll call it NORMAL 228 | REPLACE i'll keep this the same 229 | V-LINE except i'll call it LINE-BLOCK 230 | V-BLOCK exept i'll call it BLOCK 231 | 232 | 233 | 234 | treat views like a tiling window manager 235 | create a Tab object like tmux-style tabs 236 | tabs will have user's names in collab mode 237 | 238 | so it starts with one tab and one view 239 | you can never have fewer than one view open 240 | if you do its just resetting the file to an untitled in-memory buffer 241 | 242 | when you add another view 243 | you can only do so by using a specific key combination / cmd 244 | to indicate a vsplit or hsplit or new tab 245 | 246 | so when this happens, the default is to divide the tab 50/50 247 | then the user can click and drag to resize from there 248 | 249 | a view can never be less than 1 char high 250 | and 1 char wide; 251 | for every line that isnt wrapped but is cutoff by a short viewport, a > is ending that line 252 | 253 | opening more than one tab creates a 1char high tabbar at top of the screen 254 | 255 | 256 | 257 | i may split View into ReadOnlyView extended by EditableView 258 | 259 | 260 | HydraBuffer doesn't need to know about cursors 261 | the view is always the one i keep landing in 262 | its the one that colors the cursor 263 | and shows it moving around the view 264 | it has really nothing to do with the buffer 265 | if a cursor moves off of a view, does it matter? (if a tree falls in the forest, does it make a sound?) 266 | no. 267 | 268 | 269 | 270 | 271 | so there's a difference between a stream and a buffer here 272 | the host is going to need to open a buffer 273 | this also means that the entire file that is being edited needs to 274 | be able to fit within the available ram memory 275 | if we are going to save it back to the disk whole again 276 | this is all a limitation of not being able to write in the middle of a file 277 | hmm 278 | i mean technically that's difficult because if anyone could do that 279 | without a lock on the file 280 | then you'd never know where you last left off 281 | in order to insert your changes 282 | plus even if a fancy filesystem supported this 283 | you'd still have to implement your own layer on top of that 284 | like a .vdi or .vmdk file 285 | to make it fully compatible with other filesystems e.g., windows, sftp, nfs, etc. 286 | 287 | the traditional options i have at my disposal are: writeFile (total replacement) and appendFile (write to end only) 288 | 289 | so i can only use writeFile() 290 | which means i have to pass the entire file 291 | which means the entire file must fit in memory 292 | which means buffering only the maximum rendered by the views in memory is pointless 293 | 294 | well unless i get really creative with diffs 295 | for example, if while using a readStream, i notate the offset location 296 | and i assume that the file had a lock on it (could make my own .*.lck files) 297 | then i would never need the whole file in memory 298 | i could just replay chunks from the original file up to the point of my stream offset location 299 | then write my modified contents 300 | and write chunks of all this to a temporary file on disk (/tmp/nvi-filename.tmp) 301 | and then mv the new file over the top of the old 302 | 303 | i could get even more crafty and only store in-memory my changes and the location 304 | that they occurred at 305 | and just keep those in-memory until a save action is requested 306 | then do the above replay-and-merge flushing-chunks-to-tmp-disk strategy 307 | 308 | this way we can collaborate on huge text files, if we want, too 309 | but on average it will reduce the amount of data needing to be thrown around 310 | 311 | so what format would my in-memory buffer take? 312 | i could use the diff/patch format 313 | and record line-by-line changes 314 | except some really long lines could mess with us there 315 | 316 | 110,10,115:abcdefgh 317 | 318 | bah, i'm making it too complicated. i can be a bad-ass later. its getting late. 319 | 320 | so i'll just use buffers for now 321 | and always assume that the file will fit in memory 322 | and that transmitting a file-at-a-time from host to client is fast enough 323 | 324 | 325 | add funny -- COMBO X10! -- 326 | counter to the statusbar as each combo is executed within a 1 sec delay 327 | 328 | no matter where you are in vim, you should always see the current mode 329 | in a reliable place, because modes are so fundamental to vi operation 330 | my modes will be color coded like the vi powerline theme 331 | they will always appear in the view statusbar 332 | except for COMMAND mode which will appear in the statusbar 333 | this will make it very obvious what is going on 334 | 335 | additionally, my vim will begin the user in INSERT mode 336 | except remember i'm calling it NORMAL mode 337 | so that when they start out its most like what they might 338 | be famliar with: notepad, and they hvae to hit ESC to get to 339 | COMBO mode (what i call the vi NORMAL mode) 340 | 341 | make it so all @w and @h are the outer dims 342 | and any decorators like tabs, statusbars, etc. must fit inside that 343 | and relative to their parent dims who controls them 344 | 345 | instead of doing a layered TUI that redraws layers ontop of layers 346 | if i need to get more efficient 347 | i could make a pixel/char registry which is basically a matrix of function pointers 348 | the first cell points to a function that controls its appearance 349 | and subsequent cells point back to the id of the first cell of which they also belong 350 | then i just loop through the cells to determine which functions need to execute to redraw 351 | the screen 352 | 353 | its important to differentiate between the Terminal.cursor (there is only one; used to draw the screen) 354 | and the ViewCursor (there can be one or more per view; used by users to traverse a view) 355 | additionally between a ViewCursor that is the currently possessed (uses Terminal.cursor) 356 | and a ViewCursor that is not possessed (unfocused, or someone else's cursor; drawn manually) 357 | 358 | fix the command line disappearing on resize 359 | next time i implement bksp, del, etc. see if i can modularize the custom readline functionality i added to the command bar 360 | 361 | de-duplicate information presented in vim statusbars; 362 | treat Window.status_bar as global log notifications like "read 144B, 23C, 104L in 0.23sec" 363 | treat View.status_bar as stateful mode and cursor position information 364 | 365 | i'll improve on vim window management 366 | when the terminal resizes, instead of remembering exact widths and collapsing containers on the right 367 | i'll remember relative widths in percentages and try to retain the aspect ratio of each view 368 | when the terminal resizes 369 | 370 | display errors such as 'Not enough room' in the Window.status_bar 371 | support displaying errors in an obnoxious white-on-red color scheme 372 | 373 | ## TODO most immediately: 374 | 375 | * rendering multiple host and guest cursor movements 376 | * arrow keys cursor movement constrained by view text depending on mode 377 | 378 | * make it draw a dividing line 379 | * make the dividing lines draggable to hresize and vresize 380 | 381 | * make view statusbar toggle focus with click 382 | * also cursor focus toggle with click 383 | * and render both cursors in same view 384 | * hmm maybe also make it so view status bar only appears if there is more than one? 385 | 386 | * lclick to place cursor 387 | * lclick+drag to highlight 388 | * double-lclick to highlight word 389 | * triple-lclick to highlight line 390 | -------------------------------------------------------------------------------- /bin/nvi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../static/main'); 3 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "window_status_bar_bg" : 234, 3 | "window_status_bar_fg" : 255, 4 | "window_mode_fg" : 221, 5 | "view_text_bg" : 235, 6 | "view_text_fg" : 255, 7 | "view_gutter_bg" : 234, 8 | "view_gutter_fg" : 240, 9 | "view_status_bar_inactive_bg" : 233, 10 | "view_status_bar_inactive_fg" : 241, 11 | "view_status_bar_inactive_fg_bold" : 245, 12 | "view_status_bar_inactive_l1_bg" : 235, 13 | "view_status_bar_inactive_l1_fg" : 240, 14 | "view_status_bar_inactive_l1_fg_bold" : 238, 15 | "view_status_bar_inactive_l2_bg" : 235, 16 | "view_status_bar_inactive_l2_fg" : 240, 17 | "view_status_bar_inactive_l2_fg_bold" : 238, 18 | "view_status_bar_active_bg" : 236, 19 | "view_status_bar_active_fg" : 247, 20 | "view_status_bar_active_fg_bold" : 255, 21 | "view_status_bar_active_l1_bg" : 240, 22 | "view_status_bar_active_l1_fg" : 252, 23 | "view_status_bar_active_l1_fg_bold" : 255, 24 | "view_status_bar_active_l2_bg" : 252, 25 | "view_status_bar_active_l2_fg" : 244, 26 | "view_status_bar_active_l2_fg_bold" : 236, 27 | "view_block_mode_bg" : 208, 28 | "view_block_mode_fg" : 88, 29 | "view_normal_mode_bg" : 22, 30 | "view_normal_mode_fg" : 148, 31 | "view_replace_mode_bg" : 196, 32 | "view_replace_mode_fg" : 255, 33 | "user": { 34 | "id" : "mike", 35 | "name" : "Mike Smullin", 36 | "email" : "mike@smullindesign.com", 37 | "color" : 13 38 | }, 39 | "socket": "/tmp/nvi" 40 | } 41 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikesmullin/nvi/d2e0e361aa949475e0da97435cf5087b30ebf5e3/docs/screenshot.png -------------------------------------------------------------------------------- /loop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Process Loop 3 | # Author: Mike Smullin 4 | # License: MIT 5 | # Usage: 6 | # 7 | # ./loop node debug app.js 8 | # 9 | # Ctrl+C Restarts 10 | # Ctrl+\ Quits 11 | # 12 | 13 | ctrl_c() { 14 | echo -en "\n\n*** Restarting ***\n\n" 15 | } 16 | 17 | ctrl_backslash() { 18 | echo -en "\n\n*** Killing ***\n\n" 19 | exit 0 20 | } 21 | 22 | # trap keyboard interrupt (control-c) 23 | trap ctrl_c SIGINT 24 | trap ctrl_backslash SIGQUIT 25 | 26 | # the loop 27 | #while true; do $*; done 28 | while true; do 29 | reset; 30 | npm test && \ 31 | slnode debug -s static/main.js; 32 | cat # pause to read output 33 | done 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nvi", 3 | "version": "0.0.4", 4 | "description": "Very opinionated Node.JS VI clone", 5 | "main": "index.js", 6 | "bin": { 7 | "nvi": "bin/nvi" 8 | }, 9 | "dependencies": { 10 | "keypress": "~0.2.1", 11 | "sugar": "~1.3.9" 12 | }, 13 | "devDependencies": { 14 | "chai": "~1.7.2", 15 | "coffee-script": "~1.6.3" 16 | }, 17 | "scripts": { 18 | "pretest": "coffee --bare -o static/ precompile/", 19 | "test": "mocha --reporter spec --bail --ui bdd --colors --compilers coffee:coffee-script test/test.coffee" 20 | }, 21 | "repository": "git@github.com:mikesmullin/nvi.git", 22 | "author": "Mike Smullin ", 23 | "license": "GPLv3", 24 | "gitHead": "34f9faa540a745a2c95026b7e4c3a5a0e93c4b95" 25 | } 26 | -------------------------------------------------------------------------------- /precompile/controllers/application.coffee: -------------------------------------------------------------------------------- 1 | User = require '../models/User' 2 | global.Window = require '../views/Window' 3 | 4 | module.exports = class Application 5 | @init: (o) -> 6 | Logger.out 'init' 7 | Application.current_user = new User NviConfig.user 8 | # valid options: NORMAL, COMBO, REPLACE, BLOCK, LINE-BLOCK, COMMAND 9 | Application.mode = 'NORMAL' # always begin in this mode 10 | Application.command_line = '' 11 | Application.command_history = [] 12 | Application.command_history_position = 0 13 | Window.init 14 | file: o.args[0] 15 | process.stdout.on 'resize', Window.resize 16 | process.stdin.on 'keypress', Application.keypress 17 | process.stdin.on 'mousepress', Application.mousepress 18 | process.stdin.resume() # wait for stdin 19 | 20 | @keypress: (ch, key) -> 21 | Logger.out "caught keypress: "+ JSON.stringify arguments 22 | code = if ch then ch.charCodeAt 0 else -1 23 | 24 | if Application.mode is 'COMMAND' 25 | # TODO: get command mode working with resize and redraw() 26 | if code > 31 and code < 127 # valid command characters 27 | Application.command_line += ch 28 | Logger.out "type cmd len #{Application.command_line.length}" 29 | Terminal.echo(ch).flush() 30 | else if key.name is 'escape' 31 | Application.command_line = '' 32 | Application.command_history_position = 0 33 | Application.set_mode 'COMBO' 34 | else if key.name is 'backspace' 35 | Logger.out "Terminal.cursor.x #{Terminal.cursor.x}" 36 | if Terminal.cursor.x > 1 and Application.command_line.length > 0 37 | x = Terminal.cursor.x - 1 38 | cmd = Application.command_line.substr 0, x-2 39 | cmd += Application.command_line.substr x-1, Application.command_line.length-x+1 40 | Application.command_line = cmd 41 | Logger.out "bksp cmd len #{Application.command_line.length}, cmd #{Application.command_line}" 42 | Window.status_bar.set_text ':'+cmd 43 | Terminal.go(x, Terminal.screen.h).flush() 44 | else if key.name is 'delete' 45 | return # TODO: finish this WIP 46 | else if key.name is 'left' 47 | if Terminal.cursor.x > 2 48 | Terminal.move(-1).flush() 49 | else if key.name is 'right' 50 | if Terminal.cursor.x < Application.command_line.length + 2 51 | Terminal.move(1).flush() 52 | else if key.name is 'home' 53 | Terminal.go(2, Terminal.screen.h).flush() 54 | else if key.name is 'end' 55 | Terminal.go(Application.command_line.length+2, Terminal.screen.h).flush() 56 | else if key.name is 'up' 57 | 1 # retrieve history up matching beginning of current command 58 | else if key.name is 'down' 59 | 1 # retrieve history down matching beginning of current command 60 | # or matching highlighted history command plus any characters typed 61 | # or else whatever i was typing before this started (skip if tricky) 62 | else if key.name is 'return' 63 | Application.execute_cmd Application.command_line 64 | Application.command_line = '' 65 | Application.set_mode 'COMBO' 66 | 67 | if Application.mode is 'COMBO' 68 | switch ch 69 | when 'i' 70 | Application.set_mode 'NORMAL' 71 | return 72 | when ':' 73 | Application.mode = 'COMMAND' 74 | Window.status_bar.set_text ':', false 75 | return 76 | 77 | if ch is "\u0003" # Ctrl-c 78 | Window.status_bar.set_text 'Type :quit to exit Nvi' 79 | die '' # for convenience while debugging 80 | return 81 | 82 | if (Application.mode is 'NORMAL' or Application.mode is 'COMBO') and key 83 | switch key.name 84 | when 'escape' 85 | Application.set_mode 'COMBO' 86 | when 'left' 87 | Window.current_cursor().move -1 88 | when 'right' 89 | Window.current_cursor().move 1 90 | when 'up' 91 | Window.current_cursor().move 0, -1 92 | when 'down' 93 | Window.current_cursor().move 0, 1 94 | return 95 | 96 | @mousepress: (e) -> 97 | Logger.out "caught mousepress: "+ JSON.stringify e 98 | return 99 | 100 | @set_mode: (mode) -> 101 | Application.mode = mode 102 | Window.status_bar.set_text Terminal 103 | .xfg(NviConfig.window_mode_fg).fg('bold').echo("-- #{Application.mode} MODE --").fg('unbold') 104 | .xfg(NviConfig.window_status_bar_fg).get_clean() 105 | return 106 | 107 | @execute_cmd: (cmd) -> 108 | Logger.out "would execute command: #{Application.command_line}" 109 | Application.command_history.push Application.command_line 110 | args = cmd.split ' ' 111 | switch args[0] 112 | when 'x', 'wq' 113 | die '' 114 | when 'q', 'quit' 115 | die '' 116 | when 'vsplit', 'hsplit', 'split' 117 | return Window.active_tab.split args[0], args[1] 118 | when 'listen' 119 | ServerController = require './server' 120 | ServerController.init NviConfig.socket 121 | when 'connect' 122 | ClientController = require './client' 123 | ClientController.init NviConfig.socket 124 | -------------------------------------------------------------------------------- /precompile/controllers/client.coffee: -------------------------------------------------------------------------------- 1 | Socket = require '../models/Socket' 2 | 3 | module.exports = class ClientController 4 | @init: (port) -> 5 | Logger.filename = 'nvi-client.log' 6 | s = new Socket 7 | s.socket_open port, -> 8 | s.send 'handshake', (-> "%yo\u0000" ), -> 9 | s.expectOnce 'ack', (-> @recv is "%hey" ), -> 10 | Window.status_bar.set_text 'connected to host.' 11 | -------------------------------------------------------------------------------- /precompile/controllers/server.coffee: -------------------------------------------------------------------------------- 1 | Socket = require '../models/Socket' 2 | fs = require 'fs' 3 | 4 | module.exports = class ServerController 5 | @init: (port) -> 6 | fs.unlink port, -> # delete the file socket if it exists 7 | Logger.filename = 'nvi-server.log' 8 | s = new Socket 9 | s.expectOnce 'handshake', (-> @recv is "%yo" ), -> 10 | Window.status_bar.set_text 'client connected.' 11 | s.send 'ack', (-> "%hey\u0000" ), -> 12 | s.listen port 13 | -------------------------------------------------------------------------------- /precompile/main.coffee: -------------------------------------------------------------------------------- 1 | # TODO: move these all out of the global namespace 2 | # pass them via instantiation like teacup 3 | # and move utility functions into something like Underscore _ 4 | global.NviConfig = require '../config.json' 5 | global.Logger = require './models/Logger' 6 | global.Terminal = require './views/Terminal' 7 | global.delay = (s,f) -> setTimeout f, s 8 | global.interval = (s,f) -> setInterval f, s 9 | global.repeat = (n,s) -> o = ''; o += s for i in [0...n]; o 10 | global.rand = (m,x) -> Math.floor(Math.random() * (x-m)) + m 11 | sugar = require 'sugar' 12 | 13 | cleaned_up = false 14 | cleanup = -> 15 | return if cleaned_up 16 | process.stdin.pause() # stop waiting for input 17 | Terminal.fg('reset').clear().go(1,1).flush() 18 | cleaned_up = true 19 | process.on 'exit', cleanup 20 | global.die = (err) -> 21 | cleanup() 22 | if err 23 | process.stderr.write err+"\n" # output the error 24 | console.trace() # with a backtrace 25 | process.exit 1 # exit with non-zero error code 26 | process.stdout.write "see you soon!\n" 27 | process.exit 0 28 | # TODO: how does vim cleanup the scrollback buffer too? 29 | die 'must be in a tty' unless process.stdout.isTTY 30 | 31 | keypress = require 'keypress' 32 | process.stdin.setRawMode true # capture keypress 33 | keypress process.stdin # override keypress event support 34 | keypress.enableMouse process.stdout # override mouse support 35 | process.on 'exit', -> keypress.disableMouse process.stdout # return to normal for terminal 36 | process.stdin.setEncoding 'utf8' # modern times 37 | 38 | global.Application = require './controllers/application' 39 | [nil, nil, args...] = process.argv 40 | Application.init 41 | args: args 42 | -------------------------------------------------------------------------------- /precompile/models/HydraBuffer.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | # its a HydraBuffer; 5 | # because it gets read by multiple sometimes overlapping views 6 | # allowing them to share a single file 7 | # with the most minimal rewind/seeking/buffering 8 | 9 | # its what keeps an in-memory snapshot of all open 10 | # aggregate views for a given Buffer 11 | # and allows us to asynchronously update the memory snapshot 12 | # occasionally saving off the composite data to disk 13 | # when it makes sense to do so 14 | # (e.g. host presses Ctrl+S? or client presses it, and host approves) 15 | 16 | module.exports = class HydraBuffer 17 | # belongs to one or more views 18 | # one per stream 19 | @buffers: {} # registry ensures only one instance per stream 20 | constructor: (o) -> 21 | if o.file isnt undefined # we're expected to open a file on disk 22 | # absolute filename path on disk becomes buffer unique identifier 23 | # TODO: maybe fs.realpath() is useful here to resolve past symlinks? 24 | buffer = 25 | type: 'file' 26 | id: path.resolve o.file 27 | path: path.join path.dirname(o.file), path.sep 28 | base: path.basename o.file 29 | else 30 | buffer = 31 | type: 'memory' 32 | id: null 33 | path: '' 34 | base: '[No Name]' 35 | 36 | # decide whether or not this is a unique request 37 | if buffer.id is null or HydraBuffer.buffers[buffer.id] is undefined 38 | # instantiate new buffer only when needed 39 | buffer.views = [] 40 | HydraBuffer.buffers[buffer.id] = buffer 41 | 42 | buffer = HydraBuffer.buffers[buffer.id] 43 | buffer.views.push o.view # remember which views are using this buffer 44 | 45 | switch buffer.type 46 | when 'file' 47 | buffer.data = fs.readFileSync buffer.id, encoding: 'utf8' 48 | when 'memory' 49 | buffer.data = '' 50 | 51 | return buffer 52 | -------------------------------------------------------------------------------- /precompile/models/Logger.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = class Logger 5 | @filename: 'nvi.log' 6 | @out: -> 7 | o = {} 8 | switch arguments.length 9 | when 2 then [o, s] = arguments 10 | when 1 then [s] = arguments 11 | o.type ||= 'info' 12 | 13 | out = "#{Date.create().format '{MM}/{dd}/{yy} {HH}:{mm}:{ss}.{fff}'} "+ 14 | "#{if o.remote then "#{o.remote} " else ""}"+ 15 | "[#{o.type}] "+ 16 | "#{s}"+ 17 | "#{if o.type is 'out' then "" else "\n"}" 18 | 19 | fs.appendFileSync path.join(__dirname, '..', '..', Logger.filename), out 20 | return 21 | -------------------------------------------------------------------------------- /precompile/models/Socket.coffee: -------------------------------------------------------------------------------- 1 | net = require 'net' 2 | 3 | module.exports = class Socket 4 | constructor: -> 5 | @events = {} 6 | @socket = null 7 | @connected = false 8 | @buffer = '' 9 | #NviConfig.socket 10 | @state = {} 11 | @expectations = Once: [], Anytime: [] 12 | @on 'data', @receive 13 | 14 | # EventEmitter clone 15 | # with improvements 16 | on: (event, cb) -> 17 | @events[event] ||= [] 18 | @events[event].push cb 19 | once: (event, cb) -> 20 | wrapped = null 21 | @on event, wrapped = => 22 | for f, i in @events[event] when f is wrapped 23 | @events[event].splice i, 1 24 | break 25 | cb.apply null, arguments 26 | removeAllListeners: (event) -> 27 | delete @events[event] 28 | emit: (event, args...) -> 29 | if @events[event]? and @events[event].length 30 | cb.apply null, args for k, cb of @events[event] 31 | emitOne: (event, args...) -> 32 | if @events[event]? and @events[event].length 33 | @events[event][0].apply null, args 34 | emitLast: (event, args...) -> 35 | if @events[event]? and @events[event].length 36 | cb = @events[event][@events[event].length-1] 37 | @events[event] = [] # flush events before and including last 38 | cb.apply null, args 39 | emitOneIfAnyThen: (event, next) -> 40 | if @events[event]? and @events[event].length # if any event is queued 41 | @events[event][0].call null, next # call event, and event will call next 42 | else # otherwise 43 | next() # just call next 44 | 45 | # basic socket operations 46 | listen: (port, cb) -> 47 | # TODO: make this able to listen on the same port across threads 48 | server = net.createServer allowHalfOpen: false, (@socket) => 49 | @connected = true 50 | Logger.out remote: "#{@socket.remoteAddress}:#{@socket.remotePort}", 'client connected' 51 | @socket.on 'data', (d) => 52 | @socket_receive.apply @, arguments 53 | cb @socket if typeof cb is 'function' 54 | @emit 'connection' 55 | #server.setNoDelay false # disable Nagle algorithm; forcing an aggressive socket performance improvement 56 | server.on 'end', => 57 | @connected = false 58 | Logger.out type: 'fail', 'remote host sent FIN' 59 | server.on 'close', => 60 | @connected = false 61 | Logger.out type: 'fail', 'socket closed' 62 | @emit 'close' 63 | server.on 'error', (err) => 64 | Logger.out type: 'fail', "Socket error: "+ JSON.stringify err 65 | server.on 'timeout', => 66 | server.listen port, => 67 | Logger.out "listening on #{port}" 68 | @emit 'listening' 69 | 70 | socket_open: (port, cb) -> 71 | @host = '' #host if host 72 | @port = port if port 73 | Logger.out remote: "#{@host}:#{@port}", 'opening socket' 74 | @socket = new net.Socket allowHalfOpen: false 75 | @socket.setTimeout 10*1000 # wait 10sec before retrying connection 76 | @socket.setNoDelay false # disable Nagle algorithm; forcing an aggressive socket performance improvement 77 | @connected = false 78 | @socket.on 'data', (d) => 79 | @socket_receive.apply @, arguments 80 | @socket.on 'end', => 81 | @connected = false 82 | Logger.out type: 'fail', 'remote host sent FIN' 83 | @socket.on 'close', => 84 | @connected = false 85 | Logger.out type: 'fail', 'socket closed' 86 | @emit 'close' 87 | @socket.on 'error', (err) => 88 | Logger.out type: 'fail', "Socket error: "+ JSON.stringify err 89 | @socket.on 'timeout', => 90 | # this seems to get fired randomly; perhaps when packet send is delayed; unreliable 91 | @emit 'connecting' 92 | @socket.connect @port, => 93 | @connected = true 94 | Logger.out 'socket open' 95 | cb @socket if typeof cb is 'function' 96 | @emit 'connection' 97 | 98 | close: (err) -> 99 | @connected = false 100 | # TODO: make this hangup on the current client 101 | # but not drop other existing clients 102 | # and keep listening for new clients 103 | Logger.out type: 'fail', "[ERR] #{err}" if (err) 104 | Logger.out 'sent FIN to remote host' 105 | @socket.end() 106 | Logger.out 'destroying socket to ensure no more i/o happens' 107 | @socket.destroy() 108 | 109 | socket_send: (s, cb) -> 110 | Logger.out type: 'send', JSON.stringify s, null, 2 111 | @socket.write s, 'utf8', => 112 | cb() 113 | 114 | socket_receive: (buf) -> 115 | # remote can transmit messages split across several packets, 116 | # as well as more than one message per packet 117 | packet = buf.toString() 118 | Logger.out type: 'recv', JSON.stringify packet, null, 2 119 | @buffer += packet 120 | while (pos = @buffer.indexOf("\u0000")) isnt -1 # we have a complete message 121 | recv = @buffer.substr 0, pos 122 | @buffer = @buffer.substr pos+1 123 | switch recv[0] 124 | when '%' # Delimiter 125 | [cmd, data...] = recv.substr(1,recv.length-1).split('%') 126 | @emit 'data', 'd', cmd, data, recv 127 | when '{' # JSON 128 | data = JSON.parse recv 129 | @emit 'data', 'json', data.b?._cmd, data, recv 130 | return 131 | 132 | # convenient helpers 133 | send: (description, data_callback, cb) -> 134 | Logger.out "send: #{description}" 135 | @socket_send data_callback.apply(state: @state), cb 136 | 137 | hangup: (reason, cb) -> 138 | Logger.out type: 'fail', "Server hungup on client. #{reason}" 139 | @on 'close', cb 140 | @close() 141 | 142 | # expectation system 143 | _pushExpectation: -> 144 | e = {} 145 | switch arguments.length 146 | when 4 then [type, e.description, e.test_callback, e.callback] = arguments 147 | when 5 then [type, e.description, e.test_callback, e.within, e.callback] = arguments 148 | @expectations[type] ||= [] 149 | @expectations[type].push e 150 | return 151 | expectOnce: (description, test_callback, cb) -> @_pushExpectation.call @, 'Once', description, test_callback, cb 152 | expectOnceWithin: (description, test_callback, within, cb) -> @_pushExpectation.call @, 'Once', description, test_callback, within, cb 153 | expectAnytime: (description, test_callback, cb) -> @_pushExpectation.call @, 'Anytime', description, test_callback, cb 154 | _clearExpectation: (etype, i) -> 155 | @expectations[etype].splice i, 1 156 | 157 | receive: (type, cmd, data, recv) => 158 | for nil, etype of ['Once', 'Anytime'] 159 | for expectation, i in @expectations[etype] 160 | if expectation.test_callback.apply { state: @state, type: type, cmd: cmd, data: data, recv: recv } 161 | Logger.out "received #{type}: #{expectation.description}" 162 | Logger.out type: 'data', JSON.stringify { type: type, cmd: cmd, data: data } 163 | @_clearExpectation etype, i if etype is 'Once' 164 | if typeof expectation.callback is 'function' 165 | expectation.callback.apply { state: @state, data: data, recv: recv } 166 | return true 167 | Logger.out type: 'fail', 'received: unexpected response' 168 | Logger.out type: 'fail', JSON.stringify { type: type, cmd: cmd, data: data } 169 | Logger.out "expectOnce queue: "+ JSON.stringify @expectations.Once 170 | Logger.out "expectAnytime queue: "+ JSON.stringify @expectations.Anytime 171 | -------------------------------------------------------------------------------- /precompile/models/User.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class User 2 | # has one or more views 3 | constructor: (o) -> 4 | @id = o.id # unique string identifier and alias 5 | @name = o.name # full name 6 | @email = o.email # email 7 | @color = o.color # ansi xterm 256-color id 8 | #@views = [] 9 | return 10 | -------------------------------------------------------------------------------- /precompile/views/Bar.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class Bar 2 | constructor: (o) -> 3 | @bg = o.bg or die "Bar.bg must be specified!" 4 | @fg = o.fg or die "Bar.fg must be specified!" 5 | @text = o.text or '' 6 | @resize x: o.x, y: o.y, w: o.w, h: o.h 7 | return 8 | resize: (o) -> 9 | @x = o.x if o.x 10 | die "Bar.x may not be less than 1!" if @x < 1 11 | @y = o.y 12 | die "Bar.y may not be less than 1!" if @y < 1 13 | @w = o.w 14 | die "Bar.w may not be less than 1!" if @w < 1 15 | @h = o.h if o.h 16 | die "Bar.h may not be less than 1!" if @h < 1 17 | @draw() 18 | return 19 | draw: -> 20 | @set_text @text 21 | return 22 | set_text: (s, return_cursor=true) -> 23 | # TODO: provide left and right side of bar to set text in 24 | # TODO: left side of bar should be rendered last to overlap right 25 | Terminal 26 | .clear_space(x: @x, y: @y, w: @w, h: @h, bg: @bg, fg: @fg) 27 | .echo(s.substr(0, @w)).flush() 28 | @text = s 29 | Window.current_cursor()?.draw() if return_cursor # return cursor to last position 30 | return 31 | -------------------------------------------------------------------------------- /precompile/views/BufferView.coffee: -------------------------------------------------------------------------------- 1 | Bar = require './Bar' 2 | HydraBuffer = require '../models/HydraBuffer' 3 | BufferViewCursor = require './BufferViewCursor' 4 | 5 | module.exports = class BufferView 6 | # belongs to a Tab.Cell 7 | # belongs to a user 8 | # has one hydrabuffer 9 | # renders text from hydrabuffers 10 | # also renders cursors over the top of hydrabuffer text 11 | # redraws its section of real-estate on-screen on-demand 12 | # remember: treeview is also a view, with an option set for curline_bg 13 | constructor: (o) -> 14 | @tab = o.tab 15 | @tab.active_view = @ if o.active 16 | @buffer = HydraBuffer view: @, file: o.file # will instantiate itself if needed 17 | # that depends on whether we were given a filename 18 | # but let HydraBuffer track and decide on this internally 19 | @lines = @buffer.data.split("\n") 20 | @lines.pop() # discard last line erroneously appended by fs.read 21 | @lines = [''] unless @lines.length >= 1 # may never have less than one line 22 | @gutter = repeat (Math.max 4, @lines.length.toString().length + 2), ' ' 23 | @resize x: o.x, y: o.y, w: o.w, h: o.h 24 | @status_bar = new Bar x: @x, y: @y + @ih, w: @w, h: 1, bg: NviConfig.view_status_bar_active_bg, fg: NviConfig.view_status_bar_active_fg, text: Terminal 25 | .xbg(NviConfig.view_status_bar_active_l1_bg).xfg(NviConfig.view_status_bar_active_l1_fg) 26 | .echo(@buffer.path).fg('bold') 27 | .xfg(NviConfig.view_status_bar_active_l1_fg_bold).echo(@buffer.base+' ') 28 | .fg('unbold') 29 | .xbg(NviConfig.view_status_bar_active_bg).xfg(NviConfig.view_status_bar_active_fg) 30 | .get_clean() 31 | #Window.status_bar.set_text "\"#{@buffer.base}\", #{@lines.length}L, #{@buffer.data.length}C" 32 | 33 | # a view has one or more cursors 34 | # but only one is possessed at a given time 35 | # cursor 0 is always the current_user's cursor 36 | # only cursor 0 can become possessed by the current_user 37 | @cursors = [new BufferViewCursor user: Application.current_user, view: @, x: @x, y: @y, possessed: true] 38 | return 39 | destroy: -> 40 | @cell.destroy() 41 | return 42 | resize: (o) -> 43 | @x = o.x if o.x 44 | die "BufferView.x may not be less than 1!" if @x < 1 45 | @y = o.y if o.y 46 | die "BufferView.y may not be less than 1!" if @y < 1 47 | @w = o.w 48 | die "BufferView.w may not be less than 1!" if @w < 1 49 | # outer height 50 | @h = o.h; die "BufferView.h may not be less than 2!" if @h < 2 51 | # inner height (after decorators like status bar) 52 | @iw = o.w 53 | @ih = o.h - 1 # make room for status bar 54 | @draw() 55 | @status_bar.resize x: @x, y: @y + @ih, w: @w if @status_bar 56 | return 57 | draw: -> 58 | # count visible lines; truncate to view inner height when necessary 59 | visible_line_h = Math.min @lines.length, @ih 60 | # draw visible lines 61 | # TODO: fix last line shown always on bottom; overriding actual line 62 | for ln in [0...visible_line_h] 63 | line = @lines[ln] 64 | # if necessary, truncate line to visible width and append a truncation symbol (>) 65 | if line.length > @iw then line = line.substr(0, @iw-1)+'>' 66 | # draw line gutter 67 | # TODO: we shouldn't have to keep setting go(). we clear to eol so it should line up perfectly on next line 68 | # just set go once outside of for() and then fix the Terminal.echo() math 69 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(@x,@y+ln).echo((@gutter+(ln+1)).substr(-1*(@gutter.length-1))+' ') 70 | # echo line, erasing any remaining line space to end of visible width 71 | Terminal.xbg(NviConfig.view_text_bg).xfg(NviConfig.view_text_fg).echo(line).clear_n(@iw - @gutter.length - line.length).flush() 72 | # draw tilde placeholder lines, erasing any remaining line space to end of visible height 73 | if visible_line_h < @ih 74 | for y in [visible_line_h...@ih] 75 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(@x,@y+y).fg('bold').echo('~').fg('unbold').clear_n(@iw-1).flush() 76 | if @cursors 77 | cursor.draw() for cursor, i in @cursors when i isnt 0 78 | return 79 | -------------------------------------------------------------------------------- /precompile/views/BufferViewCursor.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class BufferViewCursor 2 | # belong to a user 3 | # belong to a view 4 | # cursors can also highlight and block edit 5 | constructor: (o) -> 6 | @user = o.user 7 | # TODO: shall the same cursor belong to multiple views? seems yes 8 | @view = o.view 9 | @possessed = o.possessed or false 10 | @resize x: o.x, y: o.y, w: o.w, h: o.h 11 | return 12 | resize: (o) -> 13 | @x = o.x 14 | die "BufferViewCursor.x may not be less than 1!" if @x < 1 15 | @y = o.x 16 | die "BufferViewCursor.y may not be less than 1!" if @y < 1 17 | @w = 1 18 | die "BufferViewCursor.w may not be less than 1!" if @w < 1 19 | @h = 1 20 | die "BufferViewCursor.h may not be less than 1!" if @h < 1 21 | @draw() 22 | return 23 | # users can traverse a view 24 | go: (@x, @y) -> # relative to view 25 | Logger.out "BufferView.cursor = x: #{@x}, y: #{@y}" 26 | Terminal.go(@view.x + @view.gutter.length + @x-1, @view.y + @y-1).flush() 27 | return 28 | move: (x, y=0) -> # relative to current position 29 | # TODO: limit cursor movement to within the typeable area, not the entire view 30 | dx = @x + x; dy = @y + y 31 | if dx >= 1 and dx <= @view.iw - @view.gutter.length and dy >= 1 and dy <= @view.ih 32 | @go dx, dy 33 | return 34 | draw: -> 35 | if @possessed 36 | @move 0, 0 # teleport the terminal cursor to this location 37 | else 38 | # draw a custom colored blinking cursor block ourselves 39 | return 40 | -------------------------------------------------------------------------------- /precompile/views/Cell.coffee: -------------------------------------------------------------------------------- 1 | BufferView = require './BufferView' 2 | 3 | # Cells provide a tiling window manager effect 4 | # for BufferViews in Tabs 5 | module.exports = class Cell 6 | # belongs to Tab or another Cell 7 | # has one Cell or BufferView 8 | # has zero or one prev Cell 9 | # has zero or one next Cell 10 | constructor: (o) -> 11 | # cells have a size represented in relative percentage units 12 | @p = o.p 13 | # cells maintain a chain link relationship with their neighbors 14 | @next = @prev = null 15 | # a cell may have zero or more children cells 16 | # but its only meaningful to track the first child 17 | # because either way you'd have to iterate to find a specific child 18 | @first_child = null 19 | # a cell may hold a first_child cell or a view, but not both 20 | @view = o.view or null 21 | # cells relate by dividing from an origin cell 22 | if o.origin 23 | # prepend or insert link in neighbor chain 24 | @next = o.origin 25 | @prev = o.origin.prev if o.origin.prev 26 | o.origin.prev = @ 27 | @chain = o.origin.chain 28 | else 29 | # cells have attributes shared by the chain 30 | @chain = 31 | # chains have direction, and it can change 32 | dir: o.dir or 'v' 33 | # chains may have zero or one parent cell 34 | # but its an important convenience since you would never want 35 | # to iterate siblings to find the common parent of an orphan cell 36 | parent: o.parent or null 37 | # chains have rectangular coordinates and dimensions 38 | # if you specify a parent, rect is inherited from the parent 39 | # otherwise, you must specify the chain rect 40 | x: if o.parent then o.parent.x else o.chain.x 41 | y: if o.parent then o.parent.y else o.chain.y 42 | w: if o.parent then o.parent.w else o.chain.w 43 | h: if o.parent then o.parent.h else o.chain.h 44 | # cells have rectangular coordinates and dimensions, too 45 | # but you are not allowed to specify them directly 46 | # as they are calculated during resize operations 47 | # based on the cell size percentage relative to its chain rect 48 | @x = @y = @w = @h = null 49 | @resize chain: x: @chain.x, y: @chain.y, w: @chain.w, h: @chain.h 50 | resize: (o) -> 51 | # TODO: ensure we redraw from top-to-bottom, left-to-right 52 | # otherwise the views draw over the top of each other? 53 | # TODO: make space for individual view status bars 54 | # TODO: make space for divider bars 55 | if o?.chain 56 | @chain.x = o.chain.x if o.chain.x 57 | die "Cell.chain.x may not be less than 1!" if @chain.x < 1 58 | @chain.y = o.chain.y if o.chain.y 59 | die "Cell.chain.y may not be less than 1!" if @chain.y < 1 60 | @chain.w = o.chain.w if o.chain.w 61 | die "Cell.chain.w may not be less than 1!" if @chain.w < 1 62 | @chain.h = o.chain.h if o.chain.h 63 | die "Cell.chain.h may not be less than 1!" if @chain.h < 1 64 | # recalculate every cell size in the chain 65 | # because no cell size stands on its own; 66 | # all cell sizes are relative to the chain 67 | # any cell size changes also affect siblings in the chain 68 | i = 0; pc = x: @chain.x, y: @chain.y, w: @chain.w, h: @chain.h; @each_neighbor (cell) -> 69 | switch cell.chain.dir # account for chain direction 70 | when 'v' # divider is vertical, so cells are horizontal like columns 71 | cell.x = pc.x + (i * pc.w) 72 | cell.y = pc.y # same 73 | cell.w = Math.floor cell.p * cell.chain.w # relative percentage of the total chain 74 | cell.h = cell.chain.h # same 75 | when 'h' # divider is horizontal, so cells are vertical like rows 76 | cell.x = pc.x # same 77 | cell.y = pc.y + (i * pc.h) 78 | cell.w = cell.chain.w # same 79 | cell.h = Math.floor cell.p * cell.chain.h # relative percentage of the total chain 80 | die "Cell.x may not be less than 1!" if cell.x < 1 81 | die "Cell.y may not be less than 1!" if cell.y < 1 82 | die "Cell.w may not be less than 1!" if cell.w < 1 83 | die "Cell.h may not be less than 1!" if cell.h < 1 84 | # invoke resize on affected children and/or views 85 | if affected_content = cell.view or cell.first_child 86 | affected_content.resize x: cell.x, y: cell.y, w: cell.w, h: cell.h 87 | # increment and repeat 88 | i++; pc = cell; return 89 | return 90 | draw: -> 91 | # TODO: draw the divider 92 | return 93 | 94 | # instantiates a new view with coords and dims 95 | # relative to its parent cell and row 96 | new_view: (o) -> 97 | o.x = @x; o.y = @y; o.w = @w; o.h = @h # override view coords and dims with cell's 98 | @view = new BufferView o # instantiate 99 | @view.cell = @ # bind view to cell for bottom-up iteration 100 | o.tab.views.push @view # bind to tab for top-level iteration 101 | return @view 102 | 103 | # iterate neighbor cell chain 104 | each_neighbor: (cb) -> 105 | cb c = @, 'origin' # begin with this cell 106 | cb c = c.prev, 'prev' while c.prev # explore to beginning of chain 107 | c = @ # reset to this cell 108 | cb c = c.next, 'next' while c.next # explore to end of chain 109 | return 110 | 111 | # divides a cell into two chained neighbors 112 | divide: (view) -> 113 | # count neighbors 114 | neighbors = []; @each_neighbor (neighbor, dir) -> 115 | neighbors[if dir is 'prev' then 'unshift' else 'push'] neighbor 116 | return 117 | # calculate their new average size supposing an additional neighbor 118 | p = 1 / (neighbors.length + 1) 119 | # resize all existing neighbors to be the same new average size; 120 | # making room for a new neighbor 121 | neighbor.p = p for neighbor in neighbors 122 | # instantiate the new neighbor 123 | new_neighbor = new Cell origin: @, p: p 124 | # recalculate dimensions of existing cells in chain 125 | # and invoke resize on any with views 126 | @resize() 127 | # instantiate and return the resulting new view 128 | # bound to its newly divided cell 129 | return new_neighbor.new_view view 130 | 131 | # create a parent-child relationship between two existing cells 132 | # not the same as hsplit or divide; inserts an additional cell child 133 | # with its own direction and completely new cell chain 134 | impregnate: (dir, view) -> 135 | die "must have more than one cell in the chain to impregnate" unless @prev or @next 136 | die "must not already be impregnated to impregnate" if @first_child 137 | # temporarily detach the view from this cell 138 | detached_view = @view 139 | @view = null 140 | # re-attach the view to a new cell 141 | # attach that new cell as a child of this cell 142 | # name this cell as the new cell's parent 143 | @first_child = new Cell p: 1, dir: dir, parent: @, view: detached_view 144 | # divide the new cell into a chain of two 145 | # with the additional cell containing an additional view as given 146 | return @first_child.divide view 147 | 148 | vsplit: (view) -> @divide view 149 | hsplit: (view) -> 150 | return if @prev or @next # more than one cell 151 | @impregnate 'h', view 152 | else # only one cell 153 | @chain.dir = 'h' 154 | @divide view 155 | 156 | destroy: (o) -> # called bottom-up from BufferView 157 | # refuse to destroy the last cell; a tab must always have one view 158 | return false if tab.views.length < 2 159 | # link neighbors 160 | @prev.next = @next if @prev 161 | @next.prev = @prev if @next 162 | # children should already have destroyed themselves 163 | # collapse empty parents recursively 164 | if @chain.parent.prev is null and @chain.parent.next is null 165 | @chain.parent.destroy() 166 | # always ensure the tab.top_cell reference is replaced 167 | # if its cell is destroyed 168 | @destroyed = true 169 | # TODO: it may be more convenient to track tab in the chain attributes 170 | # TODO: collapse these functions if they aren't used anywhere else 171 | closest_view = -> 172 | if @view 173 | return @view 174 | else if @first_child 175 | n = @; n = n.first_child while n.first_child 176 | return n.view 177 | return 178 | tab = closest_view().tab 179 | if tab.topmost_cell.destroyed 180 | other = @prev or @next 181 | topmost_cell = -> 182 | n = other; n = n.chain.parent while n.chain.parent 183 | return n 184 | tab.topmost_cell = topmost_cell() 185 | tab.resize() 186 | else 187 | # call resize on any surviving cells of affected chain 188 | survivor.resize() if survivor = @prev or @next 189 | return true # ride into the sunset of garbage collection 190 | -------------------------------------------------------------------------------- /precompile/views/Tab.coffee: -------------------------------------------------------------------------------- 1 | Cell = require './Cell' 2 | 3 | module.exports = class Tab 4 | # has one or more views 5 | constructor: (o) -> 6 | @name = o.name or 'untitled' 7 | Window.active_tab = @ if o.active 8 | @views = [] 9 | @topmost_cell = new Cell p: 1, chain: x: @x, y: @y, w: @w, h: @ih 10 | @resize x: o.x, y: o.y, w: o.w, h: o.h 11 | @topmost_cell.new_view tab: @, file: o.file, active: o.active 12 | destroy: -> 13 | resize: (o) -> 14 | @x = o.x if o.x 15 | die "Tab.x may not be less than 1!" if @x < 1 16 | @y = o.y if o.y 17 | die "Tab.y may not be less than 1!" if @y < 1 18 | @w = o.w 19 | die "Tab.w may not be less than 1!" if @w < 1 20 | # outer 21 | @h = o.h 22 | die "Tab.h may not be less than 1!" if @h < 1 # TODO: or 2 if tab bar is present 23 | # inner 24 | @ih = o.h # optionally without a tab bar 25 | @draw() 26 | @topmost_cell.resize chain: x: @x, y: @y, w: @w, h: @ih 27 | return 28 | draw: -> 29 | # if draw_tab_bar 30 | return 31 | 32 | activate_view: (view) -> 33 | v.active = v is view for v in @views 34 | return 35 | split: (cmd, file) -> 36 | # TODO: account for divider width during resize 37 | divider_w = 1 38 | # TODO: draw divider; 39 | # instead of blanking the whole screen and then not drawing views there 40 | # be prepared to simply draw a line for the divider horizontally or vertically where it goes 41 | 42 | # resize all view coordinates and dimensions to make room 43 | # and initialize new view in the resulting space 44 | if cmd is 'split' then cmd = 'hsplit' 45 | new_view = @active_view.cell[cmd] 46 | tab: Window.active_tab 47 | file: file 48 | 49 | # move focus to new view 50 | return @activate_view new_view 51 | -------------------------------------------------------------------------------- /precompile/views/Terminal.coffee: -------------------------------------------------------------------------------- 1 | module.exports = class Terminal 2 | # controls writing of all output to the terminal 3 | @write: (s) -> process.stdout.write s; @ 4 | # offers a system of chaining that lets you 5 | # build a string in memory, by either: 6 | @buffer: '' 7 | # pushing ansi escape codes, or; 8 | @push_raw: (s) -> Terminal.buffer += s; @ 9 | # pushing human-readable text 10 | @echo: (s) -> 11 | if s.length 12 | # automatically calculate new cursor position 13 | # by emulating terminal wrapping 14 | Terminal.cursor.x += s.length 15 | if Terminal.cursor.x > Terminal.screen.w 16 | Terminal.cursor.y += Math.floor(Terminal.cursor.x / Terminal.screen.w) 17 | Terminal.cursor.x = Terminal.cursor.x % Terminal.screen.w 18 | # also increment y once for every line feed 19 | s.replace /\n/g, -> Terminal.cursor.y++ # only counts; doesn't actually replace 20 | Terminal.push_raw s 21 | return @ 22 | # until you are ready to, either: 23 | # flush to the terminal, or; 24 | @flush: -> 25 | Terminal.write Terminal.buffer 26 | Terminal.buffer = '' 27 | @ 28 | # return a string 29 | @get_clean: -> 30 | b = Terminal.buffer 31 | Terminal.buffer = '' 32 | return b 33 | 34 | @ansi_esc: class # ansi escape sequences 35 | @cursor_pos : (x, y) -> "\x1b[#{y};#{x}H" 36 | @clear_screen : '\x1b[2J' 37 | @clear_eol : '\x1b[K' # TODO: why doesn't this work in tmux? its faster/smoother 38 | @clear_eof : '\x1b[J' 39 | @color: class 40 | # modifiers 41 | @reset : '\x1b[0m' 42 | @bold : '\x1b[1m' 43 | @inverse : '\x1b[7m' 44 | @strike : '\x1b[9m' 45 | @unbold : '\x1b[22m' 46 | # foreground 47 | @black : '\x1b[30m' 48 | @red : '\x1b[31m' 49 | @green : '\x1b[32m' 50 | @yellow : '\x1b[33m' 51 | @blue : '\x1b[34m' 52 | @magenta : '\x1b[35m' 53 | @cyan : '\x1b[36m' 54 | @white : '\x1b[37m' 55 | @xterm : (i) -> "\x1b[38;5;#{i}m" 56 | # background 57 | @bg_reset : '\x1b[49m' 58 | @bg_black : '\x1b[40m' 59 | @bg_red : '\x1b[41m' 60 | @bg_green : '\x1b[42m' 61 | @bg_yellow : '\x1b[43m' 62 | @bg_blue : '\x1b[44m' 63 | @bg_magenta : '\x1b[45m' 64 | @bg_cyan : '\x1b[46m' 65 | @bg_white : '\x1b[47m' 66 | @bg_xterm : (i) -> "\x1b[48;5;#{i}m" 67 | 68 | @clear: -> Terminal.push_raw Terminal.ansi_esc.clear_screen 69 | @cursor: x: null, y: null 70 | @screen: w: null, h: null 71 | @go: (x,y) -> # absolute 72 | die "Terminal.cursor.x #{x} may not be less than 1!" if x < 1 73 | die "Terminal.cursor.x #{x} may not be greater than Terminal.screen.w or #{Terminal.screen.w}!" if x > Terminal.screen.w 74 | Terminal.cursor.x = x 75 | die "Terminal.cursor.y #{y} may not be less than 1!" if y < 1 76 | die "Terminal.cursor.y #{y} may not be greater than Terminal.screen.h or #{Terminal.screen.h}!" if y > Terminal.screen.h 77 | Terminal.cursor.y = y 78 | Terminal.push_raw Terminal.ansi_esc.cursor_pos Terminal.cursor.x, Terminal.cursor.y 79 | #Logger.out "Terminal.cursor = x: #{Terminal.cursor.x}, y: #{Terminal.cursor.y}" 80 | return @ 81 | @move: (dx, dy=0) -> # relative to current position 82 | x = Terminal.cursor.x + dx 83 | y = Terminal.cursor.y + dy 84 | if 0 <= x <= Terminal.screen.w and 0 <= y <= Terminal.screen.h 85 | @go x, y 86 | return @ 87 | @fg: (color) -> Terminal.push_raw Terminal.ansi_esc.color[color] 88 | @bg: (color) -> Terminal.push_raw Terminal.ansi_esc.color['bg_'+color] 89 | @xfg: (i) -> Terminal.push_raw Terminal.ansi_esc.color.xterm i 90 | @xbg: (i) -> Terminal.push_raw Terminal.ansi_esc.color.bg_xterm i 91 | 92 | # since some Terminal emulators (like tmux) don't implement 93 | # things like "erase to end of line" 94 | # we have to output a bunch of spaces, instead 95 | # TODO: find out how vim is working normally/smoothly in tmux 96 | @clear_screen = -> 97 | Terminal.clear() 98 | for y in [1..Terminal.screen.h] 99 | Terminal.go 1, y 100 | Terminal.clear_eol() 101 | return @ 102 | @clear_n = (n) -> Terminal.echo repeat n, ' ' 103 | @clear_eol = -> Terminal.clear_n Terminal.screen.w - Terminal.cursor.x + 1 104 | @clear_space = (o) -> 105 | # blank out a rectangle with given bg color 106 | for y in [o.y..o.y+o.h-1] 107 | Terminal.xbg(o.bg).go(o.x, y).clear_n(o.w) 108 | # set cursor to relative 1,1 of freshly blanked space 109 | # with given fg color preset 110 | Terminal.go(o.x, o.y).xbg(o.bg).xfg(o.fg).flush() 111 | return @ 112 | -------------------------------------------------------------------------------- /precompile/views/Window.coffee: -------------------------------------------------------------------------------- 1 | Bar = require './Bar' 2 | Tab = require './Tab' 3 | 4 | module.exports = class Window 5 | # has one or more tabs 6 | @init: (o) -> 7 | Window.x = 1 8 | Window.y = 1 9 | Window.resize() 10 | Window.status_bar = new Bar x: Window.x, y: Window.h, w: Window.w, h: 1, bg: NviConfig.window_status_bar_bg, fg: NviConfig.window_status_bar_fg 11 | Window.tabs = [new Tab file: o?.file, x: Window.x, y: Window.y, w: Window.w, h: Window.ih, active: true] # can never have fewer than one tab 12 | return 13 | @resize: -> 14 | Logger.out "window caught resize #{process.stdout.columns}, #{process.stdout.rows}" 15 | Terminal.screen.w = process.stdout.columns 16 | Terminal.screen.h = process.stdout.rows 17 | # the terminal can be as small as it wants 18 | # but we can't draw a Window in anything smaller than this 19 | return if Terminal.screen.w < 1 or Terminal.screen.h < 3 20 | # outer dimensions 21 | Window.w = Terminal.screen.w 22 | Window.h = Terminal.screen.h 23 | # inner dimensions 24 | Window.ih = Window.h - 1 # make space for status bar 25 | Window.iw = Window.w 26 | Window.draw() 27 | if Window.status_bar 28 | Window.status_bar.resize y: Window.h, w: Window.w 29 | if Window.tabs 30 | tab.resize w: Window.w, h: Window.ih for tab in Window.tabs 31 | return 32 | @draw: -> 33 | #Terminal.xbg(NviConfig.view_gutter_bg).clear_screen().flush() # don't need to do this 34 | return 35 | @current_cursor: -> 36 | Window.active_tab?.active_view?.cursors?[0] 37 | -------------------------------------------------------------------------------- /static/controllers/application.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Application, User; 3 | 4 | User = require('../models/User'); 5 | 6 | global.Window = require('../views/Window'); 7 | 8 | module.exports = Application = (function() { 9 | function Application() {} 10 | 11 | Application.init = function(o) { 12 | Logger.out('init'); 13 | Application.current_user = new User(NviConfig.user); 14 | Application.mode = 'NORMAL'; 15 | Application.command_line = ''; 16 | Application.command_history = []; 17 | Application.command_history_position = 0; 18 | Window.init({ 19 | file: o.args[0] 20 | }); 21 | process.stdout.on('resize', Window.resize); 22 | process.stdin.on('keypress', Application.keypress); 23 | process.stdin.on('mousepress', Application.mousepress); 24 | return process.stdin.resume(); 25 | }; 26 | 27 | Application.keypress = function(ch, key) { 28 | var cmd, code, x; 29 | Logger.out("caught keypress: " + JSON.stringify(arguments)); 30 | code = ch ? ch.charCodeAt(0) : -1; 31 | if (Application.mode === 'COMMAND') { 32 | if (code > 31 && code < 127) { 33 | Application.command_line += ch; 34 | Logger.out("type cmd len " + Application.command_line.length); 35 | Terminal.echo(ch).flush(); 36 | } else if (key.name === 'escape') { 37 | Application.command_line = ''; 38 | Application.command_history_position = 0; 39 | Application.set_mode('COMBO'); 40 | } else if (key.name === 'backspace') { 41 | Logger.out("Terminal.cursor.x " + Terminal.cursor.x); 42 | if (Terminal.cursor.x > 1 && Application.command_line.length > 0) { 43 | x = Terminal.cursor.x - 1; 44 | cmd = Application.command_line.substr(0, x - 2); 45 | cmd += Application.command_line.substr(x - 1, Application.command_line.length - x + 1); 46 | Application.command_line = cmd; 47 | Logger.out("bksp cmd len " + Application.command_line.length + ", cmd " + Application.command_line); 48 | Window.status_bar.set_text(':' + cmd); 49 | Terminal.go(x, Terminal.screen.h).flush(); 50 | } 51 | } else if (key.name === 'delete') { 52 | return; 53 | } else if (key.name === 'left') { 54 | if (Terminal.cursor.x > 2) { 55 | Terminal.move(-1).flush(); 56 | } 57 | } else if (key.name === 'right') { 58 | if (Terminal.cursor.x < Application.command_line.length + 2) { 59 | Terminal.move(1).flush(); 60 | } 61 | } else if (key.name === 'home') { 62 | Terminal.go(2, Terminal.screen.h).flush(); 63 | } else if (key.name === 'end') { 64 | Terminal.go(Application.command_line.length + 2, Terminal.screen.h).flush(); 65 | } else if (key.name === 'up') { 66 | 1; 67 | } else if (key.name === 'down') { 68 | 1; 69 | } else if (key.name === 'return') { 70 | Application.execute_cmd(Application.command_line); 71 | Application.command_line = ''; 72 | Application.set_mode('COMBO'); 73 | } 74 | } 75 | if (Application.mode === 'COMBO') { 76 | switch (ch) { 77 | case 'i': 78 | Application.set_mode('NORMAL'); 79 | return; 80 | case ':': 81 | Application.mode = 'COMMAND'; 82 | Window.status_bar.set_text(':', false); 83 | return; 84 | } 85 | } 86 | if (ch === "\u0003") { 87 | Window.status_bar.set_text('Type :quit to exit Nvi'); 88 | die(''); 89 | return; 90 | } 91 | if ((Application.mode === 'NORMAL' || Application.mode === 'COMBO') && key) { 92 | switch (key.name) { 93 | case 'escape': 94 | Application.set_mode('COMBO'); 95 | break; 96 | case 'left': 97 | Window.current_cursor().move(-1); 98 | break; 99 | case 'right': 100 | Window.current_cursor().move(1); 101 | break; 102 | case 'up': 103 | Window.current_cursor().move(0, -1); 104 | break; 105 | case 'down': 106 | Window.current_cursor().move(0, 1); 107 | } 108 | } 109 | }; 110 | 111 | Application.mousepress = function(e) { 112 | Logger.out("caught mousepress: " + JSON.stringify(e)); 113 | }; 114 | 115 | Application.set_mode = function(mode) { 116 | Application.mode = mode; 117 | Window.status_bar.set_text(Terminal.xfg(NviConfig.window_mode_fg).fg('bold').echo("-- " + Application.mode + " MODE --").fg('unbold').xfg(NviConfig.window_status_bar_fg).get_clean()); 118 | }; 119 | 120 | Application.execute_cmd = function(cmd) { 121 | var ClientController, ServerController, args; 122 | Logger.out("would execute command: " + Application.command_line); 123 | Application.command_history.push(Application.command_line); 124 | args = cmd.split(' '); 125 | switch (args[0]) { 126 | case 'x': 127 | case 'wq': 128 | return die(''); 129 | case 'q': 130 | case 'quit': 131 | return die(''); 132 | case 'vsplit': 133 | case 'hsplit': 134 | case 'split': 135 | return Window.active_tab.split(args[0], args[1]); 136 | case 'listen': 137 | ServerController = require('./server'); 138 | return ServerController.init(NviConfig.socket); 139 | case 'connect': 140 | ClientController = require('./client'); 141 | return ClientController.init(NviConfig.socket); 142 | } 143 | }; 144 | 145 | return Application; 146 | 147 | })(); 148 | -------------------------------------------------------------------------------- /static/controllers/client.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var ClientController, Socket; 3 | 4 | Socket = require('../models/Socket'); 5 | 6 | module.exports = ClientController = (function() { 7 | function ClientController() {} 8 | 9 | ClientController.init = function(port) { 10 | var s; 11 | Logger.filename = 'nvi-client.log'; 12 | s = new Socket; 13 | return s.socket_open(port, function() { 14 | return s.send('handshake', (function() { 15 | return "%yo\u0000"; 16 | }), function() { 17 | return s.expectOnce('ack', (function() { 18 | return this.recv === "%hey"; 19 | }), function() { 20 | return Window.status_bar.set_text('connected to host.'); 21 | }); 22 | }); 23 | }); 24 | }; 25 | 26 | return ClientController; 27 | 28 | })(); 29 | -------------------------------------------------------------------------------- /static/controllers/server.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var ServerController, Socket, fs; 3 | 4 | Socket = require('../models/Socket'); 5 | 6 | fs = require('fs'); 7 | 8 | module.exports = ServerController = (function() { 9 | function ServerController() {} 10 | 11 | ServerController.init = function(port) { 12 | return fs.unlink(port, function() { 13 | var s; 14 | Logger.filename = 'nvi-server.log'; 15 | s = new Socket; 16 | s.expectOnce('handshake', (function() { 17 | return this.recv === "%yo"; 18 | }), function() { 19 | Window.status_bar.set_text('client connected.'); 20 | return s.send('ack', (function() { 21 | return "%hey\u0000"; 22 | }), function() {}); 23 | }); 24 | return s.listen(port); 25 | }); 26 | }; 27 | 28 | return ServerController; 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var args, cleaned_up, cleanup, keypress, nil, sugar, _ref, 3 | __slice = [].slice; 4 | 5 | global.NviConfig = require('../config.json'); 6 | 7 | global.Logger = require('./models/Logger'); 8 | 9 | global.Terminal = require('./views/Terminal'); 10 | 11 | global.delay = function(s, f) { 12 | return setTimeout(f, s); 13 | }; 14 | 15 | global.interval = function(s, f) { 16 | return setInterval(f, s); 17 | }; 18 | 19 | global.repeat = function(n, s) { 20 | var i, o, _i; 21 | o = ''; 22 | for (i = _i = 0; 0 <= n ? _i < n : _i > n; i = 0 <= n ? ++_i : --_i) { 23 | o += s; 24 | } 25 | return o; 26 | }; 27 | 28 | global.rand = function(m, x) { 29 | return Math.floor(Math.random() * (x - m)) + m; 30 | }; 31 | 32 | sugar = require('sugar'); 33 | 34 | cleaned_up = false; 35 | 36 | cleanup = function() { 37 | if (cleaned_up) { 38 | return; 39 | } 40 | process.stdin.pause(); 41 | Terminal.fg('reset').clear().go(1, 1).flush(); 42 | return cleaned_up = true; 43 | }; 44 | 45 | process.on('exit', cleanup); 46 | 47 | global.die = function(err) { 48 | cleanup(); 49 | if (err) { 50 | process.stderr.write(err + "\n"); 51 | console.trace(); 52 | process.exit(1); 53 | } 54 | process.stdout.write("see you soon!\n"); 55 | return process.exit(0); 56 | }; 57 | 58 | if (!process.stdout.isTTY) { 59 | die('must be in a tty'); 60 | } 61 | 62 | keypress = require('keypress'); 63 | 64 | process.stdin.setRawMode(true); 65 | 66 | keypress(process.stdin); 67 | 68 | keypress.enableMouse(process.stdout); 69 | 70 | process.on('exit', function() { 71 | return keypress.disableMouse(process.stdout); 72 | }); 73 | 74 | process.stdin.setEncoding('utf8'); 75 | 76 | global.Application = require('./controllers/application'); 77 | 78 | _ref = process.argv, nil = _ref[0], nil = _ref[1], args = 3 <= _ref.length ? __slice.call(_ref, 2) : []; 79 | 80 | Application.init({ 81 | args: args 82 | }); 83 | -------------------------------------------------------------------------------- /static/models/HydraBuffer.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var HydraBuffer, fs, path; 3 | 4 | fs = require('fs'); 5 | 6 | path = require('path'); 7 | 8 | module.exports = HydraBuffer = (function() { 9 | HydraBuffer.buffers = {}; 10 | 11 | function HydraBuffer(o) { 12 | var buffer; 13 | if (o.file !== void 0) { 14 | buffer = { 15 | type: 'file', 16 | id: path.resolve(o.file), 17 | path: path.join(path.dirname(o.file), path.sep), 18 | base: path.basename(o.file) 19 | }; 20 | } else { 21 | buffer = { 22 | type: 'memory', 23 | id: null, 24 | path: '', 25 | base: '[No Name]' 26 | }; 27 | } 28 | if (buffer.id === null || HydraBuffer.buffers[buffer.id] === void 0) { 29 | buffer.views = []; 30 | HydraBuffer.buffers[buffer.id] = buffer; 31 | } 32 | buffer = HydraBuffer.buffers[buffer.id]; 33 | buffer.views.push(o.view); 34 | switch (buffer.type) { 35 | case 'file': 36 | buffer.data = fs.readFileSync(buffer.id, { 37 | encoding: 'utf8' 38 | }); 39 | break; 40 | case 'memory': 41 | buffer.data = ''; 42 | } 43 | return buffer; 44 | } 45 | 46 | return HydraBuffer; 47 | 48 | })(); 49 | -------------------------------------------------------------------------------- /static/models/Logger.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Logger, fs, path; 3 | 4 | fs = require('fs'); 5 | 6 | path = require('path'); 7 | 8 | module.exports = Logger = (function() { 9 | function Logger() {} 10 | 11 | Logger.filename = 'nvi.log'; 12 | 13 | Logger.out = function() { 14 | var o, out, s; 15 | o = {}; 16 | switch (arguments.length) { 17 | case 2: 18 | o = arguments[0], s = arguments[1]; 19 | break; 20 | case 1: 21 | s = arguments[0]; 22 | } 23 | o.type || (o.type = 'info'); 24 | out = ("" + (Date.create().format('{MM}/{dd}/{yy} {HH}:{mm}:{ss}.{fff}')) + " ") + ("" + (o.remote ? "" + o.remote + " " : "")) + ("[" + o.type + "] ") + ("" + s) + ("" + (o.type === 'out' ? "" : "\n")); 25 | fs.appendFileSync(path.join(__dirname, '..', '..', Logger.filename), out); 26 | }; 27 | 28 | return Logger; 29 | 30 | })(); 31 | -------------------------------------------------------------------------------- /static/models/Socket.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Socket, net, 3 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 4 | __slice = [].slice; 5 | 6 | net = require('net'); 7 | 8 | module.exports = Socket = (function() { 9 | function Socket() { 10 | this.receive = __bind(this.receive, this); 11 | this.events = {}; 12 | this.socket = null; 13 | this.connected = false; 14 | this.buffer = ''; 15 | this.state = {}; 16 | this.expectations = { 17 | Once: [], 18 | Anytime: [] 19 | }; 20 | this.on('data', this.receive); 21 | } 22 | 23 | Socket.prototype.on = function(event, cb) { 24 | var _base; 25 | (_base = this.events)[event] || (_base[event] = []); 26 | return this.events[event].push(cb); 27 | }; 28 | 29 | Socket.prototype.once = function(event, cb) { 30 | var wrapped, 31 | _this = this; 32 | wrapped = null; 33 | return this.on(event, wrapped = function() { 34 | var f, i, _i, _len, _ref; 35 | _ref = _this.events[event]; 36 | for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) { 37 | f = _ref[i]; 38 | if (!(f === wrapped)) { 39 | continue; 40 | } 41 | _this.events[event].splice(i, 1); 42 | break; 43 | } 44 | return cb.apply(null, arguments); 45 | }); 46 | }; 47 | 48 | Socket.prototype.removeAllListeners = function(event) { 49 | return delete this.events[event]; 50 | }; 51 | 52 | Socket.prototype.emit = function() { 53 | var args, cb, event, k, _ref, _results; 54 | event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 55 | if ((this.events[event] != null) && this.events[event].length) { 56 | _ref = this.events[event]; 57 | _results = []; 58 | for (k in _ref) { 59 | cb = _ref[k]; 60 | _results.push(cb.apply(null, args)); 61 | } 62 | return _results; 63 | } 64 | }; 65 | 66 | Socket.prototype.emitOne = function() { 67 | var args, event; 68 | event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 69 | if ((this.events[event] != null) && this.events[event].length) { 70 | return this.events[event][0].apply(null, args); 71 | } 72 | }; 73 | 74 | Socket.prototype.emitLast = function() { 75 | var args, cb, event; 76 | event = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 77 | if ((this.events[event] != null) && this.events[event].length) { 78 | cb = this.events[event][this.events[event].length - 1]; 79 | this.events[event] = []; 80 | return cb.apply(null, args); 81 | } 82 | }; 83 | 84 | Socket.prototype.emitOneIfAnyThen = function(event, next) { 85 | if ((this.events[event] != null) && this.events[event].length) { 86 | return this.events[event][0].call(null, next); 87 | } else { 88 | return next(); 89 | } 90 | }; 91 | 92 | Socket.prototype.listen = function(port, cb) { 93 | var server, 94 | _this = this; 95 | server = net.createServer({ 96 | allowHalfOpen: false 97 | }, function(socket) { 98 | _this.socket = socket; 99 | _this.connected = true; 100 | Logger.out({ 101 | remote: "" + _this.socket.remoteAddress + ":" + _this.socket.remotePort 102 | }, 'client connected'); 103 | _this.socket.on('data', function(d) { 104 | return _this.socket_receive.apply(_this, arguments); 105 | }); 106 | if (typeof cb === 'function') { 107 | cb(_this.socket); 108 | } 109 | return _this.emit('connection'); 110 | }); 111 | server.on('end', function() { 112 | _this.connected = false; 113 | return Logger.out({ 114 | type: 'fail' 115 | }, 'remote host sent FIN'); 116 | }); 117 | server.on('close', function() { 118 | _this.connected = false; 119 | Logger.out({ 120 | type: 'fail' 121 | }, 'socket closed'); 122 | return _this.emit('close'); 123 | }); 124 | server.on('error', function(err) { 125 | return Logger.out({ 126 | type: 'fail' 127 | }, "Socket error: " + JSON.stringify(err)); 128 | }); 129 | server.on('timeout', function() {}); 130 | return server.listen(port, function() { 131 | Logger.out("listening on " + port); 132 | return _this.emit('listening'); 133 | }); 134 | }; 135 | 136 | Socket.prototype.socket_open = function(port, cb) { 137 | var _this = this; 138 | this.host = ''; 139 | if (port) { 140 | this.port = port; 141 | } 142 | Logger.out({ 143 | remote: "" + this.host + ":" + this.port 144 | }, 'opening socket'); 145 | this.socket = new net.Socket({ 146 | allowHalfOpen: false 147 | }); 148 | this.socket.setTimeout(10 * 1000); 149 | this.socket.setNoDelay(false); 150 | this.connected = false; 151 | this.socket.on('data', function(d) { 152 | return _this.socket_receive.apply(_this, arguments); 153 | }); 154 | this.socket.on('end', function() { 155 | _this.connected = false; 156 | return Logger.out({ 157 | type: 'fail' 158 | }, 'remote host sent FIN'); 159 | }); 160 | this.socket.on('close', function() { 161 | _this.connected = false; 162 | Logger.out({ 163 | type: 'fail' 164 | }, 'socket closed'); 165 | return _this.emit('close'); 166 | }); 167 | this.socket.on('error', function(err) { 168 | return Logger.out({ 169 | type: 'fail' 170 | }, "Socket error: " + JSON.stringify(err)); 171 | }); 172 | this.socket.on('timeout', function() {}); 173 | this.emit('connecting'); 174 | return this.socket.connect(this.port, function() { 175 | _this.connected = false; 176 | Logger.out('socket open'); 177 | if (typeof cb === 'function') { 178 | cb(_this.socket); 179 | } 180 | return _this.emit('connection'); 181 | }); 182 | }; 183 | 184 | Socket.prototype.close = function(err) { 185 | this.connected = false; 186 | if (err) { 187 | Logger.out({ 188 | type: 'fail' 189 | }, "[ERR] " + err); 190 | } 191 | Logger.out('sent FIN to remote host'); 192 | this.socket.end(); 193 | Logger.out('destroying socket to ensure no more i/o happens'); 194 | return this.socket.destroy(); 195 | }; 196 | 197 | Socket.prototype.socket_send = function(s, cb) { 198 | var _this = this; 199 | Logger.out({ 200 | type: 'send' 201 | }, JSON.stringify(s, null, 2)); 202 | return this.socket.write(s, 'utf8', function() { 203 | return cb(); 204 | }); 205 | }; 206 | 207 | Socket.prototype.socket_receive = function(buf) { 208 | var cmd, data, packet, pos, recv, _ref, _ref1; 209 | packet = buf.toString(); 210 | Logger.out({ 211 | type: 'recv' 212 | }, JSON.stringify(packet, null, 2)); 213 | this.buffer += packet; 214 | while ((pos = this.buffer.indexOf("\u0000")) !== -1) { 215 | recv = this.buffer.substr(0, pos); 216 | this.buffer = this.buffer.substr(pos + 1); 217 | switch (recv[0]) { 218 | case '%': 219 | _ref = recv.substr(1, recv.length - 1).split('%'), cmd = _ref[0], data = 2 <= _ref.length ? __slice.call(_ref, 1) : []; 220 | this.emit('data', 'd', cmd, data, recv); 221 | break; 222 | case '{': 223 | data = JSON.parse(recv); 224 | this.emit('data', 'json', (_ref1 = data.b) != null ? _ref1._cmd : void 0, data, recv); 225 | } 226 | } 227 | }; 228 | 229 | Socket.prototype.send = function(description, data_callback, cb) { 230 | Logger.out("send: " + description); 231 | return this.socket_send(data_callback.apply({ 232 | state: this.state 233 | }), cb); 234 | }; 235 | 236 | Socket.prototype.hangup = function(reason, cb) { 237 | Logger.out({ 238 | type: 'fail' 239 | }, "Server hungup on client. " + reason); 240 | this.on('close', cb); 241 | return this.close(); 242 | }; 243 | 244 | Socket.prototype._pushExpectation = function() { 245 | var e, type, _base; 246 | e = {}; 247 | switch (arguments.length) { 248 | case 4: 249 | type = arguments[0], e.description = arguments[1], e.test_callback = arguments[2], e.callback = arguments[3]; 250 | break; 251 | case 5: 252 | type = arguments[0], e.description = arguments[1], e.test_callback = arguments[2], e.within = arguments[3], e.callback = arguments[4]; 253 | } 254 | (_base = this.expectations)[type] || (_base[type] = []); 255 | this.expectations[type].push(e); 256 | }; 257 | 258 | Socket.prototype.expectOnce = function(description, test_callback, cb) { 259 | return this._pushExpectation.call(this, 'Once', description, test_callback, cb); 260 | }; 261 | 262 | Socket.prototype.expectOnceWithin = function(description, test_callback, within, cb) { 263 | return this._pushExpectation.call(this, 'Once', description, test_callback, within, cb); 264 | }; 265 | 266 | Socket.prototype.expectAnytime = function(description, test_callback, cb) { 267 | return this._pushExpectation.call(this, 'Anytime', description, test_callback, cb); 268 | }; 269 | 270 | Socket.prototype._clearExpectation = function(etype, i) { 271 | return this.expectations[etype].splice(i, 1); 272 | }; 273 | 274 | Socket.prototype.receive = function(type, cmd, data, recv) { 275 | var etype, expectation, i, nil, _i, _len, _ref, _ref1; 276 | _ref = ['Once', 'Anytime']; 277 | for (nil in _ref) { 278 | etype = _ref[nil]; 279 | _ref1 = this.expectations[etype]; 280 | for (i = _i = 0, _len = _ref1.length; _i < _len; i = ++_i) { 281 | expectation = _ref1[i]; 282 | if (expectation.test_callback.apply({ 283 | state: this.state, 284 | type: type, 285 | cmd: cmd, 286 | data: data, 287 | recv: recv 288 | })) { 289 | Logger.out("received " + type + ": " + expectation.description); 290 | Logger.out({ 291 | type: 'data' 292 | }, JSON.stringify({ 293 | type: type, 294 | cmd: cmd, 295 | data: data 296 | })); 297 | if (etype === 'Once') { 298 | this._clearExpectation(etype, i); 299 | } 300 | if (typeof expectation.callback === 'function') { 301 | expectation.callback.apply({ 302 | state: this.state, 303 | data: data, 304 | recv: recv 305 | }); 306 | } 307 | return true; 308 | } 309 | } 310 | } 311 | Logger.out({ 312 | type: 'fail' 313 | }, 'received: unexpected response'); 314 | Logger.out({ 315 | type: 'fail' 316 | }, JSON.stringify({ 317 | type: type, 318 | cmd: cmd, 319 | data: data 320 | })); 321 | Logger.out("expectOnce queue: " + JSON.stringify(this.expectations.Once)); 322 | return Logger.out("expectAnytime queue: " + JSON.stringify(this.expectations.Anytime)); 323 | }; 324 | 325 | return Socket; 326 | 327 | })(); 328 | -------------------------------------------------------------------------------- /static/models/User.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var User; 3 | 4 | module.exports = User = (function() { 5 | function User(o) { 6 | this.id = o.id; 7 | this.name = o.name; 8 | this.email = o.email; 9 | this.color = o.color; 10 | return; 11 | } 12 | 13 | return User; 14 | 15 | })(); 16 | -------------------------------------------------------------------------------- /static/views/Bar.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Bar; 3 | 4 | module.exports = Bar = (function() { 5 | function Bar(o) { 6 | this.bg = o.bg || die("Bar.bg must be specified!"); 7 | this.fg = o.fg || die("Bar.fg must be specified!"); 8 | this.text = o.text || ''; 9 | this.resize({ 10 | x: o.x, 11 | y: o.y, 12 | w: o.w, 13 | h: o.h 14 | }); 15 | return; 16 | } 17 | 18 | Bar.prototype.resize = function(o) { 19 | if (o.x) { 20 | this.x = o.x; 21 | } 22 | if (this.x < 1) { 23 | die("Bar.x may not be less than 1!"); 24 | } 25 | this.y = o.y; 26 | if (this.y < 1) { 27 | die("Bar.y may not be less than 1!"); 28 | } 29 | this.w = o.w; 30 | if (this.w < 1) { 31 | die("Bar.w may not be less than 1!"); 32 | } 33 | if (o.h) { 34 | this.h = o.h; 35 | } 36 | if (this.h < 1) { 37 | die("Bar.h may not be less than 1!"); 38 | } 39 | this.draw(); 40 | }; 41 | 42 | Bar.prototype.draw = function() { 43 | this.set_text(this.text); 44 | }; 45 | 46 | Bar.prototype.set_text = function(s, return_cursor) { 47 | var _ref; 48 | if (return_cursor == null) { 49 | return_cursor = true; 50 | } 51 | Terminal.clear_space({ 52 | x: this.x, 53 | y: this.y, 54 | w: this.w, 55 | h: this.h, 56 | bg: this.bg, 57 | fg: this.fg 58 | }).echo(s.substr(0, this.w)).flush(); 59 | this.text = s; 60 | if (return_cursor) { 61 | if ((_ref = Window.current_cursor()) != null) { 62 | _ref.draw(); 63 | } 64 | } 65 | }; 66 | 67 | return Bar; 68 | 69 | })(); 70 | -------------------------------------------------------------------------------- /static/views/BufferView.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Bar, BufferView, BufferViewCursor, HydraBuffer; 3 | 4 | Bar = require('./Bar'); 5 | 6 | HydraBuffer = require('../models/HydraBuffer'); 7 | 8 | BufferViewCursor = require('./BufferViewCursor'); 9 | 10 | module.exports = BufferView = (function() { 11 | function BufferView(o) { 12 | this.tab = o.tab; 13 | if (o.active) { 14 | this.tab.active_view = this; 15 | } 16 | this.buffer = HydraBuffer({ 17 | view: this, 18 | file: o.file 19 | }); 20 | this.lines = this.buffer.data.split("\n"); 21 | this.lines.pop(); 22 | if (!(this.lines.length >= 1)) { 23 | this.lines = ['']; 24 | } 25 | this.gutter = repeat(Math.max(4, this.lines.length.toString().length + 2), ' '); 26 | this.resize({ 27 | x: o.x, 28 | y: o.y, 29 | w: o.w, 30 | h: o.h 31 | }); 32 | this.status_bar = new Bar({ 33 | x: this.x, 34 | y: this.y + this.ih, 35 | w: this.w, 36 | h: 1, 37 | bg: NviConfig.view_status_bar_active_bg, 38 | fg: NviConfig.view_status_bar_active_fg, 39 | text: Terminal.xbg(NviConfig.view_status_bar_active_l1_bg).xfg(NviConfig.view_status_bar_active_l1_fg).echo(this.buffer.path).fg('bold').xfg(NviConfig.view_status_bar_active_l1_fg_bold).echo(this.buffer.base + ' ').fg('unbold').xbg(NviConfig.view_status_bar_active_bg).xfg(NviConfig.view_status_bar_active_fg).get_clean() 40 | }); 41 | this.cursors = [ 42 | new BufferViewCursor({ 43 | user: Application.current_user, 44 | view: this, 45 | x: this.x, 46 | y: this.y, 47 | possessed: true 48 | }) 49 | ]; 50 | return; 51 | } 52 | 53 | BufferView.prototype.destroy = function() { 54 | this.cell.destroy(); 55 | }; 56 | 57 | BufferView.prototype.resize = function(o) { 58 | if (o.x) { 59 | this.x = o.x; 60 | } 61 | if (this.x < 1) { 62 | die("BufferView.x may not be less than 1!"); 63 | } 64 | if (o.y) { 65 | this.y = o.y; 66 | } 67 | if (this.y < 1) { 68 | die("BufferView.y may not be less than 1!"); 69 | } 70 | this.w = o.w; 71 | if (this.w < 1) { 72 | die("BufferView.w may not be less than 1!"); 73 | } 74 | this.h = o.h; 75 | if (this.h < 2) { 76 | die("BufferView.h may not be less than 2!"); 77 | } 78 | this.iw = o.w; 79 | this.ih = o.h - 1; 80 | this.draw(); 81 | if (this.status_bar) { 82 | this.status_bar.resize({ 83 | x: this.x, 84 | y: this.y + this.ih, 85 | w: this.w 86 | }); 87 | } 88 | }; 89 | 90 | BufferView.prototype.draw = function() { 91 | var cursor, i, line, ln, visible_line_h, y, _i, _j, _k, _len, _ref, _ref1; 92 | visible_line_h = Math.min(this.lines.length, this.ih); 93 | for (ln = _i = 0; 0 <= visible_line_h ? _i < visible_line_h : _i > visible_line_h; ln = 0 <= visible_line_h ? ++_i : --_i) { 94 | line = this.lines[ln]; 95 | if (line.length > this.iw) { 96 | line = line.substr(0, this.iw - 1) + '>'; 97 | } 98 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(this.x, this.y + ln).echo((this.gutter + (ln + 1)).substr(-1 * (this.gutter.length - 1)) + ' '); 99 | Terminal.xbg(NviConfig.view_text_bg).xfg(NviConfig.view_text_fg).echo(line).clear_n(this.iw - this.gutter.length - line.length).flush(); 100 | } 101 | if (visible_line_h < this.ih) { 102 | for (y = _j = visible_line_h, _ref = this.ih; visible_line_h <= _ref ? _j < _ref : _j > _ref; y = visible_line_h <= _ref ? ++_j : --_j) { 103 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(this.x, this.y + y).fg('bold').echo('~').fg('unbold').clear_n(this.iw - 1).flush(); 104 | } 105 | } 106 | if (this.cursors) { 107 | _ref1 = this.cursors; 108 | for (i = _k = 0, _len = _ref1.length; _k < _len; i = ++_k) { 109 | cursor = _ref1[i]; 110 | if (i !== 0) { 111 | cursor.draw(); 112 | } 113 | } 114 | } 115 | }; 116 | 117 | return BufferView; 118 | 119 | })(); 120 | -------------------------------------------------------------------------------- /static/views/BufferViewCursor.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var BufferViewCursor; 3 | 4 | module.exports = BufferViewCursor = (function() { 5 | function BufferViewCursor(o) { 6 | this.user = o.user; 7 | this.view = o.view; 8 | this.possessed = o.possessed || false; 9 | this.resize({ 10 | x: o.x, 11 | y: o.y, 12 | w: o.w, 13 | h: o.h 14 | }); 15 | return; 16 | } 17 | 18 | BufferViewCursor.prototype.resize = function(o) { 19 | this.x = o.x; 20 | if (this.x < 1) { 21 | die("BufferViewCursor.x may not be less than 1!"); 22 | } 23 | this.y = o.x; 24 | if (this.y < 1) { 25 | die("BufferViewCursor.y may not be less than 1!"); 26 | } 27 | this.w = 1; 28 | if (this.w < 1) { 29 | die("BufferViewCursor.w may not be less than 1!"); 30 | } 31 | this.h = 1; 32 | if (this.h < 1) { 33 | die("BufferViewCursor.h may not be less than 1!"); 34 | } 35 | this.draw(); 36 | }; 37 | 38 | BufferViewCursor.prototype.go = function(x, y) { 39 | this.x = x; 40 | this.y = y; 41 | Logger.out("BufferView.cursor = x: " + this.x + ", y: " + this.y); 42 | Terminal.go(this.view.x + this.view.gutter.length + this.x - 1, this.view.y + this.y - 1).flush(); 43 | }; 44 | 45 | BufferViewCursor.prototype.move = function(x, y) { 46 | var dx, dy; 47 | if (y == null) { 48 | y = 0; 49 | } 50 | dx = this.x + x; 51 | dy = this.y + y; 52 | if (dx >= 1 && dx <= this.view.iw - this.view.gutter.length && dy >= 1 && dy <= this.view.ih) { 53 | this.go(dx, dy); 54 | } 55 | }; 56 | 57 | BufferViewCursor.prototype.draw = function() { 58 | if (this.possessed) { 59 | this.move(0, 0); 60 | } else { 61 | 62 | } 63 | }; 64 | 65 | return BufferViewCursor; 66 | 67 | })(); 68 | -------------------------------------------------------------------------------- /static/views/Cell.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var BufferView, Cell; 3 | 4 | BufferView = require('./BufferView'); 5 | 6 | module.exports = Cell = (function() { 7 | function Cell(o) { 8 | this.p = o.p; 9 | this.next = this.prev = null; 10 | this.first_child = null; 11 | this.view = o.view || null; 12 | if (o.origin) { 13 | this.next = o.origin; 14 | if (o.origin.prev) { 15 | this.prev = o.origin.prev; 16 | } 17 | o.origin.prev = this; 18 | this.chain = o.origin.chain; 19 | } else { 20 | this.chain = { 21 | dir: o.dir || 'v', 22 | parent: o.parent || null, 23 | x: o.parent ? o.parent.x : o.chain.x, 24 | y: o.parent ? o.parent.y : o.chain.y, 25 | w: o.parent ? o.parent.w : o.chain.w, 26 | h: o.parent ? o.parent.h : o.chain.h 27 | }; 28 | } 29 | this.x = this.y = this.w = this.h = null; 30 | this.resize({ 31 | chain: { 32 | x: this.chain.x, 33 | y: this.chain.y, 34 | w: this.chain.w, 35 | h: this.chain.h 36 | } 37 | }); 38 | } 39 | 40 | Cell.prototype.resize = function(o) { 41 | var i, pc; 42 | if (o != null ? o.chain : void 0) { 43 | if (o.chain.x) { 44 | this.chain.x = o.chain.x; 45 | } 46 | if (this.chain.x < 1) { 47 | die("Cell.chain.x may not be less than 1!"); 48 | } 49 | if (o.chain.y) { 50 | this.chain.y = o.chain.y; 51 | } 52 | if (this.chain.y < 1) { 53 | die("Cell.chain.y may not be less than 1!"); 54 | } 55 | if (o.chain.w) { 56 | this.chain.w = o.chain.w; 57 | } 58 | if (this.chain.w < 1) { 59 | die("Cell.chain.w may not be less than 1!"); 60 | } 61 | if (o.chain.h) { 62 | this.chain.h = o.chain.h; 63 | } 64 | if (this.chain.h < 1) { 65 | die("Cell.chain.h may not be less than 1!"); 66 | } 67 | } 68 | i = 0; 69 | pc = { 70 | x: this.chain.x, 71 | y: this.chain.y, 72 | w: this.chain.w, 73 | h: this.chain.h 74 | }; 75 | this.each_neighbor(function(cell) { 76 | var affected_content; 77 | switch (cell.chain.dir) { 78 | case 'v': 79 | cell.x = pc.x + (i * pc.w); 80 | cell.y = pc.y; 81 | cell.w = Math.floor(cell.p * cell.chain.w); 82 | cell.h = cell.chain.h; 83 | break; 84 | case 'h': 85 | cell.x = pc.x; 86 | cell.y = pc.y + (i * pc.h); 87 | cell.w = cell.chain.w; 88 | cell.h = Math.floor(cell.p * cell.chain.h); 89 | } 90 | if (cell.x < 1) { 91 | die("Cell.x may not be less than 1!"); 92 | } 93 | if (cell.y < 1) { 94 | die("Cell.y may not be less than 1!"); 95 | } 96 | if (cell.w < 1) { 97 | die("Cell.w may not be less than 1!"); 98 | } 99 | if (cell.h < 1) { 100 | die("Cell.h may not be less than 1!"); 101 | } 102 | if (affected_content = cell.view || cell.first_child) { 103 | affected_content.resize({ 104 | x: cell.x, 105 | y: cell.y, 106 | w: cell.w, 107 | h: cell.h 108 | }); 109 | } 110 | i++; 111 | pc = cell; 112 | }); 113 | }; 114 | 115 | Cell.prototype.draw = function() {}; 116 | 117 | Cell.prototype.new_view = function(o) { 118 | o.x = this.x; 119 | o.y = this.y; 120 | o.w = this.w; 121 | o.h = this.h; 122 | this.view = new BufferView(o); 123 | this.view.cell = this; 124 | o.tab.views.push(this.view); 125 | return this.view; 126 | }; 127 | 128 | Cell.prototype.each_neighbor = function(cb) { 129 | var c; 130 | cb(c = this, 'origin'); 131 | while (c.prev) { 132 | cb(c = c.prev, 'prev'); 133 | } 134 | c = this; 135 | while (c.next) { 136 | cb(c = c.next, 'next'); 137 | } 138 | }; 139 | 140 | Cell.prototype.divide = function(view) { 141 | var neighbor, neighbors, new_neighbor, p, _i, _len; 142 | neighbors = []; 143 | this.each_neighbor(function(neighbor, dir) { 144 | neighbors[dir === 'prev' ? 'unshift' : 'push'](neighbor); 145 | }); 146 | p = 1 / (neighbors.length + 1); 147 | for (_i = 0, _len = neighbors.length; _i < _len; _i++) { 148 | neighbor = neighbors[_i]; 149 | neighbor.p = p; 150 | } 151 | new_neighbor = new Cell({ 152 | origin: this, 153 | p: p 154 | }); 155 | this.resize(); 156 | return new_neighbor.new_view(view); 157 | }; 158 | 159 | Cell.prototype.impregnate = function(dir, view) { 160 | var detached_view; 161 | if (!(this.prev || this.next)) { 162 | die("must have more than one cell in the chain to impregnate"); 163 | } 164 | if (this.first_child) { 165 | die("must not already be impregnated to impregnate"); 166 | } 167 | detached_view = this.view; 168 | this.view = null; 169 | this.first_child = new Cell({ 170 | p: 1, 171 | dir: dir, 172 | parent: this, 173 | view: detached_view 174 | }); 175 | return this.first_child.divide(view); 176 | }; 177 | 178 | Cell.prototype.vsplit = function(view) { 179 | return this.divide(view); 180 | }; 181 | 182 | Cell.prototype.hsplit = function(view) { 183 | if (this.prev || this.next) { 184 | return this.impregnate('h', view); 185 | } else { 186 | this.chain.dir = 'h'; 187 | return this.divide(view); 188 | } 189 | }; 190 | 191 | Cell.prototype.destroy = function(o) { 192 | var closest_view, other, survivor, tab, topmost_cell; 193 | if (tab.views.length < 2) { 194 | return false; 195 | } 196 | if (this.prev) { 197 | this.prev.next = this.next; 198 | } 199 | if (this.next) { 200 | this.next.prev = this.prev; 201 | } 202 | if (this.chain.parent.prev === null && this.chain.parent.next === null) { 203 | this.chain.parent.destroy(); 204 | } 205 | this.destroyed = true; 206 | closest_view = function() { 207 | var n; 208 | if (this.view) { 209 | return this.view; 210 | } else if (this.first_child) { 211 | n = this; 212 | while (n.first_child) { 213 | n = n.first_child; 214 | } 215 | return n.view; 216 | } 217 | }; 218 | tab = closest_view().tab; 219 | if (tab.topmost_cell.destroyed) { 220 | other = this.prev || this.next; 221 | topmost_cell = function() { 222 | var n; 223 | n = other; 224 | while (n.chain.parent) { 225 | n = n.chain.parent; 226 | } 227 | return n; 228 | }; 229 | tab.topmost_cell = topmost_cell(); 230 | tab.resize(); 231 | } else { 232 | if (survivor = this.prev || this.next) { 233 | survivor.resize(); 234 | } 235 | } 236 | return true; 237 | }; 238 | 239 | return Cell; 240 | 241 | })(); 242 | -------------------------------------------------------------------------------- /static/views/Tab.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Cell, Tab; 3 | 4 | Cell = require('./Cell'); 5 | 6 | module.exports = Tab = (function() { 7 | function Tab(o) { 8 | this.name = o.name || 'untitled'; 9 | if (o.active) { 10 | Window.active_tab = this; 11 | } 12 | this.views = []; 13 | this.topmost_cell = new Cell({ 14 | p: 1, 15 | chain: { 16 | x: this.x, 17 | y: this.y, 18 | w: this.w, 19 | h: this.ih 20 | } 21 | }); 22 | this.resize({ 23 | x: o.x, 24 | y: o.y, 25 | w: o.w, 26 | h: o.h 27 | }); 28 | this.topmost_cell.new_view({ 29 | tab: this, 30 | file: o.file, 31 | active: o.active 32 | }); 33 | } 34 | 35 | Tab.prototype.destroy = function() {}; 36 | 37 | Tab.prototype.resize = function(o) { 38 | if (o.x) { 39 | this.x = o.x; 40 | } 41 | if (this.x < 1) { 42 | die("Tab.x may not be less than 1!"); 43 | } 44 | if (o.y) { 45 | this.y = o.y; 46 | } 47 | if (this.y < 1) { 48 | die("Tab.y may not be less than 1!"); 49 | } 50 | this.w = o.w; 51 | if (this.w < 1) { 52 | die("Tab.w may not be less than 1!"); 53 | } 54 | this.h = o.h; 55 | if (this.h < 1) { 56 | die("Tab.h may not be less than 1!"); 57 | } 58 | this.ih = o.h; 59 | this.draw(); 60 | this.topmost_cell.resize({ 61 | chain: { 62 | x: this.x, 63 | y: this.y, 64 | w: this.w, 65 | h: this.ih 66 | } 67 | }); 68 | }; 69 | 70 | Tab.prototype.draw = function() {}; 71 | 72 | Tab.prototype.activate_view = function(view) { 73 | var v, _i, _len, _ref; 74 | _ref = this.views; 75 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 76 | v = _ref[_i]; 77 | v.active = v === view; 78 | } 79 | }; 80 | 81 | Tab.prototype.split = function(cmd, file) { 82 | var divider_w, new_view; 83 | divider_w = 1; 84 | if (cmd === 'split') { 85 | cmd = 'hsplit'; 86 | } 87 | new_view = this.active_view.cell[cmd]({ 88 | tab: Window.active_tab, 89 | file: file 90 | }); 91 | return this.activate_view(new_view); 92 | }; 93 | 94 | return Tab; 95 | 96 | })(); 97 | -------------------------------------------------------------------------------- /static/views/Terminal.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Terminal; 3 | 4 | module.exports = Terminal = (function() { 5 | function Terminal() {} 6 | 7 | Terminal.write = function(s) { 8 | process.stdout.write(s); 9 | return this; 10 | }; 11 | 12 | Terminal.buffer = ''; 13 | 14 | Terminal.push_raw = function(s) { 15 | Terminal.buffer += s; 16 | return this; 17 | }; 18 | 19 | Terminal.echo = function(s) { 20 | if (s.length) { 21 | Terminal.cursor.x += s.length; 22 | if (Terminal.cursor.x > Terminal.screen.w) { 23 | Terminal.cursor.y += Math.floor(Terminal.cursor.x / Terminal.screen.w); 24 | Terminal.cursor.x = Terminal.cursor.x % Terminal.screen.w; 25 | } 26 | s.replace(/\n/g, function() { 27 | return Terminal.cursor.y++; 28 | }); 29 | Terminal.push_raw(s); 30 | } 31 | return this; 32 | }; 33 | 34 | Terminal.flush = function() { 35 | Terminal.write(Terminal.buffer); 36 | Terminal.buffer = ''; 37 | return this; 38 | }; 39 | 40 | Terminal.get_clean = function() { 41 | var b; 42 | b = Terminal.buffer; 43 | Terminal.buffer = ''; 44 | return b; 45 | }; 46 | 47 | Terminal.ansi_esc = (function() { 48 | function _Class() {} 49 | 50 | _Class.cursor_pos = function(x, y) { 51 | return "\x1b[" + y + ";" + x + "H"; 52 | }; 53 | 54 | _Class.clear_screen = '\x1b[2J'; 55 | 56 | _Class.clear_eol = '\x1b[K'; 57 | 58 | _Class.clear_eof = '\x1b[J'; 59 | 60 | _Class.color = (function() { 61 | function _Class() {} 62 | 63 | _Class.reset = '\x1b[0m'; 64 | 65 | _Class.bold = '\x1b[1m'; 66 | 67 | _Class.inverse = '\x1b[7m'; 68 | 69 | _Class.strike = '\x1b[9m'; 70 | 71 | _Class.unbold = '\x1b[22m'; 72 | 73 | _Class.black = '\x1b[30m'; 74 | 75 | _Class.red = '\x1b[31m'; 76 | 77 | _Class.green = '\x1b[32m'; 78 | 79 | _Class.yellow = '\x1b[33m'; 80 | 81 | _Class.blue = '\x1b[34m'; 82 | 83 | _Class.magenta = '\x1b[35m'; 84 | 85 | _Class.cyan = '\x1b[36m'; 86 | 87 | _Class.white = '\x1b[37m'; 88 | 89 | _Class.xterm = function(i) { 90 | return "\x1b[38;5;" + i + "m"; 91 | }; 92 | 93 | _Class.bg_reset = '\x1b[49m'; 94 | 95 | _Class.bg_black = '\x1b[40m'; 96 | 97 | _Class.bg_red = '\x1b[41m'; 98 | 99 | _Class.bg_green = '\x1b[42m'; 100 | 101 | _Class.bg_yellow = '\x1b[43m'; 102 | 103 | _Class.bg_blue = '\x1b[44m'; 104 | 105 | _Class.bg_magenta = '\x1b[45m'; 106 | 107 | _Class.bg_cyan = '\x1b[46m'; 108 | 109 | _Class.bg_white = '\x1b[47m'; 110 | 111 | _Class.bg_xterm = function(i) { 112 | return "\x1b[48;5;" + i + "m"; 113 | }; 114 | 115 | return _Class; 116 | 117 | })(); 118 | 119 | return _Class; 120 | 121 | }).call(this); 122 | 123 | Terminal.clear = function() { 124 | return Terminal.push_raw(Terminal.ansi_esc.clear_screen); 125 | }; 126 | 127 | Terminal.cursor = { 128 | x: null, 129 | y: null 130 | }; 131 | 132 | Terminal.screen = { 133 | w: null, 134 | h: null 135 | }; 136 | 137 | Terminal.go = function(x, y) { 138 | if (x < 1) { 139 | die("Terminal.cursor.x " + x + " may not be less than 1!"); 140 | } 141 | if (x > Terminal.screen.w) { 142 | die("Terminal.cursor.x " + x + " may not be greater than Terminal.screen.w or " + Terminal.screen.w + "!"); 143 | } 144 | Terminal.cursor.x = x; 145 | if (y < 1) { 146 | die("Terminal.cursor.y " + y + " may not be less than 1!"); 147 | } 148 | if (y > Terminal.screen.h) { 149 | die("Terminal.cursor.y " + y + " may not be greater than Terminal.screen.h or " + Terminal.screen.h + "!"); 150 | } 151 | Terminal.cursor.y = y; 152 | Terminal.push_raw(Terminal.ansi_esc.cursor_pos(Terminal.cursor.x, Terminal.cursor.y)); 153 | return this; 154 | }; 155 | 156 | Terminal.move = function(x, y) { 157 | var dx, dy; 158 | if (y == null) { 159 | y = 0; 160 | } 161 | dx = Terminal.cursor.x + x; 162 | dy = Terminal.cursor.y + y; 163 | if (dx >= 0 && dx <= Terminal.screen.w && dy >= 0 && dy <= Terminal.screen.h) { 164 | this.go(dx, dy); 165 | } 166 | return this; 167 | }; 168 | 169 | Terminal.fg = function(color) { 170 | return Terminal.push_raw(Terminal.ansi_esc.color[color]); 171 | }; 172 | 173 | Terminal.bg = function(color) { 174 | return Terminal.push_raw(Terminal.ansi_esc.color['bg_' + color]); 175 | }; 176 | 177 | Terminal.xfg = function(i) { 178 | return Terminal.push_raw(Terminal.ansi_esc.color.xterm(i)); 179 | }; 180 | 181 | Terminal.xbg = function(i) { 182 | return Terminal.push_raw(Terminal.ansi_esc.color.bg_xterm(i)); 183 | }; 184 | 185 | Terminal.clear_screen = function() { 186 | var y, _i, _ref; 187 | Terminal.clear(); 188 | for (y = _i = 1, _ref = Terminal.screen.h; 1 <= _ref ? _i <= _ref : _i >= _ref; y = 1 <= _ref ? ++_i : --_i) { 189 | Terminal.go(1, y); 190 | Terminal.clear_eol(); 191 | } 192 | return this; 193 | }; 194 | 195 | Terminal.clear_n = function(n) { 196 | return Terminal.echo(repeat(n, ' ')); 197 | }; 198 | 199 | Terminal.clear_eol = function() { 200 | return Terminal.clear_n(Terminal.screen.w - Terminal.cursor.x + 1); 201 | }; 202 | 203 | Terminal.clear_space = function(o) { 204 | var y, _i, _ref, _ref1; 205 | for (y = _i = _ref = o.y, _ref1 = o.y + o.h - 1; _ref <= _ref1 ? _i <= _ref1 : _i >= _ref1; y = _ref <= _ref1 ? ++_i : --_i) { 206 | Terminal.xbg(o.bg).go(o.x, y).clear_n(o.w); 207 | } 208 | Terminal.go(o.x, o.y).xbg(o.bg).xfg(o.fg).flush(); 209 | return this; 210 | }; 211 | 212 | return Terminal; 213 | 214 | }).call(this); 215 | -------------------------------------------------------------------------------- /static/views/View.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Bar, HydraBuffer, View, ViewCursor; 3 | 4 | Bar = require('./Bar'); 5 | 6 | HydraBuffer = require('../models/HydraBuffer'); 7 | 8 | ViewCursor = require('./ViewCursor'); 9 | 10 | module.exports = View = (function() { 11 | function View(o) { 12 | this.tab = o.tab; 13 | if (o.active) { 14 | this.tab.active_view = this; 15 | } 16 | this.buffer = HydraBuffer({ 17 | view: this, 18 | file: o.file 19 | }); 20 | this.lines = this.buffer.data.split("\n"); 21 | this.lines.pop(); 22 | if (!(this.lines.length >= 1)) { 23 | this.lines = ['']; 24 | } 25 | this.gutter = repeat(Math.max(4, this.lines.length.toString().length + 2), ' '); 26 | this.resize({ 27 | x: o.x, 28 | y: o.y, 29 | w: o.w, 30 | h: o.h 31 | }); 32 | this.status_bar = new Bar({ 33 | x: this.x, 34 | y: this.y + this.ih, 35 | w: this.w, 36 | h: 1, 37 | bg: NviConfig.view_status_bar_active_bg, 38 | fg: NviConfig.view_status_bar_active_fg, 39 | text: Terminal.xbg(NviConfig.view_status_bar_active_l1_bg).xfg(NviConfig.view_status_bar_active_l1_fg).echo(this.buffer.path).fg('bold').xfg(NviConfig.view_status_bar_active_l1_fg_bold).echo(this.buffer.base + ' ').fg('unbold').xbg(NviConfig.view_status_bar_active_bg).xfg(NviConfig.view_status_bar_active_fg).get_clean() 40 | }); 41 | this.cursors = [ 42 | new ViewCursor({ 43 | user: Window.current_user, 44 | view: this, 45 | x: this.x, 46 | y: this.y, 47 | possessed: true 48 | }) 49 | ]; 50 | return; 51 | } 52 | 53 | View.prototype.resize = function(o) { 54 | if (o.x) { 55 | this.x = o.x; 56 | } 57 | if (this.x < 1) { 58 | die("View.x may not be less than 1!"); 59 | } 60 | if (o.y) { 61 | this.y = o.y; 62 | } 63 | if (this.y < 1) { 64 | die("View.y may not be less than 1!"); 65 | } 66 | this.w = o.w; 67 | if (this.w < 1) { 68 | die("View.w may not be less than 1!"); 69 | } 70 | this.h = o.h; 71 | if (this.h < 2) { 72 | die("View.h may not be less than 2!"); 73 | } 74 | this.iw = o.w; 75 | this.ih = o.h - 1; 76 | this.draw(); 77 | }; 78 | 79 | View.prototype.draw = function() { 80 | var cursor, i, line, ln, visible_line_h, y, _i, _j, _k, _len, _ref, _ref1; 81 | visible_line_h = Math.min(this.lines.length, this.ih); 82 | for (ln = _i = 0; 0 <= visible_line_h ? _i < visible_line_h : _i > visible_line_h; ln = 0 <= visible_line_h ? ++_i : --_i) { 83 | line = this.lines[ln]; 84 | if (line.length > this.iw) { 85 | line = line.substr(0, this.iw - 1) + '>'; 86 | } 87 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(this.x, this.y + ln).echo((this.gutter + (ln + 1)).substr(-1 * (this.gutter.length - 1)) + ' '); 88 | Terminal.xbg(NviConfig.view_text_bg).xfg(NviConfig.view_text_fg).echo(line).clear_n(this.iw - this.gutter.length - line.length).flush(); 89 | } 90 | if (visible_line_h < this.ih) { 91 | for (y = _j = visible_line_h, _ref = this.ih; visible_line_h <= _ref ? _j < _ref : _j > _ref; y = visible_line_h <= _ref ? ++_j : --_j) { 92 | Terminal.xbg(NviConfig.view_gutter_bg).xfg(NviConfig.view_gutter_fg).go(this.x, this.y + y).fg('bold').echo('~').fg('unbold').clear_n(this.iw - 1).flush(); 93 | } 94 | } 95 | if (this.status_bar) { 96 | this.status_bar.resize({ 97 | x: this.x, 98 | y: this.y + this.ih, 99 | w: this.w 100 | }); 101 | } 102 | if (this.cursors) { 103 | _ref1 = this.cursors; 104 | for (i = _k = 0, _len = _ref1.length; _k < _len; i = ++_k) { 105 | cursor = _ref1[i]; 106 | if (i !== 0) { 107 | cursor.draw(); 108 | } 109 | } 110 | } 111 | }; 112 | 113 | return View; 114 | 115 | })(); 116 | -------------------------------------------------------------------------------- /static/views/ViewCursor.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var ViewCursor; 3 | 4 | module.exports = ViewCursor = (function() { 5 | function ViewCursor(o) { 6 | this.user = o.user; 7 | this.view = o.view; 8 | this.possessed = o.possessed || false; 9 | this.resize({ 10 | x: o.x, 11 | y: o.y, 12 | w: o.w, 13 | h: o.h 14 | }); 15 | return; 16 | } 17 | 18 | ViewCursor.prototype.resize = function(o) { 19 | this.x = o.x; 20 | if (this.x < 1) { 21 | die("ViewCursor.x may not be less than 1!"); 22 | } 23 | this.y = o.x; 24 | if (this.y < 1) { 25 | die("ViewCursor.y may not be less than 1!"); 26 | } 27 | this.w = 1; 28 | if (this.w < 1) { 29 | die("ViewCursor.w may not be less than 1!"); 30 | } 31 | this.h = 1; 32 | if (this.h < 1) { 33 | die("ViewCursor.h may not be less than 1!"); 34 | } 35 | this.draw(); 36 | }; 37 | 38 | ViewCursor.prototype.go = function(x, y) { 39 | this.x = x; 40 | this.y = y; 41 | Logger.out("View.cursor = x: " + this.x + ", y: " + this.y); 42 | Terminal.go(this.view.x + this.view.gutter.length + this.x - 1, this.view.y + this.y - 1).flush(); 43 | }; 44 | 45 | ViewCursor.prototype.move = function(x, y) { 46 | var dx, dy; 47 | if (y == null) { 48 | y = 0; 49 | } 50 | dx = this.x + x; 51 | dy = this.y + y; 52 | if (dx >= 1 && dx <= this.view.iw - this.view.gutter.length && dy >= 1 && dy <= this.view.ih) { 53 | this.go(dx, dy); 54 | } 55 | }; 56 | 57 | ViewCursor.prototype.draw = function() { 58 | if (this.possessed) { 59 | this.move(0, 0); 60 | } else { 61 | 62 | } 63 | }; 64 | 65 | return ViewCursor; 66 | 67 | })(); 68 | -------------------------------------------------------------------------------- /static/views/Window.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var Bar, Tab, Window; 3 | 4 | Bar = require('./Bar'); 5 | 6 | Tab = require('./Tab'); 7 | 8 | module.exports = Window = (function() { 9 | function Window() {} 10 | 11 | Window.init = function(o) { 12 | Window.x = 1; 13 | Window.y = 1; 14 | Window.resize(); 15 | Window.status_bar = new Bar({ 16 | x: Window.x, 17 | y: Window.h, 18 | w: Window.w, 19 | h: 1, 20 | bg: NviConfig.window_status_bar_bg, 21 | fg: NviConfig.window_status_bar_fg 22 | }); 23 | Window.tabs = [ 24 | new Tab({ 25 | file: o != null ? o.file : void 0, 26 | x: Window.x, 27 | y: Window.y, 28 | w: Window.w, 29 | h: Window.ih, 30 | active: true 31 | }) 32 | ]; 33 | }; 34 | 35 | Window.resize = function() { 36 | var tab, _i, _len, _ref; 37 | Logger.out("window caught resize " + process.stdout.columns + ", " + process.stdout.rows); 38 | Terminal.screen.w = process.stdout.columns; 39 | Terminal.screen.h = process.stdout.rows; 40 | if (Terminal.screen.w < 1 || Terminal.screen.h < 3) { 41 | return; 42 | } 43 | Window.w = Terminal.screen.w; 44 | Window.h = Terminal.screen.h; 45 | Window.ih = Window.h - 1; 46 | Window.iw = Window.w; 47 | Window.draw(); 48 | if (Window.status_bar) { 49 | Window.status_bar.resize({ 50 | y: Window.h, 51 | w: Window.w 52 | }); 53 | } 54 | if (Window.tabs) { 55 | _ref = Window.tabs; 56 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 57 | tab = _ref[_i]; 58 | tab.resize({ 59 | w: Window.w, 60 | h: Window.ih 61 | }); 62 | } 63 | } 64 | }; 65 | 66 | Window.draw = function() {}; 67 | 68 | Window.current_cursor = function() { 69 | var _ref, _ref1, _ref2; 70 | return (_ref = Window.active_tab) != null ? (_ref1 = _ref.active_view) != null ? (_ref2 = _ref1.cursors) != null ? _ref2[0] : void 0 : void 0 : void 0; 71 | }; 72 | 73 | return Window; 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | assert = require('chai').assert 2 | global.Logger = out: -> 3 | global.Terminal = require '../precompile/views/Terminal' 4 | 5 | describe 'the nvi editor', -> 6 | it 'can render xterm-256 colors', -> 7 | Terminal.echo("\n ").flush() 8 | # without a space at the beginning of the line, 9 | # newlines have the background color of the first character on the line 10 | # until the end of the line. weird! 11 | for i in [0..255] 12 | Terminal.fg('reset').echo("\n ").flush() if (i is 8 or i is 16) or 0 is (i-16) % 36 13 | Terminal.xbg(i).xfg(0).echo(("000"+i).substr(-3)).fg('reset').echo(' ').flush() 14 | Terminal.echo("\n").flush() 15 | --------------------------------------------------------------------------------