ruco 0.0.56 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source :rubygems
3
3
  gem 'clipboard', '>=0.9.4'
4
4
 
5
5
  group :dev do # not development <-> would add unneeded development dependencies in gemspec
6
+ gem 'ffi', :platform => [:mingw]
6
7
  gem 'rake'
7
8
  gem 'rspec', '~>2'
8
9
  gem 'jeweler'
data/Gemfile.lock CHANGED
@@ -25,6 +25,7 @@ PLATFORMS
25
25
 
26
26
  DEPENDENCIES
27
27
  clipboard (>= 0.9.4)
28
+ ffi
28
29
  jeweler
29
30
  rake
30
31
  rspec (~> 2)
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ end
8
8
 
9
9
  task :run do
10
10
  file = 'spec/temp.txt'
11
- File.open(file, 'w'){|f|f.write("12345\n1234\n#{'abcdefg'*20}\n123")}
11
+ File.open(file, 'wb'){|f|f.write("12345\n1234\n#{'abcdefg'*20}\n123")}
12
12
  exec "./bin/ruco #{file}"
13
13
  end
14
14
 
@@ -19,6 +19,25 @@ task :try do
19
19
  Curses.getch
20
20
  end
21
21
 
22
+ task :try_color do
23
+ require 'curses'
24
+ if Curses::has_colors?
25
+ Curses::start_color
26
+ # initialize every color we want to use
27
+ # id, foreground, background
28
+ Curses::init_pair( Curses::COLOR_BLACK, Curses::COLOR_BLACK, Curses::COLOR_BLACK )
29
+ Curses::init_pair( Curses::COLOR_RED, Curses::COLOR_RED, Curses::COLOR_BLACK )
30
+ Curses::init_pair( Curses::COLOR_GREEN, Curses::COLOR_GREEN, Curses::COLOR_BLACK )
31
+ end
32
+
33
+ Curses.setpos(0,0)
34
+ Curses.attrset(Curses.color_pair(Curses::COLOR_RED)) # fetch color pair with the id xxx
35
+ Curses.addstr("xxxxxxxx\nyyyyyyy");
36
+ Curses.attrset(Curses.color_pair(Curses::COLOR_GREEN))
37
+ Curses.addstr("xxxxxxxx\nyyyyyyy");
38
+ Curses.getch
39
+ end
40
+
22
41
  task :key do
23
42
  require 'curses'
24
43
 
data/Readme.md CHANGED
@@ -1,4 +1,4 @@
1
- Simple, extendable, test-driven commandline editor written in ruby.
1
+ Simple, extendable, test-driven commandline editor written in ruby, for Linux/Mac/Windows.
2
2
 
3
3
  Features:
4
4
 
@@ -81,6 +81,7 @@ TIPS
81
81
 
82
82
  TODO
83
83
  =====
84
+ - slow when opening file with 10,000+ short lines -> investigate
84
85
  - check writable status every x seconds (e.g. in background) -> faster while typing
85
86
  - search help e.g. 'Nothing found' '#4 of 6 hits' 'no more hits, start from beginning ?'
86
87
  - highlight current work when reopening search (so typing replaces it)
@@ -96,8 +97,13 @@ TODO
96
97
  - add auto-confirm to 'replace?' dialog -> type s == skip, no enter needed
97
98
  - 1.8: unicode support <-> already finished but unusable due to Curses (see encoding branch)
98
99
 
