Fixed argument expansion for quoted arguments.
[rox-shell.git] / roxshell / line.py
blob74a0c7636b7326519e55cc860797497fa541842c
1 """
2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
4 """
5 import os, sys, fnmatch
7 import _gio as gio
8 import gtk
10 import support
11 from directory import DirModel
13 class CantHandle(Exception):
14 pass
16 class Value:
17 def __init__(self, arg, value):
18 self.arg = arg
19 self.value = value
20 self.parsed = self.parse_value(value)
22 def get_entry_text(self):
23 return self.value
25 def get_button_label(self):
26 return self.get_entry_text() or '(nothing)'
28 def finish_edit(self):
29 iv = self.arg.view.iv
30 model = iv.get_model()
31 if iv.flags() & gtk.HAS_FOCUS:
32 cursor_path = (iv.get_cursor() or (None, None))[0]
33 else:
34 cursor_path = None
35 selected = iv.get_selected_items()
37 if cursor_path and selected and cursor_path not in selected:
38 raise Warning("Cursor not in selection!")
39 if cursor_path and not selected:
40 selected = [cursor_path]
42 self.finish_edit_with_selection(selected)
44 def finish_edit_with_selection(self, selected):
45 pass
47 def get_default_command(self):
48 return None
50 def updown(self, entry, delta):
51 pass
53 class Option(Value):
54 def parse_value(self, value):
55 if value.startswith('-'):
56 return value
57 else:
58 raise CantHandle
60 def expand_to_argv(self):
61 return [self.value]
63 class Quote(Value):
64 def parse_value(self, value):
65 first = value[:1]
66 if first and first in '"\'':
67 if value[-1] == first:
68 return value[1:-1]
69 else:
70 return value[1:]
71 else:
72 raise CantHandle
74 def expand_to_argv(self):
75 return [self.parsed]
77 class SelectedFiles(Value):
78 abs_path = None # GFile for the directory containing the files
79 to_select = [] # Paths in the IconView's model to be selected
81 def __init__(self, arg, value):
82 Value.__init__(self, arg, value)
83 path, leaf = support.split_expanded_path(self.parsed)
84 if path:
85 self.abs_path = arg.view.cwd.file.resolve_relative_path(path)
86 else:
87 self.abs_path = arg.view.cwd.file
88 self.path = path
89 self.leaf = leaf
91 def tab(self, entry):
92 value = self.parsed
94 path, leaf = os.path.split(value)
95 case_insensitive = (leaf == leaf.lower())
96 prefix_match = None
97 single_match_is_dir = False
98 for i, row in self.arg.iter_matches(leaf, case_insensitive):
99 name = row[DirModel.NAME]
100 if prefix_match is not None:
101 single_match_is_dir = False # Multiple matches
102 if not name.startswith(prefix_match):
103 # Have to shorten the match then
104 same = []
105 for a, b in zip(prefix_match, name):
106 if a == b:
107 same.append(a)
108 else:
109 break
110 prefix_match = ''.join(same)
111 else:
112 prefix_match = name
113 if row[DirModel.INFO].get_file_type() == gio.FILE_TYPE_DIRECTORY:
114 single_match_is_dir = True
115 if single_match_is_dir:
116 prefix_match += '/'
117 if prefix_match and prefix_match != leaf:
118 self.parsed = os.path.join(path, prefix_match)
119 new = self.get_entry_text()
120 entry.set_text(new)
121 entry.set_position(len(new))
123 def updown(self, entry, delta):
124 leaf = self.leaf
126 iv = self.arg.view.iv
127 model = iv.get_model()
128 cursor_path = self.arg.view.get_cursor_path()
129 if delta > 0:
130 def apply_delta(i):
131 return model.iter_next(i) or model.get_iter_first()
132 else:
133 def apply_delta(i):
134 n, = model.get_path(i)
135 if n == 0:
136 n = model.iter_n_children(None)
137 return model.get_iter((n - 1,))
138 if cursor_path:
139 start = model.get_iter(cursor_path)
140 i = apply_delta(start)
141 else:
142 start = i = model.get_iter_root()
143 if not start:
144 return # Empty directory
145 start = model[start][DirModel.NAME]
147 case_insensitive = (leaf == leaf.lower())
149 while i:
150 name = model[i][DirModel.NAME]
151 if name == start:
152 return
153 if case_insensitive:
154 name = name.lower()
155 if name.startswith(leaf):
156 path = model.get_path(i)
157 iv.unselect_all()
158 iv.set_cursor(path)
159 iv.select_path(path)
160 return
161 i = apply_delta(i)
163 class Filename(SelectedFiles):
164 selected_filename = None
165 selected_type = None
167 def parse_value(self, value):
168 return value
170 def get_selected_files(self):
171 leaf = self.leaf
172 iv = self.arg.view.iv
173 model = iv.get_model()
175 self.selected_filename = None
176 self.selected_type = None
178 if not self.value:
179 return [] # Empty entry
181 if not leaf:
182 self.selected_filename = ''
183 self.selected_type = gio.FILE_TYPE_DIRECTORY
184 return []
185 elif leaf in ('.', '..'):
186 self.selected_filename = leaf
187 self.selected_type = gio.FILE_TYPE_DIRECTORY
188 return []
190 cursor_path = (self.arg.view.iv.get_cursor() or (None, None))[0]
191 if cursor_path:
192 cursor_filename = model[model.get_iter(cursor_path)][0]
193 else:
194 cursor_filename = None
196 # Rules are:
197 # - Select any exact match
198 # - Else, select any exact case-insensitive match
199 # - Else, select the cursor item if the prefix matches
200 # - Else, select the first prefix match
201 # - Else, select nothing
203 # If the user only entered lower-case letters do a case insensitive match
204 model = self.arg.view.iv.get_model()
206 case_insensitive = (leaf == leaf.lower())
207 exact_case_match = None
208 exact_match = None
209 prefix_match = None
210 for i, row in self.arg.iter_matches(leaf, case_insensitive):
211 name = row[0]
212 if name == leaf:
213 exact_case_match = model.get_path(i)
214 break
215 if case_insensitive:
216 name = name.lower()
217 if name == leaf:
218 exact_match = model.get_path(i)
219 if not prefix_match:
220 prefix_match = model.get_path(i)
221 if case_insensitive and cursor_filename:
222 cursor_filename = cursor_filename.lower()
223 if exact_case_match:
224 to_select = exact_case_match
225 elif exact_match:
226 to_select = exact_match
227 elif cursor_filename and cursor_filename.startswith(leaf):
228 to_select = cursor_path
229 elif prefix_match:
230 to_select = prefix_match
231 else:
232 to_select = None
234 if to_select is None:
235 return []
237 row = model[model.get_iter(to_select)]
238 self.selected_filename = row[DirModel.NAME]
239 self.selected_type = row[DirModel.INFO].get_file_type()
241 return [to_select]
243 def get_entry_text(self):
244 return self.parsed
246 def get_button_label(self):
247 return self.selected_filename or '(none)'
249 def expand_to_argv(self):
250 if not self.value:
251 raise Warning("Empty argument")
252 if self.selected_filename is None:
253 raise Warning("No filename selected")
254 abs_path = self.abs_path.resolve_relative_path(self.selected_filename)
255 return [self.arg.view.cwd.file.get_relative_path(abs_path) or abs_path.get_path()]
257 def finish_edit_with_selection(self, selected):
258 if not self.value:
259 model = self.arg.view.iv.get_model()
260 if not selected:
261 raise Warning("No selection and no cursor item!")
262 if len(selected) > 1:
263 raise Warning("Multiple selection!")
264 path = selected[0]
266 row = model[model.get_iter(path)]
267 self.selected_filename = row[DirModel.NAME]
268 self.selected_type = row[DirModel.INFO].get_file_type()
269 self.value = self.parsed = self.selected_filename
271 def get_default_command(self):
272 if self.selected_filename is None:
273 return None
274 if self.selected_type == gio.FILE_TYPE_DIRECTORY:
275 return 'cd'
276 else:
277 return 'gvim'
279 class Newfile(SelectedFiles):
280 def parse_value(self, value):
281 if value.startswith('!'):
282 return value[1:]
283 raise CantHandle
285 def get_entry_text(self):
286 return '!' + self.parsed
288 def get_selected_files(self):
289 # No completion for new files
290 # TODO: highlight if it exists
291 return []
293 def expand_to_argv(self):
294 value = self.parsed
295 path, leaf = os.path.split(value)
296 if not leaf:
297 raise Warning("No name given for new file!")
298 if path:
299 final_dir = self.arg.view.cwd.file.resolve_relative_path(path)
300 unix_path = final_dir.get_path()
301 if not os.path.exists(unix_path):
302 os.makedirs(unix_path)
303 return [value]
305 def get_default_command(self):
306 return 'mkdir'
308 class Glob(SelectedFiles):
309 def updown(self, entry, delta):
310 pass
312 def parse_value(self, value):
313 for x in value:
314 if x in '*?[':
315 return value
316 raise CantHandle
318 def get_selected_files(self):
319 to_select = []
320 pattern = self.leaf
321 def match(m, path, iter):
322 name = m[iter][0]
323 if fnmatch.fnmatch(name, pattern):
324 to_select.append(path)
325 self.arg.view.iv.get_model().foreach(match)
326 return to_select
328 def expand_to_argv(self):
329 pattern = self.parsed
330 matches = [row[0] for i, row in self.arg.view.iter_contents() if fnmatch.fnmatch(row[0], pattern)]
331 if not matches:
332 raise Warning("Nothing matches '%s'!" % pattern)
333 return matches
335 def tab(self, entry):
336 return
338 class BaseArgument:
339 def __init__(self, view):
340 self.view = view
342 def get_entry_text(self):
343 return ''
345 def get_button_label(self):
346 return '?'
348 def entry_changed(self, entry):
349 return
351 def finish_edit(self):
352 pass
354 def validate(self):
355 pass
357 def tab(self, entry):
358 pass
360 def updown(self, entry, delta):
361 pass
363 class Argument(BaseArgument):
364 """Represents a word entered by the user for a command."""
365 # This is a bit complicated. An argument can be any of these:
366 # - A glob pattern matching multiple filenames (*.html)
367 # - A single filename (index.html)
368 # - A set of files selected manually (a.html, b.html)
369 # - A quoted string ('*.html')
370 # - An option (--index)
371 def __init__(self, view):
372 BaseArgument.__init__(self, view)
373 self.result = self.parse_value('')
375 def parse_value(self, value):
376 for t in [Newfile, Quote, Option, Glob, Filename]:
377 try:
378 return t(self, value)
379 except CantHandle:
380 continue
381 assert False
383 def iter_matches(self, match, case_insensitive):
384 """Return all rows with a name matching match"""
385 for i, row in self.view.iter_contents():
386 name = row[0]
387 if case_insensitive:
388 name = name.lower()
389 if name.startswith(match):
390 yield i, row
392 def entry_changed(self, entry):
393 self.result = self.parse_value(entry.get_text())
395 if not isinstance(self.result, SelectedFiles):
396 self.view.iv.unselect_all()
397 return
399 cursor_path = (self.view.iv.get_cursor() or (None, None))[0]
401 # Check which directory the view should be displaying...
402 viewed = self.view.view_dir.file
404 # Switch if necessary...
405 if self.result.abs_path.get_uri() != viewed.get_uri():
406 self.view.set_view_dir(self.result.abs_path)
408 to_select = self.result.get_selected_files()
410 iv = self.view.iv
411 iv.unselect_all()
412 if to_select:
413 for path in to_select:
414 iv.select_path(path)
415 if cursor_path not in to_select:
416 iv.set_cursor(to_select[0])
418 def tab(self, entry):
419 self.result.tab(entry)
421 def get_default_command(self):
422 return self.result.get_default_command()
424 def updown(self, entry, delta):
425 self.result.updown(entry, delta)
427 def finish_edit(self):
428 self.result.finish_edit()
430 def get_entry_text(self):
431 return self.result.get_entry_text()
433 def get_button_label(self):
434 return self.result.get_button_label()
436 def expand_to_argv(self):
437 return self.result.expand_to_argv()
439 class CommandArgument(BaseArgument):
440 command = ''
441 default_command = None
443 def entry_changed(self, entry):
444 self.command = entry.get_text() or None
446 def get_button_label(self):
447 return self.command if self.command else \
448 self.default_command if self.default_command else \
449 "Open"
451 def expand_to_argv(self):
452 return [self.command or self.default_command]
454 def set_default_command_from_args(self, args):
455 self.default_command = None
457 if len(args) != 1:
458 return
460 self.default_command = args[0].get_default_command()
462 class ArgvView:
463 def __init__(self, hbox):
464 self.hbox = hbox
465 self.args = []
466 self.widgets = []
468 def set_args(self, args):
469 self.args = args
470 self.edit_arg = self.args[-1]
471 self.build()
473 def build(self):
474 for w in self.widgets:
475 w.destroy()
476 self.widgets = []
477 self.active_entry = None
479 cmd_arg = self.args[0]
481 for x in self.args:
482 if x is self.edit_arg:
483 arg = gtk.Entry()
484 arg.set_text(x.get_entry_text())
485 def entry_changed(entry, x = x):
486 x.entry_changed(entry)
487 if x is not cmd_arg and not cmd_arg.command:
488 cmd_arg.set_default_command_from_args(self.args[1:])
489 self.widgets[0].set_label(cmd_arg.get_button_label())
490 arg.connect('changed', entry_changed)
491 self.active_entry = arg
492 else:
493 arg = gtk.Button(x.get_button_label())
494 arg.set_relief(gtk.RELIEF_NONE)
495 arg.connect('clicked', lambda b, x = x: self.activate(x))
496 arg.show()
497 self.hbox.pack_start(arg, False, True, 0)
498 self.widgets.append(arg)
500 def activate(self, x):
501 """Start editing argument 'x'"""
502 if x is self.edit_arg:
503 return
504 try:
505 self.edit_arg.finish_edit()
506 except Warning, ex:
507 self.edit_arg.view.warning(str(ex))
508 self.edit_arg = x
509 self.build()
510 i = self.args.index(x)
511 self.widgets[i].grab_focus()
513 def space(self):
514 if not self.active_entry.flags() & gtk.HAS_FOCUS:
515 return False # Not focussed
516 if self.active_entry.get_position() != len(self.active_entry.get_text()):
517 return False # Not at end
518 i = self.args.index(self.edit_arg)
519 try:
520 self.edit_arg.finish_edit()
521 except Warning, ex:
522 self.edit_arg.view.warning(str(ex))
523 self.edit_arg = Argument(self.edit_arg.view)
524 self.args.insert(i + 1, self.edit_arg)
525 self.build()
526 self.widgets[i + 1].grab_focus()
527 return True
529 def tab(self):
530 if self.active_entry.get_position() == len(self.active_entry.get_text()):
531 self.edit_arg.tab(self.active_entry)
532 return True
534 def updown(self, delta):
535 if self.active_entry and self.active_entry.flags() & gtk.HAS_FOCUS:
536 self.edit_arg.updown(self.active_entry, delta)
537 return True
538 else:
539 return False
541 def key_press_event(self, kev):
542 if not self.active_entry:
543 return False
544 old_text = self.active_entry.get_text()
545 self.active_entry.grab_focus() # Otherwise it selects the added text
546 self.active_entry.event(kev)
547 return self.active_entry.get_text() != old_text
549 def finish_edit(self):
550 self.edit_arg.finish_edit()