fled 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +230 -0
- data/bin/fled +152 -0
- data/lib/dtc/utils/dsldsl.rb +259 -0
- data/lib/dtc/utils/exec.rb +134 -0
- data/lib/dtc/utils/file_visitor.rb +78 -0
- data/lib/dtc/utils/interactive_edit.rb +81 -0
- data/lib/dtc/utils/mini_select.rb +177 -0
- data/lib/dtc/utils.rb +9 -0
- data/lib/fled/file_listing.rb +260 -0
- data/lib/fled.rb +38 -0
- data/tests/helper.rb +90 -0
- data/tests/readme.rb +256 -0
- data/tests/test_operations.rb +229 -0
- metadata +59 -0
@@ -0,0 +1,260 @@
|
|
1
|
+
module FlEd
|
2
|
+
class FileListing
|
3
|
+
def initialize
|
4
|
+
@objects_by_id = {}
|
5
|
+
@objects = []
|
6
|
+
end
|
7
|
+
RELATION_KEYS = [:parent]
|
8
|
+
def dup
|
9
|
+
result = self.class.new
|
10
|
+
each do |object|
|
11
|
+
result.add object[:uid], object.dup
|
12
|
+
end
|
13
|
+
result.each do |object|
|
14
|
+
RELATION_KEYS.each do |relation|
|
15
|
+
next unless foreign = object[relation]
|
16
|
+
unless foreign[:uid]
|
17
|
+
raise RuntimeError, "Cannot duplicate anonymous object and maintain #{relation.inspect} referential"
|
18
|
+
end
|
19
|
+
unless (object[relation] = result[foreign[:uid]])
|
20
|
+
raise RuntimeError, "Cannot duplicate object in #{relation.inspect} referential, no duplicate found in target (?!)"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
result
|
25
|
+
end
|
26
|
+
def each &block ; @objects.each(&block) ; end
|
27
|
+
def count ; @objects.count ; end
|
28
|
+
def add uid, object = {}
|
29
|
+
if uid
|
30
|
+
uid = uid.to_s
|
31
|
+
raise RuntimeError, "UID #{uid.inspect} already declared" if @objects_by_id[uid]
|
32
|
+
@objects_by_id[uid] = object
|
33
|
+
object[:uid] = uid
|
34
|
+
end
|
35
|
+
@objects << object
|
36
|
+
object
|
37
|
+
end
|
38
|
+
def [](uid)
|
39
|
+
if uid.is_a?(String)
|
40
|
+
@objects_by_id[uid]
|
41
|
+
else
|
42
|
+
@objects[uid]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
class FileListing # to and from text format
|
47
|
+
def to_s
|
48
|
+
return "" if @objects.empty?
|
49
|
+
max_width = @objects.map { |o| o[:line] }.map(&:length).max + 10
|
50
|
+
@objects.map { |e| "#{e[:line].ljust(max_width)}:#{e[:uid]}" }.join("\n")
|
51
|
+
end
|
52
|
+
def self.parse listing
|
53
|
+
objects = self.new
|
54
|
+
previous_indent = nil
|
55
|
+
previous = nil
|
56
|
+
stack = []
|
57
|
+
listing.split("\n").each do |line|
|
58
|
+
next if line.strip.empty?
|
59
|
+
raise RuntimeError, "Unparsable line #{line.inspect}" unless line =~ /^((?: )*)(.*?)(?::(\d+))?\r?$/
|
60
|
+
indent = $1
|
61
|
+
name = $2.strip
|
62
|
+
if (dir = name[-1..-1] == "/")
|
63
|
+
name = name[0..-2]
|
64
|
+
end
|
65
|
+
current = objects.add($3, :name => name, :line => "#{$1}#{$2}")
|
66
|
+
current[:dir] = true if dir
|
67
|
+
next if name.strip == "" # Ignore indent when there is no name - element will be deleted, parent isnt used
|
68
|
+
if previous_indent && previous_indent != indent
|
69
|
+
if previous_indent.length < indent.length
|
70
|
+
stack.push(previous)
|
71
|
+
else
|
72
|
+
stack.pop
|
73
|
+
end
|
74
|
+
end
|
75
|
+
current[:parent] = stack.last if stack.count > 0
|
76
|
+
previous = current
|
77
|
+
previous_indent = indent
|
78
|
+
end
|
79
|
+
objects
|
80
|
+
end
|
81
|
+
end
|
82
|
+
class FileListing # Shell operation list builder
|
83
|
+
def operations_from! source_listing
|
84
|
+
errors = []
|
85
|
+
operations = []
|
86
|
+
pending_renames = []
|
87
|
+
running_source = source_listing.dup
|
88
|
+
self.breadth_first do |target, path|
|
89
|
+
next if path.any? { |o| o[:error] }
|
90
|
+
if target[:name] != "" && !target[:uid]
|
91
|
+
operations << [:mk, self.path_of(target).map { |o| o[:name] }]
|
92
|
+
fake_source = {:name => target[:name]}
|
93
|
+
fake_source[:parent] = running_source[path.last[:source][:uid]] unless path.empty?
|
94
|
+
target_uid = "new_#{running_source.count}"
|
95
|
+
target_uid += "_" while @objects_by_id[target_uid] || running_source[target_uid]
|
96
|
+
target[:uid] = target_uid
|
97
|
+
@objects_by_id[target_uid] = target
|
98
|
+
target[:source] = running_source.add(target_uid, fake_source)
|
99
|
+
else
|
100
|
+
source = running_source[target[:uid]]
|
101
|
+
if !(target[:source] = source)
|
102
|
+
target[:error] = true
|
103
|
+
errors += [[:fail, :no_such_uid, target]]
|
104
|
+
next
|
105
|
+
end
|
106
|
+
next if target[:name] == ""
|
107
|
+
if (target[:parent] || {})[:uid] != (source[:parent] ||{})[:uid]
|
108
|
+
existing_names = running_source.children_of((target[:parent] || {})[:uid]).map { |o| o[:name] }
|
109
|
+
new_name = target[:name]
|
110
|
+
new_name += ".tmp" while existing_names.any? { |n| n.casecmp(new_name) == 0 }
|
111
|
+
if new_name != target[:name]
|
112
|
+
pending_renames << [:renamed, target, target[:name]]
|
113
|
+
end
|
114
|
+
source_path = running_source.path_of(source).map { |o| o[:name] }
|
115
|
+
target[:name] = source[:name] = new_name
|
116
|
+
operations << [:moved,
|
117
|
+
source_path,
|
118
|
+
self.path_of(target).map { |o| o[:name] }
|
119
|
+
]
|
120
|
+
if target[:parent]
|
121
|
+
source[:parent] = running_source[target[:parent][:uid]]
|
122
|
+
else
|
123
|
+
source.delete(:parent)
|
124
|
+
end
|
125
|
+
elsif target[:name] != source[:name]
|
126
|
+
source_path = running_source.path_of(source).map { |o| o[:name] }
|
127
|
+
existing_names = running_source.children_of((target[:parent] || {})[:uid]).map { |o| o[:name] }
|
128
|
+
new_name = target[:name]
|
129
|
+
new_name += ".tmp" while existing_names.any? { |n| n.casecmp(new_name) == 0 }
|
130
|
+
if new_name != target[:name]
|
131
|
+
pending_renames << [:renamed, target, target[:name]]
|
132
|
+
end
|
133
|
+
target[:name] = source[:name] = new_name
|
134
|
+
operations << [:renamed,
|
135
|
+
source_path,
|
136
|
+
target[:name]
|
137
|
+
]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
pending_renames.each do |op|
|
142
|
+
target = op[1]
|
143
|
+
new_name = op[2]
|
144
|
+
existing_names = running_source.children_of((target[:parent] || {})[:uid]).map { |o| o[:name] }
|
145
|
+
if existing_names.any? { |n| n.casecmp(new_name) == 0 }
|
146
|
+
errors += [[:warn, :would_overwrite, target, new_name]]
|
147
|
+
else
|
148
|
+
operations << [:renamed,
|
149
|
+
running_source.path_of(target).map { |o| o[:name] },
|
150
|
+
new_name
|
151
|
+
]
|
152
|
+
target[:name] = target[:source][:name] = new_name
|
153
|
+
end
|
154
|
+
end
|
155
|
+
self.depth_first do |target, path|
|
156
|
+
if target[:name] == ""
|
157
|
+
operations << [:rm, running_source.path_of(target[:source]).map { |o| o[:name] }, target[:source]]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
errors + operations
|
161
|
+
end
|
162
|
+
def has_child? parent, child
|
163
|
+
while child = child[:parent]
|
164
|
+
return true if child[:uid] == parent[:uid]
|
165
|
+
end
|
166
|
+
false
|
167
|
+
end
|
168
|
+
def path_name_of object
|
169
|
+
File.join(path_of(object).map { |o| o[:name] })
|
170
|
+
end
|
171
|
+
def path_of object
|
172
|
+
res = []
|
173
|
+
while object
|
174
|
+
res.unshift object
|
175
|
+
object = object[:parent]
|
176
|
+
end
|
177
|
+
res
|
178
|
+
end
|
179
|
+
def children_of parent, &blk
|
180
|
+
unless !parent || parent.is_a?(Hash)
|
181
|
+
parent_object = self[parent]
|
182
|
+
raise RuntimeError, "No parent #{parent.inspect} found" unless parent_object
|
183
|
+
parent = parent_object
|
184
|
+
end
|
185
|
+
@objects.
|
186
|
+
select { |e| (e[:parent].nil? && parent.nil?) || (e[:parent] == parent) }.
|
187
|
+
sort { |a, b| a[:name] <=> b[:name] }.
|
188
|
+
each(&blk)
|
189
|
+
end
|
190
|
+
def depth_first parent = nil, path = [], &block
|
191
|
+
children_of parent do |child|
|
192
|
+
depth_first(child, path + [child], &block)
|
193
|
+
yield child, path
|
194
|
+
end
|
195
|
+
end
|
196
|
+
def breadth_first parent = nil, path = [], &block
|
197
|
+
to_browse = []
|
198
|
+
children_of parent do |child|
|
199
|
+
yield child, path
|
200
|
+
to_browse += [child]
|
201
|
+
end
|
202
|
+
to_browse.each { |child| breadth_first child, path + [child], &block }
|
203
|
+
end
|
204
|
+
end
|
205
|
+
class ListingBuilder < DTC::Utils::FileVisitor
|
206
|
+
attr_accessor :listing
|
207
|
+
def initialize listing = FileListing.new
|
208
|
+
@listing = listing
|
209
|
+
end
|
210
|
+
def add_object path, dir
|
211
|
+
uid = next_uid
|
212
|
+
name = File.basename(path)
|
213
|
+
object = @listing.add(uid,
|
214
|
+
:path => path,
|
215
|
+
:name => File.basename(path),
|
216
|
+
:parent => @object_stack.last,
|
217
|
+
:line => "#{" " * self.depth}#{name}#{dir ? "/" : ""}"
|
218
|
+
)
|
219
|
+
object[:dir] = true if dir
|
220
|
+
object
|
221
|
+
end
|
222
|
+
def enter_folder dir
|
223
|
+
depth = self.depth
|
224
|
+
if self.depth == 0
|
225
|
+
@object_stack = []
|
226
|
+
else
|
227
|
+
@object_stack.push add_object(dir, true)
|
228
|
+
end
|
229
|
+
super
|
230
|
+
end
|
231
|
+
def visit_file name, full_path
|
232
|
+
add_object full_path, false
|
233
|
+
super
|
234
|
+
end
|
235
|
+
def leave_folder
|
236
|
+
@object_stack.pop
|
237
|
+
super
|
238
|
+
end
|
239
|
+
protected
|
240
|
+
def next_uid
|
241
|
+
@listing.count
|
242
|
+
end
|
243
|
+
def source_path source
|
244
|
+
res = []
|
245
|
+
while source
|
246
|
+
res.unshift target[:name]
|
247
|
+
target = target[:parent]
|
248
|
+
end
|
249
|
+
File.join(res)
|
250
|
+
end
|
251
|
+
def target_path target
|
252
|
+
res = []
|
253
|
+
while target
|
254
|
+
res.unshift target[:name]
|
255
|
+
target = target[:parent]
|
256
|
+
end
|
257
|
+
File.join(res)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
data/lib/fled.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'dtc/utils'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module FlEd
|
5
|
+
require 'fled/file_listing'
|
6
|
+
|
7
|
+
VERSION = '0.0.1'
|
8
|
+
|
9
|
+
def self.operation_list_to_bash ops
|
10
|
+
ops = ops.map do |op|
|
11
|
+
case op.first
|
12
|
+
when :mk
|
13
|
+
[:mkdir, File.join(op[1])]
|
14
|
+
when :moved
|
15
|
+
[:mv, File.join(op[1]), File.join(op[2])]
|
16
|
+
when :renamed
|
17
|
+
[:mv, File.join(op[1]), File.join((op[1].empty? ? [] : op[1][0..-2]) + [op[2]])]
|
18
|
+
when :rm
|
19
|
+
[op[2][:dir] ? :rmdir : :rm , File.join(op[1])]
|
20
|
+
else
|
21
|
+
op
|
22
|
+
end
|
23
|
+
end
|
24
|
+
warnings, operations = *ops.partition { |e| e.first == :warn }
|
25
|
+
result = []
|
26
|
+
unless warnings.empty?
|
27
|
+
result = ["# Warning:"]
|
28
|
+
warnings.each do |warning|
|
29
|
+
result += ["# - #{warning[1]}: #{File.join(warning[2][:source][:path], warning[3])}"]
|
30
|
+
end
|
31
|
+
result += ['', 'exit 1 # There are warnings to check first !', '']
|
32
|
+
end
|
33
|
+
result += operations.map do |op|
|
34
|
+
"#{Shellwords.join(op.map(&:to_s))}"
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
data/tests/helper.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__),'../lib')
|
3
|
+
require 'fled'
|
4
|
+
|
5
|
+
class PrintingFileVisitor < DTC::Utils::FileVisitor
|
6
|
+
def enter_folder dir
|
7
|
+
depth = self.depth
|
8
|
+
puts (" " * depth) + (depth > 0 ? File.basename(dir) : dir)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
def visit_file name, full_path
|
12
|
+
puts (" " * depth) + name
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class TestListingBuilder < FlEd::ListingBuilder
|
18
|
+
def next_uid
|
19
|
+
@uid
|
20
|
+
end
|
21
|
+
def enter_folder dir, uid = nil
|
22
|
+
@uid = uid
|
23
|
+
super dir
|
24
|
+
end
|
25
|
+
def visit_file name, uid
|
26
|
+
@uid = uid
|
27
|
+
super name, current_path(name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class TestFS
|
32
|
+
def initialize &block
|
33
|
+
@root = DTC::Utils::DSLDSL::DSLHashWriter.write_static_tree_dsl(&block)
|
34
|
+
end
|
35
|
+
def receive visitor, root = @root
|
36
|
+
root.each_pair do |name, val|
|
37
|
+
next if name == :options
|
38
|
+
if val.is_a?(Array)
|
39
|
+
visitor.visit_file name, val[0]
|
40
|
+
elsif val.is_a?(Hash)
|
41
|
+
if visitor.enter_folder(name, val[:options][0])
|
42
|
+
receive(visitor, val)
|
43
|
+
visitor.leave_folder
|
44
|
+
end
|
45
|
+
else
|
46
|
+
raise RuntimeError, "Unknown value #{val.inspect}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
def new_builder
|
51
|
+
builder = TestListingBuilder.new
|
52
|
+
builder.enter_folder("$")
|
53
|
+
receive builder
|
54
|
+
builder.leave_folder
|
55
|
+
builder
|
56
|
+
end
|
57
|
+
def new_listing
|
58
|
+
new_builder.listing
|
59
|
+
end
|
60
|
+
def operation_list_if_edited_as edited_text
|
61
|
+
listing = new_listing
|
62
|
+
parsed = FlEd::FileListing.parse(edited_text)
|
63
|
+
parsed.operations_from!(listing)
|
64
|
+
end
|
65
|
+
def operations_if_edited_as edited_text
|
66
|
+
operation_list_if_edited_as(edited_text).map do |op|
|
67
|
+
case op.first
|
68
|
+
when :mk
|
69
|
+
[:mkdir, File.join(op[1])]
|
70
|
+
when :moved
|
71
|
+
[:mv, File.join(op[1]), File.join(op[2])]
|
72
|
+
when :renamed
|
73
|
+
[:mv, File.join(op[1]), File.join((op[1].empty? ? [] : op[1][0..-2]) + [op[2]])]
|
74
|
+
when :rm
|
75
|
+
[op[2][:dir] ? :rmdir : :rm , File.join(op[1])]
|
76
|
+
else
|
77
|
+
op
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
def commands_if_edited_as edited_text
|
82
|
+
FlEd::operation_list_to_bash(operation_list_if_edited_as(edited_text))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if __FILE__==$0
|
87
|
+
Dir[File.join(File.dirname(__FILE__),'./test_*.rb')].each do |test_file|
|
88
|
+
require test_file
|
89
|
+
end
|
90
|
+
end
|
data/tests/readme.rb
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
if Kernel.respond_to?(:require_relative)
|
2
|
+
require_relative "helper"
|
3
|
+
else
|
4
|
+
require File.join(File.dirname(__FILE__), 'helper')
|
5
|
+
end
|
6
|
+
|
7
|
+
class MarkdownDumbWriter
|
8
|
+
def initialize
|
9
|
+
@result = []
|
10
|
+
end
|
11
|
+
def ensure_clear exception = nil
|
12
|
+
unless @result.last == ""
|
13
|
+
@result << ""
|
14
|
+
else
|
15
|
+
if exception && @result.length > 1 &&
|
16
|
+
(@result[-2] || "")[0..exception.length - 1] == exception
|
17
|
+
@result.pop
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
def << str ; @result += str.is_a?(Array) ? str : [str] ; end
|
22
|
+
def nl ; @result += [""] ; end
|
23
|
+
def title str, depth = 2, *args
|
24
|
+
ensure_clear "#"
|
25
|
+
self << "#{"#" * depth} #{str}"
|
26
|
+
nl
|
27
|
+
args.each { |a| text a }
|
28
|
+
end
|
29
|
+
def h1 str, *a ; title str, 1, *a ; end
|
30
|
+
def h2 str, *a ; title str, 2, *a ; end
|
31
|
+
def h3 str, *a ; title str, 3, *a ; end
|
32
|
+
def code str, indent = " "
|
33
|
+
ensure_clear
|
34
|
+
self << reindent(str, indent)
|
35
|
+
nl
|
36
|
+
end
|
37
|
+
def reindent str, indent = ""
|
38
|
+
lines = str.split(/\r?\n/).map
|
39
|
+
lines.shift if lines.first.empty?
|
40
|
+
min_spaces = lines.map { |l| l =~ /^( +)/ ? $1.length : nil }.select{ |e| e }.min || 0
|
41
|
+
lines.map { |l| indent + ((min_spaces == 0 ? l : l[min_spaces..-1]) || "") }
|
42
|
+
end
|
43
|
+
def text str ; self << reindent(str, "") ; end
|
44
|
+
def em str ; text "*#{str}*" ; end
|
45
|
+
def li str ;
|
46
|
+
ensure_clear "-"
|
47
|
+
self << reindent(str, "- ")
|
48
|
+
nl
|
49
|
+
end
|
50
|
+
def show_example fs, example
|
51
|
+
code example
|
52
|
+
ops = fs.commands_if_edited_as(example)
|
53
|
+
text "Generates:"
|
54
|
+
if ops.empty?
|
55
|
+
em "No operation"
|
56
|
+
else
|
57
|
+
code ops.join("\n")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
def result ; @result.join("\n") ; end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ExampleDSLWriter < DTC::Utils::DSLDSL::DSLArrayWriter
|
64
|
+
def self.run &blk
|
65
|
+
visitor = self.new()
|
66
|
+
visitor.visit_dsl(&blk)
|
67
|
+
writer = MarkdownDumbWriter.new
|
68
|
+
visitor.each do |method, *args|
|
69
|
+
writer.__send__(method, *args)
|
70
|
+
end
|
71
|
+
writer.result
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
readme = ExampleDSLWriter.run do
|
76
|
+
h1 "FlEd", '`fled` lets you organise your files and folders in your favourite editor'
|
77
|
+
|
78
|
+
h2 "Disclaimer", <<-MD
|
79
|
+
Warning: This is a very dangerous tool. The author recommends you do not
|
80
|
+
use it. The author cannot be held responsible in any case.
|
81
|
+
MD
|
82
|
+
|
83
|
+
h2 "Introduction", <<-MD
|
84
|
+
`fled` enumerates a folder and its files, and generates a text listing.
|
85
|
+
You can then edit that listing in your favourite editor, and save changes.
|
86
|
+
`fled` then reloads those changes, and prints a shell script that would move
|
87
|
+
your files and folders around as-per your edits.
|
88
|
+
|
89
|
+
**You should review that shell script very carefully before running it.**
|
90
|
+
|
91
|
+
MD
|
92
|
+
|
93
|
+
h3 "Philosophy", <<-MD
|
94
|
+
`fled` only generates text, it does not perform any operation directly.
|
95
|
+
|
96
|
+
The design optimises for making the edits very simple. The consequence of
|
97
|
+
this is that very small edits can have large consequences, which makes
|
98
|
+
this a **very dangerous** tool. But so is `rm` and the rest of the shell anyway...
|
99
|
+
MD
|
100
|
+
|
101
|
+
h3 "Caveats", <<-MD
|
102
|
+
`fled` is only aware of files it scanned. It will not warn for overwrites,
|
103
|
+
nor use temporary files in those cases, etc.
|
104
|
+
|
105
|
+
`fled`'s editing model is rather complex and fuzzy. While there are some test
|
106
|
+
cases defined, any help is much appreciated.
|
107
|
+
|
108
|
+
You should be scared when using `fled`.
|
109
|
+
MD
|
110
|
+
|
111
|
+
h3 "Examples"
|
112
|
+
[
|
113
|
+
["Print help text and option list", "fled --help"],
|
114
|
+
["Edit current folder", "fled"],
|
115
|
+
["Edit all files directly in `path` folder", "fled -a path -d 0"],
|
116
|
+
["Save default options", "fled --options > fled.config.yaml"],
|
117
|
+
["Edit current folder using options", "fled --load fled.config.yaml"],
|
118
|
+
["Add options to a command (`mkdir`, `mv`, `rm` or `rmdir`)", "fled | sed 's/^mv/mv -i/'"],
|
119
|
+
].each do |t, c|
|
120
|
+
text t
|
121
|
+
code c
|
122
|
+
end
|
123
|
+
|
124
|
+
h2 "Listing Format"
|
125
|
+
|
126
|
+
fs = TestFS.new do
|
127
|
+
folder(0) {
|
128
|
+
file_one(1)
|
129
|
+
folder_two(2) {
|
130
|
+
file_three(3)
|
131
|
+
}
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
code fs.new_listing.to_s
|
136
|
+
|
137
|
+
text <<-MD
|
138
|
+
Each line of the listing is in the format *`[indentation]`* *`[name]`* `:`*`[uid]`*
|
139
|
+
|
140
|
+
- The *indentation* must consist of only spaces, and is used to indicate the parent folder
|
141
|
+
- The *name* must not use colons (`:`). If it is cleared, it is assumed the file/folder is to be deleted
|
142
|
+
- The *uid* is used by FlEd to recognise the original of the edited line. Do not assume a *uid* does not
|
143
|
+
change between runs. It is valid only once.
|
144
|
+
MD
|
145
|
+
|
146
|
+
h2 "Operations"
|
147
|
+
|
148
|
+
h3 "Creating a new folder", 'Add a new line (therefore with no uid):'
|
149
|
+
show_example fs, <<-EXAMPLE
|
150
|
+
folder/ :0
|
151
|
+
new_folder
|
152
|
+
folder_two/ :2
|
153
|
+
EXAMPLE
|
154
|
+
|
155
|
+
h3 "Moving"
|
156
|
+
text 'Change the indentation and/or line order to change the parent of a file or folder:'
|
157
|
+
show_example fs, <<-EXAMPLE
|
158
|
+
folder/ :0
|
159
|
+
folder_two/ :2
|
160
|
+
file_one :1
|
161
|
+
file_three :3
|
162
|
+
EXAMPLE
|
163
|
+
em 'Moving an item below itself or its children is not recommended, as the listing may not be exhaustive'
|
164
|
+
|
165
|
+
h3 "Renaming"
|
166
|
+
text 'Edit the name while preserving the uid to rename the item'
|
167
|
+
show_example fs, <<-EXAMPLE
|
168
|
+
folder_renamed/ :0
|
169
|
+
file_one :1
|
170
|
+
folder_two/ :2
|
171
|
+
file_changed :3
|
172
|
+
EXAMPLE
|
173
|
+
text '*Swapping file names may not work in cases where the generated intermediary file exists but was not included in the listing*'
|
174
|
+
|
175
|
+
h3 "Deleting", 'Clear a name but leave the uid to delete that item'
|
176
|
+
show_example fs, <<-EXAMPLE
|
177
|
+
folder_renamed/ :0
|
178
|
+
:1
|
179
|
+
:2
|
180
|
+
:3
|
181
|
+
EXAMPLE
|
182
|
+
|
183
|
+
h3 "No-op"
|
184
|
+
text 'If a line (and all child-lines) is removed from the listing, it will have no operation.'
|
185
|
+
show_example fs, <<-EXAMPLE
|
186
|
+
folder/ :0
|
187
|
+
EXAMPLE
|
188
|
+
nl
|
189
|
+
nl
|
190
|
+
text '*Note that removing a folder without removing its children will move its children:*'
|
191
|
+
|
192
|
+
show_example fs, <<-EXAMPLE
|
193
|
+
folder/ :0
|
194
|
+
file_one :1
|
195
|
+
file_three :3
|
196
|
+
EXAMPLE
|
197
|
+
|
198
|
+
nl
|
199
|
+
text "If an indent is forgotten:"
|
200
|
+
|
201
|
+
show_example fs, <<-EXAMPLE
|
202
|
+
folder/ :0
|
203
|
+
file_one :1
|
204
|
+
file_three :3
|
205
|
+
EXAMPLE
|
206
|
+
|
207
|
+
h3 "All together"
|
208
|
+
show_example fs, <<-EXAMPLE
|
209
|
+
folder_new/ :0
|
210
|
+
new_folder/
|
211
|
+
first :1
|
212
|
+
second :3
|
213
|
+
:2
|
214
|
+
EXAMPLE
|
215
|
+
|
216
|
+
h2 "Edge cases", "These sort-of work, but are still rather experimental"
|
217
|
+
|
218
|
+
h3 "Swapping files"
|
219
|
+
|
220
|
+
fs = TestFS.new do
|
221
|
+
folder(0) {
|
222
|
+
file_one(1)
|
223
|
+
file_two(2)
|
224
|
+
}
|
225
|
+
end
|
226
|
+
code fs.new_listing.to_s
|
227
|
+
text "When applying"
|
228
|
+
show_example fs, <<-EXAMPLE
|
229
|
+
folder/ :0
|
230
|
+
file_two :1
|
231
|
+
file_one :2
|
232
|
+
EXAMPLE
|
233
|
+
|
234
|
+
h3 "Tree swapping"
|
235
|
+
fs = TestFS.new do
|
236
|
+
folder(0) {
|
237
|
+
sub_folder(1) {
|
238
|
+
sub_sub_folder(2) {
|
239
|
+
file.txt(3)
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
end
|
244
|
+
code fs.new_listing.to_s
|
245
|
+
text "When applying"
|
246
|
+
show_example fs, <<-EXAMPLE
|
247
|
+
sub_sub_folder/ :2
|
248
|
+
sub_folder/ :1
|
249
|
+
folder/ :0
|
250
|
+
file.txt :3
|
251
|
+
EXAMPLE
|
252
|
+
|
253
|
+
h2 "Contributors"
|
254
|
+
li "[Eric Doughty-Papassideris](http://github.com/ddlsmurf)"
|
255
|
+
end
|
256
|
+
puts readme
|