99
- Author
100
- ======
100
+ Authors
101
+ =======
102
+
103
+ ### [Contributors](http://github.com/grosser/ruco/contributors)
104
+ - [AJ Palkovic](https://github.com/ajpalkovic)
105
+
106
+
101
107
  [Michael Grosser](http://grosser.it)<br/>
102
108
  grosser.michael@gmail.com<br/>
103
109
  Hereby placed under public domain, do what you want, just do not hold me accountable...
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.56
1
+ 0.1.0
data/bin/ruco CHANGED
@@ -20,6 +20,7 @@ Usage:
20
20
  Options:
21
21
  BANNER
22
22
  opts.on("-c", "--convert-tabs","Convert tabs to spaces") { options[:convert_tabs] = true }
23
+ opts.on("-u", "--undo-stack-size SIZE","Maximum size of the undo stack. 0 allows for a complete undo stack.") {|size| options[:undo_stack_size] = size.to_i }
23
24
  opts.on("--debug-cache","Show caching in action") { options[:debug_cache] = true }
24
25
  opts.on("--debug-keys", "Show pressed keys") { options[:debug_keys] = true }
25
26
  opts.on("-v", "--version","Show Version"){
@@ -55,6 +56,7 @@ def init_screen
55
56
  begin
56
57
  yield
57
58
  ensure
59
+ Curses.clear # needed to clear the menu/status bar on windows
58
60
  Curses.close_screen
59
61
  end
60
62
  end
@@ -107,7 +109,7 @@ def debug_key(key)
107
109
  end
108
110
 
109
111
  def log(stuff)
110
- File.open('ruco.log','a'){|f| f.puts stuff }
112
+ File.open('ruco.log','ab'){|f| f.puts stuff }
111
113
  end
112
114
 
113
115
  @options = parse_options
@@ -116,6 +118,7 @@ require 'ruco'
116
118
 
117
119
  app = Ruco::Application.new(ARGV[0],
118
120
  :convert_tabs => @options[:convert_tabs],
121
+ :undo_stack_size => @options[:undo_stack_size],
119
122
  :lines => Curses.stdscr.maxy, :columns => Curses.stdscr.maxx
120
123
  )
121
124
 
@@ -155,7 +155,7 @@ module Ruco
155
155
 
156
156
  action :quit do
157
157
  if editor.modified?
158
- ask("Loose changes? Enter=Yes Esc=Cancel") do
158
+ ask("Lose changes? Enter=Yes Esc=Cancel") do
159
159
  editor.store_session
160
160
  :quit
161
161
  end
@@ -253,7 +253,7 @@ module Ruco
253
253
  @status_lines = 1
254
254
 
255
255
  editor_options = @options.slice(
256
- :columns, :convert_tabs, :convert_newlines
256
+ :columns, :convert_tabs, :convert_newlines, :undo_stack_size
257
257
  ).merge(
258
258
  :window => @options.nested(:window),
259
259
  :history => @options.nested(:history),
@@ -1,5 +1,17 @@
1
1
  class File
2
2
  def self.write(to, content)
3
- File.open(to, 'w'){|f| f.write(content) }
3
+ File.open(to, 'wb'){|f| f.write(content) }
4
+ end
5
+
6
+ # Open files in binary mode. On linux this is ignored by ruby.
7
+ # On Windows ruby open files in text mode by default, so it replaces \r with \n,
8
+ # so the specs fail. If files are opened in binary mode, which is the only mode
9
+ # on linux, it does not replace the newlines. This thread has slightly more information:
10
+ # http://groups.google.com/group/rubyinstaller/browse_thread/thread/c7fbe346831e58cc
11
+ def self.binary_read(file)
12
+ io = File.open(file, 'rb')
13
+ content = io.read
14
+ io.close
15
+ content
4
16
  end
5
17
  end
data/lib/ruco/editor.rb CHANGED
@@ -2,10 +2,11 @@ module Ruco
2
2
  class Editor
3
3
  attr_reader :file
4
4
  attr_reader :text_area
5
+ attr_reader :history
5
6
  private :text_area
6
7
  delegate :view, :style_map, :cursor, :position,
7
8
  :insert, :indent, :unindent, :delete, :delete_line,
8
- :redo, :undo,
9
+ :redo, :undo, :save_state,
9
10
  :selecting, :selection, :text_in_selection, :reset,
10
11
  :move, :resize, :move_line,
11
12
  :to => :text_area
@@ -19,7 +20,7 @@ module Ruco
19
20
  raise "#{@file} is larger than 1MB, did you really want to open that with Ruco?"
20
21
  end
21
22
 
22
- content = (File.exist?(@file) ? File.read(@file) : '')
23
+ content = (File.exist?(@file) ? File.binary_read(@file) : '')
23
24
  content.tabs_to_spaces! if @options[:convert_tabs]
24
25
 
25
26
  # cleanup newline formats
@@ -29,6 +30,7 @@ module Ruco
29
30
 
30
31
  @saved_content = content
31
32
  @text_area = EditorArea.new(content, @options)
33
+ @history = @text_area.history
32
34
  restore_session
33
35
  end
34
36
 
@@ -51,7 +53,7 @@ module Ruco
51
53
  lines << '' if @options[:blank_line_before_eof_on_save] and lines.last.to_s !~ /^\s*$/
52
54
  content = lines * @newline
53
55
 
54
- File.open(@file,'w'){|f| f.write(content) }
56
+ File.open(@file,'wb'){|f| f.write(content) }
55
57
  @saved_content = content.gsub(/\r?\n/, "\n")
56
58
 
57
59
  true
@@ -1,9 +1,11 @@
1
1
  module Ruco
2
2
  class Editor
3
3
  module History
4
+ attr_reader :history
5
+
4
6
  def initialize(content, options)
5
7
  super(content, options)
6
- @history = Ruco::History.new((options[:history]||{}).reverse_merge(:state => state, :track => [:content], :entries => 100, :timeout => 2))
8
+ @history = Ruco::History.new((options[:history]||{}).reverse_merge(:state => state, :track => [:content], :entries => options[:undo_stack_size], :timeout => 2))
7
9
  end
8
10
 
9
11
  def undo
@@ -15,7 +17,7 @@ module Ruco
15
17
  @history.redo
16
18
  self.state = @history.state
17
19
  end
18
-
20
+
19
21
  def view
20
22
  @history.add(state)
21
23
  super
File without changes
@@ -1,4 +1,5 @@
1
1
  require "digest/md5"
2
+ require "fileutils"
2
3
 
3
4
  module Ruco
4
5
  class FileStore
@@ -8,21 +9,27 @@ module Ruco
8
9
  end
9
10
 
10
11
  def set(key, value)
11
- `mkdir -p #{@folder}` unless File.exist? @folder
12
+ FileUtils.mkdir_p @folder unless File.exist? @folder
12
13
  File.write(file(key), serialize(value))
13
14
  cleanup
14
15
  end
15
16
 
16
17
  def get(key)
17
18
  file = file(key)
18
- deserialize File.read(file) if File.exist?(file)
19
+ deserialize File.binary_read(file) if File.exist?(file)
19
20
  end
20
21
 
21
22
  private
22
23
 
24
+ def entries
25
+ (Dir.entries(@folder) - ['.','..']).
26
+ map{|entry| File.join(@folder, entry) }.
27
+ sort_by{|file| File.mtime(file) }
28
+ end
29
+
23
30
  def cleanup
24
- delete = `ls -t #{@folder}`.split("\n")[@options[:keep]..-1] || []
25
- delete.each{|f| File.delete("#{@folder}/#{f}") }
31
+ delete = entries[0...-@options[:keep]] || []
32
+ delete.each{|f| File.delete(f) }
26
33
  end
27
34
 
28
35
  def file(key)
@@ -37,4 +44,4 @@ module Ruco
37
44
  Marshal.load(value)
38
45
  end
39
46
  end
40
- end
47
+ end
data/lib/ruco/history.rb CHANGED
@@ -1,41 +1,65 @@
1
1
  module Ruco
2
2
  class History
3
3
  attr_accessor :timeout
4
+ attr_reader :stack, :position
4
5
 
5
6
  def initialize(options)
6
7
  @options = options
7
- @stack = [@options.delete(:state)]
8
+ @options[:entries] ||= 100
8
9
  @timeout = options.delete(:timeout) || 0
9
- timeout!
10
+
11
+ @stack = [{:mutable => false, :created_at => 0, :type => :initial, :state => @options.delete(:state)}]
10
12
  @position = 0
11
13
  end
12
14
 
13
15
  def state
14
- @stack[@position]
16
+ @stack[@position][:state]
15
17
  end
16
18
 
17
19
  def add(state)
18
20
  return unless tracked_field_changes?(state)
19
21
  remove_undone_states
20
- if merge_timeout?
22
+ unless merge? state
23
+ # can no longer modify previous states
24
+ @stack[@position][:mutable] = false
25
+
26
+ state_type = type(state)
21
27
  @position += 1
22
- @last_merge = Time.now.to_f
28
+ @stack[@position] = {:mutable => true, :type => state_type, :created_at => Time.now.to_f}
23
29
  end
24
- @stack[@position] = state
30
+ @stack[@position][:state] = state
25
31
  limit_stack
26
32
  end
27
33
 
28
34
  def undo
29
- timeout!
30
35
  @position = [@position - 1, 0].max
31
36
  end
32
37
 
33
38
  def redo
34
- timeout!
35
39
  @position = [@position + 1, @stack.size - 1].min
36
40
  end
37
41
 
38
42
  private
43
+ def type(state)
44
+ @options[:track].each do |field|
45
+ if state[field].is_a?(String) && @stack[@position][:state][field].is_a?(String)
46
+ diff = state[field].length - @stack[@position][:state][field].length
47
+ if diff > 0
48
+ return :insert
49
+ elsif diff < 0
50
+ return :delete
51
+ end
52
+ end
53
+ end
54
+ nil
55
+ end
56
+
57
+ def merge?(state)
58
+ top = @stack[@position]
59
+ top[:mutable] &&
60
+ top[:type] == type(state) &&
61
+ top[:created_at]+@timeout > Time.now.to_f
62
+ end
39
63
 
40
64
  def remove_undone_states
41
65
  @stack.slice!(@position + 1, 9999999)
@@ -48,18 +72,11 @@ module Ruco
48
72
  end
49
73
 
50
74
  def limit_stack
75
+ return if @options[:entries] == 0
51
76
  to_remove = @stack.size - @options[:entries]
52
77
  return if to_remove < 1
53
78
  @stack.slice!(0, to_remove)
54
79
  @position -= to_remove
55
80
  end
56
-
57
- def timeout!
58
- @last_merge = 0
59
- end
60
-
61
- def merge_timeout?
62
- (Time.now.to_f - @last_merge) > @timeout
63
- end
64
81
  end
65
82
  end
data/ruco.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{ruco}
8
- s.version = "0.0.56"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Michael Grosser"]
12
- s.date = %q{2011-05-11}
12
+ s.date = %q{2011-05-20}
13
13
  s.default_executable = %q{ruco}
14
14
  s.email = %q{michael@grosser.it}
15
15
  s.executables = ["ruco"]
@@ -15,11 +15,11 @@ describe Ruco::Application do
15
15
  end
16
16
 
17
17
  def write(content)
18
- File.open(@file,'w'){|f| f.write(content) }
18
+ File.open(@file,'wb'){|f| f.write(content) }
19
19
  end
20
20
 
21
21
  def read
22
- File.read(@file)
22
+ File.binary_read(@file)
23
23
  end
24
24
 
25
25
  def editor_part(view)
@@ -110,7 +110,7 @@ describe Ruco::Application do
110
110
  it "asks before closing changed file -- escape == no" do
111
111
  app.key('a')
112
112
  app.key(:"Ctrl+w")
113
- app.view.split("\n").last.should include("Loose changes")
113
+ app.view.split("\n").last.should include("Lose changes")
114
114
  app.key(:escape).should_not == :quit
115
115
  app.key("\n").should_not == :quit
116
116
  end
@@ -118,7 +118,7 @@ describe Ruco::Application do
118
118
  it "asks before closing changed file -- enter == yes" do
119
119
  app.key('a')
120
120
  app.key(:"Ctrl+w")
121
- app.view.split("\n").last.should include("Loose changes")
121
+ app.view.split("\n").last.should include("Lose changes")
122
122
  app.key(:enter).should == :quit
123
123
  end
124
124
  end
@@ -2,11 +2,11 @@ require File.expand_path('spec/spec_helper')
2
2
 
3
3
  describe Ruco::Editor do
4
4
  def write(content)
5
- File.open(@file,'w'){|f| f.write(content) }
5
+ File.open(@file,'wb'){|f| f.write(content) }
6
6
  end
7
7
 
8
8
  def read
9
- File.read(@file)
9
+ File.binary_read(@file)
10
10
  end
11
11
 
12
12
  let(:editor){
@@ -753,6 +753,25 @@ describe Ruco::Editor do
753
753
  end
754
754
 
755
755
  describe 'history' do
756
+ it "does not overwrite the initial state" do
757
+ write("a")
758
+ editor.insert("b")
759
+ editor.view # trigger save point
760
+ stack = editor.history.stack
761
+ stack.length.should == 2
762
+ stack[0][:state][:content].should == "a"
763
+ stack[1][:state][:content].should == "ba"
764
+
765
+ editor.undo
766
+ editor.history.position.should == 0
767
+
768
+ editor.insert("c")
769
+ editor.view # trigger save point
770
+ stack.length.should == 2
771
+ stack[0][:state][:content].should == "a"
772
+ stack[1][:state][:content].should == "ca"
773
+ end
774
+
756
775
  it "can undo an action" do
757
776
  write("a")
758
777
  editor.insert("b")
@@ -768,6 +787,7 @@ describe Ruco::Editor do
768
787
 
769
788
  it "removes selection on undo" do
770
789
  editor.insert('a')
790
+ editor.view # trigger save point
771
791
  editor.selecting{move(:to, 1,1)}
772
792
  editor.selection.should_not == nil
773
793
  editor.view # trigger save point
@@ -777,7 +797,6 @@ describe Ruco::Editor do
777
797
 
778
798
  it "sets modified on undo" do
779
799
  editor.insert('a')
780
- editor.view # trigger save point
781
800
  editor.save
782
801
  editor.modified?.should == false
783
802
  editor.undo
@@ -790,14 +809,14 @@ describe Ruco::Editor do
790
809
  write('xxx')
791
810
  editor.insert('a')
792
811
  editor.save.should == true
793
- File.read(@file).should == 'axxx'
812
+ File.binary_read(@file).should == 'axxx'
794
813
  end
795
814
 
796
815
  it 'creates the file' do
797
816
  `rm #{@file}`
798
817
  editor.insert('a')
799
818
  editor.save.should == true
800
- File.read(@file).should == 'a'
819
+ File.binary_read(@file).should == 'a'
801
820
  end
802
821
 
803
822
  it 'does not crash when it cannot save a file' do
@@ -1,6 +1,10 @@
1
1
  require File.expand_path('spec/spec_helper')
2
2
 
3
3
  describe Ruco::FileStore do
4
+ def mark_all_as_old
5
+ store.send(:entries).each{|e| File.utime(1,1,e) }
6
+ end
7
+
4
8
  before do
5
9
  @folder = 'spec/sessions'
6
10
  `rm -rf #{@folder}`
@@ -21,23 +25,10 @@ describe Ruco::FileStore do
21
25
  store.set('xxx', 1)
22
26
  store.set('yyy', 1)
23
27
  store.set('zzz', 1)
24
- store.set('aaa', 1)
25
- store.get('xxx').should == nil
26
- end
27
-
28
- it "drops least recently used key" do
29
- store.set('xxx', 1)
30
- sleep(0.1)
31
- store.set('yyy', 1)
32
- sleep(0.1)
33
- store.set('xxx', 1)
34
- sleep(0.1)
35
- store.set('zzz', 1)
36
- sleep(0.1)
37
- store.set('aaa', 1)
38
- sleep(0.1)
39
- store.get('xxx').should == 1
40
- store.get('yyy').should == nil
28
+ mark_all_as_old
29
+ store.set('aaa', 2)
30
+ store.get('aaa').should == 2
31
+ ['xxx','yyy','zzz'].map{|f| store.get(f) }.should =~ [1,1,nil]
41
32
  end
42
33
 
43
34
  it "does not drop if used multiple times" do
@@ -45,6 +36,7 @@ describe Ruco::FileStore do
45
36
  store.set('yyy', 1)
46
37
  store.set('zzz', 1)
47
38
  store.set('zzz', 1)
39
+ mark_all_as_old
48
40
  store.set('zzz', 1)
49
41
  store.set('zzz', 1)
50
42
  store.get('xxx').should == 1
@@ -74,6 +74,20 @@ describe Ruco::History do
74
74
  history.undo
75
75
  history.state.should == {:x => 3}
76
76
  end
77
+
78
+ describe 'with strings' do
79
+ let(:history){ Ruco::History.new(:state => {:x => 'a'}, :track => [:x], :timeout => 0.1) }
80
+
81
+ it "triggers a new state on insertion and deletion" do
82
+ %w{ab abc ab a a}.each{|state| history.add(:x => state)}
83
+
84
+ history.undo
85
+ history.state.should == {:x => "abc"}
86
+
87
+ history.undo
88
+ history.state.should == {:x => "a"}
89
+ end
90
+ end
77
91
 
78
92
  describe 'with timeout' do
79
93
  let(:history){ Ruco::History.new(:state => {:x => 1}, :track => [:x], :entries => 3, :timeout => 0.1) }
@@ -114,4 +128,16 @@ describe Ruco::History do
114
128
  history.state.should == {:x => 3}
115
129
  end
116
130
  end
131
+
132
+ describe 'with no entry limit' do
133
+ let(:history){ Ruco::History.new(:state => {:x => 1}, :track => [:x], :entries => 0, :timeout => 0) }
134
+
135
+ it "should track unlimited states" do
136
+ 200.times do |i|
137
+ history.add(:x => i+5)
138
+ end
139
+ history.stack.length.should == 201
140
+ end
141
+ end
142
+
117
143
  end
@@ -29,8 +29,11 @@ describe Ruco::StatusBar do
29
29
  end
30
30
 
31
31
  it "indicates not writable" do
32
- editor.stub!(:file).and_return '/etc/sudoers'
33
- bar.view.should include('!')
32
+ # this test will always fail on Windows with cygwin because of how cygwin sets up permissions
33
+ unless RUBY_PLATFORM =~ /mingw/
34
+ editor.stub!(:file).and_return '/etc/sudoers'
35
+ bar.view.should include('!')
36
+ end
34
37
  end
35
38
 
36
39
  it "shows line and column and right side" do
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruco
3
3
  version: !ruby/object:Gem::Version
4
- hash: 111
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 56
10
- version: 0.0.56
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Michael Grosser
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-11 00:00:00 +02:00
18
+ date: 2011-05-20 00:00:00 +02:00
19
19
  default_executable: ruco
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency