fled 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|