From 4c6a5bd129980807c61068fb3fb30b27044dfb7a Mon Sep 17 00:00:00 2001 From: Marcin Kulik Date: Wed, 9 Mar 2011 19:05:53 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + Gemfile | 13 + Gemfile.lock | 85 +++++ app.rb | 68 ++++ app/models.rb | 32 ++ app/views/index.erb | 9 + app/views/layout.erb | 31 ++ app/views/show.erb | 16 + config.ru | 6 + config/init.rb | 3 + public/javascripts/ansi-interpreter.js | 180 +++++++++++ public/javascripts/misc.js | 10 + public/javascripts/player.js | 54 ++++ public/javascripts/terminal.js | 429 +++++++++++++++++++++++++ public/javascripts/utf8.js | 68 ++++ public/stylesheets/main.css | 183 +++++++++++ public/stylesheets/reset.css | 88 +++++ 17 files changed, 1278 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 app.rb create mode 100644 app/models.rb create mode 100644 app/views/index.erb create mode 100644 app/views/layout.erb create mode 100644 app/views/show.erb create mode 100644 config.ru create mode 100644 config/init.rb create mode 100644 public/javascripts/ansi-interpreter.js create mode 100644 public/javascripts/misc.js create mode 100644 public/javascripts/player.js create mode 100644 public/javascripts/terminal.js create mode 100644 public/javascripts/utf8.js create mode 100644 public/stylesheets/main.css create mode 100644 public/stylesheets/reset.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02f6344 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.sqlite3 +*.rbc +public/system/* diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d38a966 --- /dev/null +++ b/Gemfile @@ -0,0 +1,13 @@ +source :rubygems + +gem 'bundler' +gem 'sinatra' +gem 'dm-core' +gem 'dm-timestamps' +gem 'dm-migrations' +gem 'dm-validations' +gem 'dm-serializer' +gem 'dm-sqlite-adapter' +gem 'dm-postgres-adapter' +gem "dm-paperclip" +gem 'require_relative' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d605871 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,85 @@ +GEM + remote: http://rubygems.org/ + specs: + addressable (2.2.4) + data_objects (0.10.3) + addressable (~> 2.1) + datamapper (1.0.2) + dm-aggregates (= 1.0.2) + dm-constraints (= 1.0.2) + dm-core (= 1.0.2) + dm-migrations (= 1.0.2) + dm-serializer (= 1.0.2) + dm-timestamps (= 1.0.2) + dm-transactions (= 1.0.2) + dm-types (= 1.0.2) + dm-validations (= 1.0.2) + dm-aggregates (1.0.2) + dm-core (~> 1.0.2) + dm-constraints (1.0.2) + dm-core (~> 1.0.2) + dm-migrations (~> 1.0.2) + dm-core (1.0.2) + addressable (~> 2.2) + extlib (~> 0.9.15) + dm-do-adapter (1.0.2) + data_objects (~> 0.10.2) + dm-core (~> 1.0.2) + dm-migrations (1.0.2) + dm-core (~> 1.0.2) + dm-paperclip (2.4.1) + datamapper + extlib + dm-postgres-adapter (1.0.2) + dm-do-adapter (~> 1.0.2) + do_postgres (~> 0.10.2) + dm-serializer (1.0.2) + dm-core (~> 1.0.2) + fastercsv (~> 1.5.3) + json_pure (~> 1.4) + dm-sqlite-adapter (1.0.2) + dm-do-adapter (~> 1.0.2) + do_sqlite3 (~> 0.10.2) + dm-timestamps (1.0.2) + dm-core (~> 1.0.2) + dm-transactions (1.0.2) + dm-core (~> 1.0.2) + dm-types (1.0.2) + dm-core (~> 1.0.2) + fastercsv (~> 1.5.3) + json_pure (~> 1.4) + stringex (~> 1.1.0) + uuidtools (~> 2.1.1) + dm-validations (1.0.2) + dm-core (~> 1.0.2) + do_postgres (0.10.3) + data_objects (= 0.10.3) + do_sqlite3 (0.10.3) + data_objects (= 0.10.3) + extlib (0.9.15) + fastercsv (1.5.4) + json_pure (1.5.1) + rack (1.2.1) + require_relative (0.0.1) + sinatra (1.2.0) + rack (~> 1.1) + tilt (>= 1.2.2, < 2.0) + stringex (1.1.0) + tilt (1.2.2) + uuidtools (2.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + bundler + dm-core + dm-migrations + dm-paperclip + dm-postgres-adapter + dm-serializer + dm-sqlite-adapter + dm-timestamps + dm-validations + require_relative + sinatra diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..817b478 --- /dev/null +++ b/app.rb @@ -0,0 +1,68 @@ +APP_ROOT = File.dirname(__FILE__) + +require_relative 'config/init' +require_relative 'app/models' + +set :root, File.dirname(__FILE__) +set :static, true +set :views, File.join(APP_ROOT, 'app', 'views') + +helpers do + def player_data(movie) + data = File.read(movie.typescript.path).split("\n",2)[1] + time = File.read(movie.timing.path) + + chars = "'" + data.bytes.map { |b| '\x' + format('%02x', b) }.join('') + "'"; + formatted_time = '[' + time.split("\n").map { |line| delay, n = line.split; '[' + delay.to_f.to_s + ',' + n.to_i.to_s + ']'}.join(',') + ']' + + out = "" + out + end +end + +def make_paperclip_mash(file_hash) + mash = Mash.new + mash['tempfile'] = file_hash[:tempfile] + mash['filename'] = file_hash[:filename] + mash['content_type'] = file_hash[:type] + mash['size'] = file_hash[:tempfile].size + mash +end + +get %r{/(?\d+)} do + @movie = Movie.get(params[:id]) or pass + erb :show +end + +get '/' do + @movies = Movie.all(:order => :created_at.desc) + erb :index +end + +get '/about' do + erb :about +end + +post '/scripts' do + movie = Movie.new( + :terminal_cols => params[:terminal_cols], + :terminal_lines => params[:terminal_lines], + :typescript => make_paperclip_mash(params[:typescript]), + :timing => make_paperclip_mash(params[:timing]) + ) + + if movie.save + response.status = 201 + content_type = :json + movie.to_json + else + response.status = 422 + content_type = :json + movie.errors.to_json + end +end diff --git a/app/models.rb b/app/models.rb new file mode 100644 index 0000000..f38963a --- /dev/null +++ b/app/models.rb @@ -0,0 +1,32 @@ +class Movie + include DataMapper::Resource + include Paperclip::Resource + + property :id, Serial + + property :name, String + + property :typescript_file_name, String, :required => true, :length => 256 + property :typescript_content_type, String, :length => 128 + property :typescript_file_size, Integer + property :typescript_updated_at, DateTime + + property :timing_file_name, String, :required => true, :length => 256 + property :timing_content_type, String, :length => 128 + property :timing_file_size, Integer + property :timing_updated_at, DateTime + + property :terminal_type, String + property :terminal_cols, Integer, :required => true + property :terminal_lines, Integer, :required => true + + timestamps :at + + has_attached_file :typescript, :path => "#{APP_ROOT}/public/system/:attachment/:id" + has_attached_file :timing, :path => "#{APP_ROOT}/public/system/:attachment/:id" + + validates_attachment_presence :typescript + validates_attachment_presence :timing +end + +DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{File.expand_path(File.join(APP_ROOT, 'db', 'db.sqlite3'))}") diff --git a/app/views/index.erb b/app/views/index.erb new file mode 100644 index 0000000..9af57cc --- /dev/null +++ b/app/views/index.erb @@ -0,0 +1,9 @@ +

Ansi.tv

+ + diff --git a/app/views/layout.erb b/app/views/layout.erb new file mode 100644 index 0000000..66f4142 --- /dev/null +++ b/app/views/layout.erb @@ -0,0 +1,31 @@ + + + + + + + + + + + + +
+
+ + +
+
+ <%= yield %> +
+
+
+ + diff --git a/app/views/show.erb b/app/views/show.erb new file mode 100644 index 0000000..6042699 --- /dev/null +++ b/app/views/show.erb @@ -0,0 +1,16 @@ +

Becoming more productive with Vim (<%= @movie.id %>)

+ +
+
+

+    
--------------========
+
+
+
+ +
+

I’ve been doing some reflecting this week on how I can work smarter (instead of harder), and one of the things I came up with was adding a few more tools to my Vim repertoire. I spend more than half of my engineering time in Vim (the other half usually being in a web browser), so I figured that a few minutes here and there would eventually add up in a big way.

+

But like anything else with Vim, there are always multiple ways of accomplishing the very same thing, so I make no guarantees that there aren’t simpler ways of getting this done — but I can say that this way gets the job done, and is pretty easy to get working on your own system.

+
+ +<%= player_data(@movie) %> diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..0ae4887 --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +require "bundler/setup" +Bundler.require + +require File.expand_path(File.join(File.dirname(__FILE__), "app")) + +run Sinatra::Application diff --git a/config/init.rb b/config/init.rb new file mode 100644 index 0000000..91e9a33 --- /dev/null +++ b/config/init.rb @@ -0,0 +1,3 @@ +Paperclip.configure do |config| + config.use_dm_validations = true +end diff --git a/public/javascripts/ansi-interpreter.js b/public/javascripts/ansi-interpreter.js new file mode 100644 index 0000000..c8ead6c --- /dev/null +++ b/public/javascripts/ansi-interpreter.js @@ -0,0 +1,180 @@ +SP.AnsiInterpreter = function(terminal) { + this.terminal = terminal; + this.compilePatterns(); +} + +SP.AnsiInterpreter.prototype = { + PATTERNS: { + "\x07": function(data) { + console.log("bell!"); + }, + + "\x08": function(data) { + // console.log("bs"); + this.terminal.bs(); + }, + + "\x0a": function(data) { + this.terminal.cursorDown(); + }, + + "\x0d": function(data) { + this.terminal.cr(); + }, + + "\x0e": function(data) { + }, + + "\x0f": function(data) { + }, + + "\x82": function(data) { // Reserved (?) + }, + + "\x94": function(data) { // Cancel Character, ignore previous character + }, + + // 20 - 7e + "([\x20-\x7e]|\xe2..|[\xc5\xc4].)+": function(data, match) { + this.terminal.print(match[0]); + }, + + "\x1b\\(B": function(data) { // SCS (Set G0 Character SET) + }, + + "\x1b\\[(?:[0-9]+)?(?:;[0-9]+)*([\x40-\x7e])": function(data, match) { + this.params = []; + var re = /(\d+)/g; + var m; + + while (m = re.exec(match[0])) { + this.params.push(parseInt(m[1])); + } + + this.n = this.params[0]; + this.m = this.params[1]; + + this.handleCSI(match[1]); + }, + + "\x1b\\[\\?([\x30-\x3f]+)([hlsr])": function(data, match) { // private standards + // h = Sets DEC/xterm specific mode (http://ttssh2.sourceforge.jp/manual/en/about/ctrlseq.html#decmode) + // l = Resets mode (http://ttssh2.sourceforge.jp/manual/en/about/ctrlseq.html#mode) + // 1049 + h = Save cursor position, switch to alternate screen buffer, and clear screen. + // 1049 + l = Clear screen, switch to normal screen buffer, and restore cursor position. + // 1001 + s = ? + // 1001 + r = ? + + if (match[1] == '1049') { + if (match[2] == 'h') { + this.terminal.saveCursor(); + } else if (match[2] == 'l') { + this.terminal.restoreCursor(); + } + } + }, + + "\x1b\x3d": function(data) { // DECKPAM - Set keypad to applications mode (ESCape instead of digits) + }, + + "\x1b\x3e": function(data) { // DECKPNM - Set keypad to numeric mode (digits intead of ESCape seq) + }, + + "\x1b\\\x5d\x30\x3b(?:.)*?\x07": function(data, match) { // OSC - Operating System Command (terminal title) + }, + + "\x1b\x37": function(data) { // save cursor pos and char attrs + this.terminal.saveCursor(); + }, + + "\x1b\x38": function(data) { // restore cursor pos and char attrs + this.terminal.restoreCursor(); + } + }, + + handleCSI: function(term) { + switch(term) { + case "@": + this.terminal.reserveCharacters(this.n); + break; + case "A": + this.terminal.cursorUp(this.n || 1); + break; + case "B": + this.terminal.cursorDown(this.n || 1); + break; + case "C": + this.terminal.cursorForward(this.n || 1); + break; + case "D": + this.terminal.cursorBack(this.n || 1); + break; + case "H": + this.terminal.setCursorPos(this.n || 1, this.m || 1); + break; + case "J": + this.terminal.eraseData(this.n || 0); + break; + case "K": + this.terminal.eraseLine(this.n || 0); + break; + case "l": // l, Reset mode + console.log("(TODO) reset: " + this.n); + break; + case "m": + this.terminal.setSGR(this.params); + break; + case "r": // Set top and bottom margins (scroll region on VT100) + break; + default: + console.log('no handler for CSI term: ' + term); + } + }, + + compilePatterns: function() { + this.COMPILED_PATTERNS = []; + var regexp; + + for (re in this.PATTERNS) { + regexp = new RegExp('^' + re); + this.COMPILED_PATTERNS.push([regexp, this.PATTERNS[re]]); + } + }, + + feed: function(data) { + if (data.length == 0) return; + + // console.log(data); + + var match; + var handler; + + for (var i=0; i"); + container.append(row); + container.append("\n"); + this.lineData[l] = []; + this.updateLine(l); + } + }, + + setSGR: function(codes) { + if (codes.length == 0) { + codes = [0]; + } + + for (var i=0; i= 30 && n <= 37) { + this.fg = n - 30; + } else if (n >= 40 && n <= 47) { + this.bg = n - 40; + } + } + }, + + updateLine: function(n) { + n = (typeof n != "undefined" ? n : this.cursorLine); + this.dirtyLines.push(n); + }, + + updateDirtyLines: function() { + var updated = []; + + for (var i=0; i' + (text[this.cursorCol] || '') + ""], text.slice(this.cursorCol + 1) || []); + } else { + html = this.lineData[n]; + } + + this.element.find(".line:eq(" + n + ")").html(html.join('')); + }, + + setCursorPos: function(line, col) { + line -= 1; + col -= 1; + var oldLine = this.cursorLine; + this.cursorLine = line; + this.cursorCol = col; + this.updateLine(oldLine); + this.updateLine(); + }, + + saveCursor: function() { + this.savedCol = this.cursorCol; + this.savedLine = this.cursorLine; + }, + + restoreCursor: function() { + var oldLine = this.cursorLine; + + this.cursorLine = this.savedLine; + this.cursorCol = this.savedCol; + + this.updateLine(oldLine); + this.updateLine(); + }, + + cursorLeft: function() { + if (this.cursorCol > 0) + this.cursorCol = this.cursorCol - 1; + this.updateLine(); + }, + + cursorRight: function() { + if (this.cursorCol < this.cols) + this.cursorCol = this.cursorCol + 1; + this.updateLine(); + }, + + cursorUp: function() { + if (this.cursorLine > 0) + this.cursorLine = this.cursorLine - 1; + this.updateLine(this.cursorLine); + this.updateLine(this.cursorLine+1); + }, + + cursorDown: function() { + if (this.cursorLine < this.lines) + this.cursorLine = this.cursorLine + 1; + this.updateLine(this.cursorLine); + this.updateLine(this.cursorLine-1); + }, + + cursorForward: function(n) { + for (var i=0; i 0) { + this.lineData[this.cursorLine][this.cursorCol - 1] = ' '; + this.cursorCol = this.cursorCol - 1; + this.updateLine(); + } + }, + + print: function(text) { + text = Utf8.decode(text); + + for (var i=0; i 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } + else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + + } + + return utftext; + }, + + // public method for url decoding + decode : function (utftext) { + var string = ""; + var i = 0; + var c = c1 = c2 = 0; + + while ( i < utftext.length ) { + + c = utftext.charCodeAt(i); + + if (c < 128) { + string += String.fromCharCode(c); + i++; + } + else if((c > 191) && (c < 224)) { + c2 = utftext.charCodeAt(i+1); + string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); + i += 2; + } + else { + c2 = utftext.charCodeAt(i+1); + c3 = utftext.charCodeAt(i+2); + string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); + i += 3; + } + + } + + return string; + } + +} diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css new file mode 100644 index 0000000..0421d17 --- /dev/null +++ b/public/stylesheets/main.css @@ -0,0 +1,183 @@ +.clear { + clear: both; + display: block; + visibility: hidden; +} + +body { + background-color: #E7E7DE; + margin: 0; + padding: 0; +} + +body, div, p { + font-family: arial, helvetica; + font-size: 12px; +} + +/* 325A66 */ +/* DEA140 */ +/* A32B26 */ +/* 590D0B */ + +h1 { + color: #3e3e3e; + font-family: 'Trebuchet MS'; + font-size: 42px; + padding: 15px 0; +} + +h2 { + color: #3e3e3e; + font-family: 'Trebuchet MS'; + font-size: 26px; + padding: 15px 15px 40px 15px; +} + +#top { + position: relative; + height: 80px; +} + +#logo { + display: block; + width: 300px; + position: absolute; + top: 0; + left: 0; +} + +#menu { + display: block; + width: 300px; + position: absolute; + top: 0; + right: 0; + font-size: 11px; + font-family: Verdana; +} + +#menu .links { + background-color: #172322; + list-style: none; + padding: 10px; + border-bottom-left-radius: 10px; +} + +#menu .links li { + color: #eee; + display: inline-block; + margin-right: 20px; +} + +#all { + margin: 0 auto; + width: 980px; + text-align: left; +} + +#main { + background-color: white; + width: 980px; + font-size: 11px; + border-top-left-radius: 10px; +} + +.player { + /* border: 1px solid #777;*/ + /* background-color: #333;*/ + float: left; + display: block; + padding: 0px; + margin: 0px 0px 30px 20px; + position: relative; +} + +.term { + padding: 0px; + margin: 0px; + display: block; + font-family: 'Droid Sans Mono', Monospace; + white-space: pre; + background-color: black; + line-height: 1.2em; + color: #ccc; +} + +.term .line { + font-size: 12px; + /* background-color: black;*/ + /* padding: 0;*/ + /* margin: 0;*/ +} + +.line .cursor { + background-color: #D3D7CF; +} + +.line span.cursor.inverted { + background-color: inherit; +} + +.hud { + background-color: #333; + opacity: 0.85; + position: absolute; + left: 20px; + right: 20px; + bottom: 10px; + display: none; + height: 30px; + color: white; +} + +.player:hover .hud { + display: block; +} + + .fg0 { color: #000000 } + .fg1 { color: #CC0000 } + .fg2 { color: #4E9A06 } + .fg3 { color: #C4A000 } + .fg4 { color: #3465A4 } + .fg5 { color: #75507B } + .fg6 { color: #06989A } + .fg7 { color: #D3D7CF } + .fg8 { color: #555753; font-weight: bold } + .fg9 { color: #EF2929; font-weight: bold } +.fg10 { color: #8AE234; font-weight: bold } +.fg11 { color: #FCE94F; font-weight: bold } +.fg12 { color: #729FCF; font-weight: bold } +.fg13 { color: #AD7FA8; font-weight: bold } +.fg14 { color: #34E2E2; font-weight: bold } +.fg15 { color: #EEEEEC; font-weight: bold } + +.bright { font-weight: bold } + + .bg0 { background-color: #000000 } + .bg1 { background-color: #CC0000 } + .bg2 { background-color: #4E9A06 } + .bg3 { background-color: #C4A000 } + .bg4 { background-color: #3465A4 } + .bg5 { background-color: #75507B } + .bg6 { background-color: #06989A } + .bg7 { background-color: #D3D7CF } + .bg8 { background-color: #555753 } + .bg9 { background-color: #EF2929 } +.bg10 { background-color: #8AE234 } +.bg11 { background-color: #FCE94F } +.bg12 { background-color: #729FCF } +.bg13 { background-color: #AD7FA8 } +.bg14 { background-color: #34E2E2 } +.bg15 { background-color: #EEEEEC } + +.description { + color: #666; + font-family: arial, helvetica; + font-size: 16px; + margin: 20px; +} + +.description p { + margin-bottom: 20px; +} diff --git a/public/stylesheets/reset.css b/public/stylesheets/reset.css new file mode 100644 index 0000000..b5b012c --- /dev/null +++ b/public/stylesheets/reset.css @@ -0,0 +1,88 @@ +/* html5doctor.com Reset Stylesheet v1.6.1 +Last Updated: 2010-09-17 +Author: Richard Clark - http://richclarkdesign.com +*/ +html, body, div, span, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +abbr, address, cite, code, +del, dfn, em, img, ins, kbd, q, samp, +small, strong, sub, sup, var, +b, i, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, figcaption, figure, +footer, header, hgroup, menu, nav, section, summary, +time, mark, audio, video { + margin:0; + padding:0; + border:0; + outline:0; + font-size:100%; + vertical-align:baseline; + background:transparent; +} +body { + line-height:1; +} +article,aside,details,figcaption,figure, +footer,header,hgroup,menu,nav,section { + display:block; +} +nav ul { + list-style:none; +} +blockquote, q { + quotes:none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content:''; + content:none; +} +a { + margin:0; + padding:0; + font-size:100%; + vertical-align:baseline; + background:transparent; +} +/* change colours to suit your needs */ +ins { + background-color:#ff9; + color:#000; + text-decoration:none; +} +/* change colours to suit your needs */ +mark { + background-color:#ff9; + color:#000; + font-style:italic; + font-weight:bold; +} +del { + text-decoration: line-through; +} +abbr[title], dfn[title] { + border-bottom:1px dotted; + cursor:help; +} +table { + border-collapse:collapse; + border-spacing:0; +} +/* change border colour to suit your needs */ +hr { + display:block; + height:1px; + border:0; + border-top:1px solid #cccccc; + margin:1em 0; + padding:0; +} + +input, select { + vertical-align:middle; + margin-top: 0; +} +