├── .ameba.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bin └── vicr ├── shard.lock ├── shard.yml ├── spec ├── spec_helper.cr ├── vicr │ ├── config │ │ ├── compiler_spec.cr │ │ ├── editor_spec.cr │ │ └── run_file_spec.cr │ └── service │ │ ├── carc_in_spec.cr │ │ └── github_spec.cr └── vicr_spec.cr └── src ├── vicr.cr └── vicr ├── buffer.cr ├── cli.cr ├── config ├── compiler.cr ├── editor.cr ├── run_file.cr └── settings.cr ├── runner.cr ├── service ├── carc_in.cr └── github.cr └── version.cr /.ameba.yml: -------------------------------------------------------------------------------- 1 | LineLength: 2 | # Disallows lines longer that MaxLength number of symbols. 3 | Enabled: true 4 | MaxLength: 105 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /.crystal/ 4 | /.shards/ 5 | /out 6 | /bin/ameba* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | install: 3 | - shards install 4 | script: 5 | - crystal spec 6 | - bin/ameba 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Vitalii Elenhaupt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OUT_DIR=./out 2 | 3 | all: build 4 | 5 | build: 6 | mkdir -p $(OUT_DIR) 7 | crystal build --release bin/vicr -o $(OUT_DIR)/vicr 8 | 9 | install: 10 | cp $(OUT_DIR)/vicr /usr/local/bin/ 11 | 12 | run: 13 | $(OUT_DIR)/vicr 14 | 15 | clean: 16 | rm -rf $(OUT_DIR) .crystal .deps libs 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Build Status](https://travis-ci.org/veelenga/vicr.svg?branch=master)](https://travis-ci.org/veelenga/vicr) 4 | 5 | Vicr stands for **"Vim-like Interactive CRystal"** and represents a tiny command line application 6 | that designed to quickly execute Crystal code with fast feedback and options to proceed: 7 | 8 | ![](https://github.com/veelenga/bin/raw/master/vicr/demo.gif) 9 | 10 | ## Installation 11 | 12 | ### OS X 13 | 14 | ```sh 15 | $ brew tap veelenga/tap 16 | $ brew install vicr 17 | ``` 18 | 19 | ### From sources: 20 | 21 | ```sh 22 | $ git clone https://github.com/veelenga/vicr 23 | $ cd vicr 24 | $ make 25 | $ sudo make install 26 | ``` 27 | 28 | ## Usage 29 | 30 | Open terminal, run `vicr`, write your Crystal program, save and exit. 31 | 32 | ### Options 33 | 34 | Vicr is able to load file content for you to start playing with Crystal code straight away. 35 | For example: 36 | 37 | ```sh 38 | # loads local file 39 | $ vicr src/vicr/cli.cr 40 | 41 | # loads Github file 42 | $ vicr https://github.com/manastech/crystal/blob/master/samples/2048.cr 43 | 44 | # loads Github gist 45 | $ vicr https://gist.github.com/veelenga/a5b861ccd32ff559b7d2#file-benchmark_test-cr 46 | 47 | # loads CarcIn file 48 | $ vicr https://carc.in/#/r/rlj 49 | 50 | # loads play.crystal-lang.org file 51 | $ vicr https://play.crystal-lang.org/#/r/rlj 52 | 53 | # loads raw file 54 | $ vicr http://example.com/program.cr 55 | ``` 56 | 57 | Use help (`-h` flag) for more information. 58 | 59 | ### Customization 60 | 61 | It is possible to configure Vicr start-up settings using `~/.vicr/init.yaml` configuration file. 62 | You can customize settings and use your favorite editor and even change compiler params: 63 | 64 | ```yml 65 | # ~/.vicr/init.yaml 66 | --- 67 | editor: 68 | executable: nvim 69 | args: 70 | - "--cmd" 71 | - "set paste" 72 | 73 | compiler: 74 | executable: crystal 75 | args_before: 76 | - "run" 77 | - "--no-debug" 78 | ``` 79 | 80 | ## Contributing 81 | 82 | If you feel like you have a good idea to be implemented, please open a discussion. 83 | If you found a defect and enough motivated to fix it, pull requests are welcome. 84 | -------------------------------------------------------------------------------- /bin/vicr: -------------------------------------------------------------------------------- 1 | require "../src/vicr" 2 | 3 | Vicr::Cli.run 4 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | ameba: 4 | git: https://github.com/crystal-ameba/ameba.git 5 | version: 1.0.0 6 | 7 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: vicr 2 | version: 0.5.0 3 | crystal: 0.27.0 4 | 5 | authors: 6 | - Vitalii Elenhaupt 7 | 8 | license: MIT 9 | 10 | development_dependencies: 11 | ameba: 12 | github: crystal-ameba/ameba 13 | version: ~> 1.0 14 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/vicr/service/*" 3 | require "../src/vicr/config/*" 4 | -------------------------------------------------------------------------------- /spec/vicr/config/compiler_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Vicr::Config 4 | describe Compiler do 5 | context "yaml mapping" do 6 | it "maps properties" do 7 | compiler = Compiler.from_yaml({ 8 | executable: "test", 9 | args_before: ["1", "2"], 10 | args_after: ["3"], 11 | }.to_yaml) 12 | 13 | compiler.executable.should eq "test" 14 | compiler.args_before.should eq ["1", "2"] 15 | compiler.args_after.should eq ["3"] 16 | end 17 | 18 | it "allows args_before to be nil" do 19 | compiler = Compiler.from_yaml({ 20 | executable: "test", 21 | args_after: [""], 22 | }.to_yaml) 23 | compiler.args_before.should be nil 24 | end 25 | 26 | it "allows args_after to be nil" do 27 | compiler = Compiler.from_yaml({ 28 | executable: "test", 29 | args_before: [""], 30 | }.to_yaml) 31 | compiler.args_after.should be nil 32 | end 33 | 34 | it "requires executable" do 35 | expect_raises(YAML::ParseException) { 36 | Compiler.from_yaml({args_before: [""]}.to_yaml) 37 | } 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/vicr/config/editor_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Vicr::Config 4 | describe Editor do 5 | context "yaml mapping" do 6 | it "maps properties" do 7 | editor = Editor.from_yaml({ 8 | executable: "test", 9 | args: ["1", "2"], 10 | }.to_yaml) 11 | editor.executable.should eq "test" 12 | editor.args.should eq ["1", "2"] 13 | end 14 | 15 | it "requires executable" do 16 | expect_raises(YAML::ParseException) { 17 | Editor.from_yaml({args: [""]}.to_yaml) 18 | } 19 | end 20 | 21 | it "allows args to be nil" do 22 | editor = Editor.from_yaml({executable: "test"}.to_yaml) 23 | editor.args.should be_nil 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/vicr/config/run_file_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | def test_run_file 4 | tempfile = File.tempfile "run_file" 5 | run_file = Vicr::RunFile.new tempfile.path 6 | begin 7 | yield run_file 8 | rescue e 9 | tempfile.close 10 | raise e 11 | end 12 | end 13 | 14 | module Vicr 15 | describe RunFile do 16 | describe "#path" do 17 | it "returns path to file" do 18 | test_run_file do |run_file| 19 | run_file.path.should_not be_nil 20 | end 21 | end 22 | end 23 | 24 | describe "#delete" do 25 | it "removes file" do 26 | test_run_file do |run_file| 27 | File.exists?(run_file.path).should be_true 28 | run_file.delete 29 | File.exists?(run_file.path).should be_false 30 | end 31 | end 32 | end 33 | 34 | describe "#create_new" do 35 | it "creates new file" do 36 | test_run_file do |run_file| 37 | run_file.delete 38 | File.exists?(run_file.path).should be_false 39 | run_file.create_new 40 | File.exists?(run_file.path).should be_true 41 | end 42 | end 43 | end 44 | 45 | describe "#write" do 46 | it "writes into file" do 47 | test_run_file do |run_file| 48 | run_file.write("test") 49 | run_file.lines.should eq ["test"] 50 | end 51 | end 52 | end 53 | 54 | describe "#lines" do 55 | it "returns empty string when file is empty" do 56 | test_run_file do |run_file| 57 | run_file.lines.should eq [] of String 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/vicr/service/carc_in_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Vicr::Service 4 | describe CarcIn do 5 | describe ".raw" do 6 | it "returns path to raw file on carc.in" do 7 | CarcIn.raw("https://carc.in/#/r/rlc") 8 | .should eq "https://carc.in/runs/rlc.cr" 9 | end 10 | 11 | it "returns nil if this is not carc.in path" do 12 | CarcIn.raw("https://github.com/test/test.cr").should be_nil 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/vicr/service/github_spec.cr: -------------------------------------------------------------------------------- 1 | require "../../spec_helper" 2 | 3 | module Vicr::Service 4 | describe Github do 5 | describe ".raw" do 6 | it "returns path to raw file on github" do 7 | Github.raw("https://github.com/user/repo/blob/master/file.cr") 8 | .should eq "https://raw.githubusercontent.com/user/repo/master/file.cr" 9 | end 10 | 11 | it "returns nil if this is not github path" do 12 | Github.raw("https://example.com/user/repo/blob/master/file.cr") 13 | .should eq nil 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/vicr_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | -------------------------------------------------------------------------------- /src/vicr.cr: -------------------------------------------------------------------------------- 1 | require "./vicr/*" 2 | -------------------------------------------------------------------------------- /src/vicr/buffer.cr: -------------------------------------------------------------------------------- 1 | require "./service/*" 2 | 3 | module Vicr 4 | module Buffer 5 | extend self 6 | include Service 7 | 8 | def load(path : String) 9 | buffer = File.read path if File.exists? path 10 | buffer ||= load_http_file path if path.starts_with? "http" 11 | buffer || raise "Unable to resolve '#{path}'" 12 | end 13 | 14 | private def load_http_file(path : String) 15 | raw = Github.raw(path) || Github.gist_raw(path, "Crystal") || CarcIn.raw(path) || path 16 | resp = HTTP::Client.get raw 17 | resp.status_code == 200 ? resp.body : nil 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /src/vicr/cli.cr: -------------------------------------------------------------------------------- 1 | require "option_parser" 2 | require "http/client" 3 | require "json" 4 | require "./service/*" 5 | 6 | module Vicr 7 | class Cli 8 | def self.run(args = ARGV) 9 | Cli.new.run(args) 10 | end 11 | 12 | def run(args) 13 | path, debug, clear = nil, false, false 14 | 15 | OptionParser.parse(args) do |parser| 16 | parser.banner = <<-USAGE 17 | Usage: vicr [switches] [arguments] or 18 | vicr [path to file to load] 19 | USAGE 20 | parser.on("-f PATH", "--file PATH", "File to load") { |p| path = p } 21 | parser.on("-c", "--clear", "Clear current buffer") { clear = true } 22 | parser.on("-d", "--debug", "Debug output") { debug = true } 23 | parser.on("-v", "--version", "Show version") { puts VERSION; exit } 24 | parser.on("-h", "--help", "Show this help") { puts parser; exit } 25 | parser.unknown_args do |unknown_args| 26 | path ||= unknown_args.first if unknown_args.size == 1 27 | end 28 | end 29 | 30 | opts = {} of Symbol => String 31 | opts[:buffer] = Buffer.load(path.not_nil!) if path 32 | opts[:buffer] = "" if clear 33 | 34 | Runner.new(opts).start 35 | rescue e 36 | e.inspect_with_backtrace STDERR if debug 37 | puts e.message.colorize :red 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /src/vicr/config/compiler.cr: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module Vicr::Config 4 | struct Compiler 5 | include YAML::Serializable 6 | 7 | property executable : String 8 | property args_before : Array(String)? 9 | property args_after : Array(String)? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /src/vicr/config/editor.cr: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | 3 | module Vicr::Config 4 | struct Editor 5 | include YAML::Serializable 6 | 7 | property executable : String 8 | property args : Array(String)? 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /src/vicr/config/run_file.cr: -------------------------------------------------------------------------------- 1 | module Vicr 2 | class RunFile 3 | getter! :path 4 | 5 | def initialize(path, buffer = nil) 6 | @path = File.expand_path path 7 | buffer ? write buffer : File.exists?(@path) || create_new 8 | end 9 | 10 | def delete 11 | File.delete @path if File.exists? @path 12 | end 13 | 14 | def create_new 15 | Dir.mkdir_p Config::Settings::DIR 16 | File.new @path, "w" 17 | end 18 | 19 | def write(buffer : String) 20 | File.write @path, buffer 21 | end 22 | 23 | def lines 24 | File.read_lines @path 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /src/vicr/config/settings.cr: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "../config/*" 3 | 4 | module Vicr::Config 5 | class Settings 6 | DIR = File.expand_path("~/.vicr", home: true) 7 | 8 | include YAML::Serializable 9 | 10 | property editor : Editor = editor_default 11 | property compiler : Compiler = compiler_default 12 | 13 | def run_file 14 | DIR + "/run.cr" 15 | end 16 | 17 | def self.load 18 | settings = File.exists?(settings_filepath) ? File.read(settings_filepath) : "{}" 19 | Settings.from_yaml settings 20 | end 21 | 22 | def self.settings_filepath 23 | DIR + "/init.yaml" 24 | end 25 | 26 | def self.editor_default 27 | Editor.from_yaml({executable: "vim"}.to_yaml) 28 | end 29 | 30 | def self.compiler_default 31 | Compiler.from_yaml({executable: "crystal", args_before: %w(run --no-debug)}.to_yaml) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /src/vicr/runner.cr: -------------------------------------------------------------------------------- 1 | require "colorize" 2 | require "./config/*" 3 | 4 | module Vicr 5 | class Runner 6 | include Config 7 | 8 | @editor : Editor 9 | @compiler : Compiler 10 | 11 | def initialize(opts) 12 | settings = Settings.load 13 | 14 | @editor = settings.editor 15 | @compiler = settings.compiler 16 | @run_file = RunFile.new settings.run_file, opts[:buffer]? 17 | 18 | @editor_args = Array(String).new 19 | @editor_args.concat @editor.args.not_nil! if @editor.args 20 | @editor_args << @run_file.path 21 | 22 | @compiler_args = Array(String).new 23 | @compiler_args.concat @compiler.args_before.not_nil! if @compiler.args_before 24 | @compiler_args << @run_file.path 25 | @compiler_args.concat @compiler.args_after.not_nil! if @compiler.args_after 26 | end 27 | 28 | def start 29 | edit; run 30 | loop { act next_action } 31 | end 32 | 33 | def act(action) 34 | case action 35 | when :run 36 | run 37 | when :edit 38 | edit; run 39 | when :new 40 | @run_file.create_new 41 | edit; run 42 | when :quit 43 | exit 44 | end 45 | end 46 | 47 | def next_action 48 | puts "(r)un (e)dit (n)ew (q)uit".colorize :yellow 49 | print ">> ".colorize(:red) 50 | 51 | action = :unknown 52 | while action == :unknown 53 | action = STDIN.raw do |io| 54 | case io.gets 1 55 | when "r", "\r" 56 | :run 57 | when "e", " " 58 | :edit 59 | when "n" 60 | :new 61 | when "\e", "\u{3}", "\u{4}", "q", "Q" 62 | :quit 63 | else 64 | :unknown 65 | end 66 | end 67 | end 68 | puts action.colorize :green 69 | action 70 | end 71 | 72 | def edit 73 | system(@editor.executable, @editor_args) || 74 | raise "Unable to edit '#{@run_file.path}' using '#{@editor.executable}'" 75 | end 76 | 77 | def run 78 | system(@compiler.executable, @compiler_args) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /src/vicr/service/carc_in.cr: -------------------------------------------------------------------------------- 1 | module Vicr::Service::CarcIn 2 | extend self 3 | 4 | def raw(path : String) 5 | if md = path.match /(carc\.in|play\.crystal-lang\.org)\/.*\/.*\/(.*)/ 6 | "https://carc.in/runs/#{md[2]}.cr" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/vicr/service/github.cr: -------------------------------------------------------------------------------- 1 | require "http/client" 2 | require "json" 3 | require "levenshtein" 4 | 5 | module Vicr::Service::Github 6 | extend self 7 | 8 | def raw(path : String) 9 | if md = path.match /github\.com\/(.*)\/(.*)\/blob\/(.*)\/(.*)/ 10 | user, repo, branch, file = md[1], md[2], md[3], md[4] 11 | "https://raw.githubusercontent.com/#{user}/#{repo}/#{branch}/#{file}" 12 | end 13 | end 14 | 15 | def gist_raw(path : String, language : String = nil) 16 | path, filename = path.split("#file-") + [nil] 17 | if path && (md = path.match /gist\.github\.com\/.*\/(.*)/) 18 | files = gist_files md[1] 19 | if filename 20 | files.map { |file| {url: file[:raw_url], dist: Levenshtein.distance file[:filename], filename} } 21 | .sort_by!(&.[:dist].to_i) 22 | .first[:url].as String 23 | else 24 | (files.find { |file| file[:language] == language } || files.first)[:raw_url] 25 | end 26 | end 27 | end 28 | 29 | private def gist(id : String) 30 | resp = HTTP::Client.get "https://api.github.com/gists/" + id 31 | return JSON.parse(resp.body) if resp.status_code == 200 32 | raise ArgumentError.new "Gist with id '#{id}' not found" 33 | end 34 | 35 | private def gist_files(id : String) 36 | gist_files = [] of NamedTuple(filename: String, language: String, raw_url: String) 37 | gist(id)["files"].as_h.each do |file, value| 38 | gist_files << { 39 | filename: file.to_s, 40 | language: value["language"].to_s, 41 | raw_url: value["raw_url"].to_s, 42 | } 43 | end 44 | gist_files 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /src/vicr/version.cr: -------------------------------------------------------------------------------- 1 | module Vicr 2 | VERSION = "0.5.0" 3 | end 4 | --------------------------------------------------------------------------------