Missing import.
[rox-shell.git] / rox / shell / line.py
blob27cf10a039cfd1cb1d40e5bc05ae8583cf76a666
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 BaseArgument:
14 def __init__(self, view):
15 self.view = view
17 def get_entry_text(self):
18 return ''
20 def get_button_label(self):
21 return '?'
23 def entry_changed(self, entry):
24 return
26 def finish_edit(self):
27 pass
29 def validate(self):
30 pass
32 def tab(self, entry):
33 pass
35 def updown(self, entry, delta):
36 pass
38 class Argument(BaseArgument):
39 """Represents a word entered by the user for a command."""
40 # This is a bit complicated. An argument can be any of these:
41 # - A glob pattern matching multiple filenames (*.html)
42 # - A single filename (index.html)
43 # - A set of files selected manually (a.html, b.html)
44 # - A quoted string ('*.html')
45 # - An option (--index)
46 def __init__(self, view):
47 BaseArgument.__init__(self, view)
48 self.value = ''
49 self.type = 'empty'
51 def type_from_value(self, value):
52 if not value:
53 return 'empty'
54 elif value.startswith("!"):
55 return 'newfile'
56 elif value.startswith("'") or value.startswith('"'):
57 return 'quoted'
58 elif value.startswith("-"):
59 return 'option'
60 for x in value:
61 if x in '*?[':
62 return 'glob'
63 return 'filename'
65 def iter_matches(self, match, case_insensitive):
66 """Return all rows with a name matching match"""
67 for i, row in self.view.iter_contents():
68 name = row[0]
69 if case_insensitive:
70 name = name.lower()
71 if name.startswith(match):
72 yield i, row
74 def entry_changed(self, entry):
75 self.value = entry.get_text()
76 self.type = self.type_from_value(self.value)
78 if self.type not in ('filename', 'glob', 'newfile'):
79 return
81 if self.type == 'newfile':
82 value = self.value[1:]
83 else:
84 value = self.value
86 # Check which directory the view should be displaying...
87 viewed = self.view.view_dir.file
89 path, leaf = support.split_expanded_path(value)
90 if path:
91 abs_path = self.view.cwd.file.resolve_relative_path(path)
92 else:
93 abs_path = self.view.cwd.file
95 # Switch if necessary...
96 if abs_path.get_uri() != viewed.get_uri():
97 self.view.set_view_dir(abs_path)
99 if self.type == 'newfile':
100 return # Just switch views, no auto-complete
102 iv = self.view.iv
103 model = iv.get_model()
105 cursor_path = (self.view.iv.get_cursor() or (None, None))[0]
106 if cursor_path:
107 cursor_filename = model[model.get_iter(cursor_path)][0]
108 else:
109 cursor_filename = None
111 # If the user only entered lower-case letters do a case insensitive match
112 if self.type == 'filename':
113 # Rules are:
114 # - Select any exact match
115 # - Else, select any exact case-insensitive match
116 # - Else, select the cursor item if the prefix matches
117 # - Else, select the first prefix match
118 # - Else, select nothing
120 case_insensitive = (leaf == leaf.lower())
121 exact_case_match = None
122 exact_match = None
123 prefix_match = None
124 for i, row in self.iter_matches(leaf, case_insensitive):
125 name = row[0]
126 if name == leaf:
127 exact_case_match = model.get_path(i)
128 break
129 if case_insensitive:
130 name = name.lower()
131 if name == leaf:
132 exact_match = model.get_path(i)
133 if not prefix_match:
134 prefix_match = model.get_path(i)
135 if case_insensitive and cursor_filename:
136 cursor_filename = cursor_filename.lower()
137 if exact_case_match:
138 to_select = [exact_case_match]
139 elif exact_match:
140 to_select = [exact_match]
141 elif cursor_filename and cursor_filename.startswith(leaf):
142 to_select = [cursor_path]
143 elif prefix_match:
144 to_select = [prefix_match]
145 else:
146 to_select = []
147 elif self.type == 'glob':
148 to_select = []
149 pattern = leaf
150 def match(m, path, iter):
151 name = m[iter][0]
152 if fnmatch.fnmatch(name, pattern):
153 to_select.append(path)
154 model.foreach(match)
156 iv.unselect_all()
157 if to_select:
158 for path in to_select:
159 iv.select_path(path)
160 if cursor_path not in to_select:
161 iv.set_cursor(to_select[0])
163 def tab(self, entry):
164 if self.type == 'filename':
165 value = self.value
166 elif self.type == 'newfile':
167 value = self.value[1:]
168 else:
169 return
171 path, leaf = os.path.split(value)
172 case_insensitive = (leaf == leaf.lower())
173 prefix_match = None
174 single_match_is_dir = False
175 for i, row in self.iter_matches(leaf, case_insensitive):
176 name = row[DirModel.NAME]
177 if prefix_match is not None:
178 single_match_is_dir = False # Multiple matches
179 if not name.startswith(prefix_match):
180 # Have to shorten the match then
181 same = []
182 for a, b in zip(prefix_match, name):
183 if a == b:
184 same.append(a)
185 else:
186 break
187 prefix_match = ''.join(same)
188 else:
189 prefix_match = name
190 if row[DirModel.INFO].get_file_type() == gio.FILE_TYPE_DIRECTORY:
191 single_match_is_dir = True
192 if single_match_is_dir:
193 prefix_match += '/'
194 if prefix_match and prefix_match != leaf:
195 new = os.path.join(path, prefix_match)
196 if self.type == 'newfile':
197 new = '!' + new
198 entry.set_text(new)
199 entry.set_position(len(new))
201 def updown(self, entry, delta):
202 if self.type == 'newfile':
203 leaf = self.value[1:]
204 elif self.type == 'filename':
205 leaf = self.value
206 else:
207 return
209 leaf = os.path.basename(leaf)
211 model = self.view.iv.get_model()
212 cursor_path = self.view.get_cursor_path()
213 if delta > 0:
214 def apply_delta(i):
215 return model.iter_next(i) or model.get_iter_first()
216 else:
217 def apply_delta(i):
218 n, = model.get_path(i)
219 if n == 0:
220 n = model.iter_n_children(None)
221 return model.get_iter((n - 1,))
222 if cursor_path:
223 start = model.get_iter(cursor_path)
224 i = apply_delta(start)
225 else:
226 start = i = model.get_iter_root()
227 if not start:
228 return # Empty directory
229 start = model[start][DirModel.NAME]
231 case_insensitive = (leaf == leaf.lower())
233 while i:
234 name = model[i][DirModel.NAME]
235 if name == start:
236 return
237 if case_insensitive:
238 name = name.lower()
239 if name.startswith(leaf):
240 path = model.get_path(i)
241 self.view.iv.unselect_all()
242 self.view.iv.set_cursor(path)
243 self.view.iv.select_path(path)
244 return
245 i = apply_delta(i)
247 def finish_edit(self):
248 iv = self.view.iv
249 model = iv.get_model()
250 cursor_path = (iv.get_cursor() or (None, None))[0]
251 selected = iv.get_selected_items()
253 if cursor_path and selected and cursor_path not in selected:
254 raise Warning("Cursor not in selection!")
255 if cursor_path and not selected:
256 selected = [cursor_path]
258 if self.type == 'empty':
259 if not selected:
260 raise Warning("No selection and no cursor item!")
261 if len(selected) > 1:
262 raise Warning("Multiple selection!")
263 path = selected[0]
265 self.type = 'filename'
266 self.selected_item = model[model.get_iter(path)][0]
267 if self.type == 'filename':
268 self.selected_filename = None
270 path, leaf = support.split_expanded_path(self.value)
272 if len(selected) != 1:
273 raise Warning("Must be one selected item!")
274 # The cursor must be on the single selected item
275 selected_name = model[model.get_iter(selected[0])][0]
276 # Selected item must match text
277 if selected_name.lower().startswith(leaf.lower()):
278 abs_path = self.view.view_dir.file.resolve_relative_path(selected_name)
279 # NB: selected item may be above cwd
280 self.selected_filename = self.view.cwd.file.get_relative_path(abs_path) or abs_path.get_path()
281 else:
282 raise Warning("Selected item does not match entered text!")
284 def get_entry_text(self):
285 return self.value
287 def get_button_label(self):
288 if self.type == 'filename':
289 return self.selected_filename or '(none)'
290 return self.value
292 def expand_to_argv(self):
293 if self.type == 'empty':
294 raise Warning("Empty argument")
295 if self.type == 'filename':
296 if self.selected_filename is None:
297 raise Warning("No filename selected")
298 return [self.selected_filename]
299 elif self.type == 'glob':
300 pattern = self.value
301 matches = [row[0] for i, row in self.view.iter_contents() if fnmatch.fnmatch(row[0], pattern)]
302 if not matches:
303 raise Warning("Nothing matches '%s'!" % pattern)
304 return matches
305 elif self.type == 'newfile':
306 value = self.value[1:]
307 path, leaf = os.path.split(value)
308 if not leaf:
309 raise Warning("No name given for new file!")
310 if path:
311 final_dir = self.view.cwd.file.resolve_relative_path(path)
312 unix_path = final_dir.get_path()
313 if not os.path.exists(unix_path):
314 os.makedirs(unix_path)
315 return [value]
316 elif self.type == 'quoted':
317 first = self.value[0]
318 if self.value.endswith(first):
319 return [self.value[1:-1]]
320 else:
321 return [self.value[1:]]
322 elif self.type == 'option':
323 return [self.value]
324 else:
325 assert False, "Unknown type " + self.type
327 class CommandArgument(BaseArgument):
328 command = ''
330 def entry_changed(self, entry):
331 self.command = entry.get_text() or None
333 def get_button_label(self):
334 return self.command if self.command else "Open"
336 def expand_to_argv(self):
337 return [self.command]
339 class ArgvView:
340 def __init__(self, hbox):
341 self.hbox = hbox
342 self.args = []
343 self.widgets = []
345 def set_args(self, args):
346 self.args = args
347 self.edit_arg = self.args[-1]
348 self.build()
350 def build(self):
351 for w in self.widgets:
352 w.destroy()
353 self.widgets = []
354 self.active_entry = None
356 for x in self.args:
357 if x is self.edit_arg:
358 arg = gtk.Entry()
359 arg.set_text(x.get_entry_text())
360 arg.connect('changed', x.entry_changed)
361 self.active_entry = arg
362 else:
363 arg = gtk.Button(x.get_button_label())
364 arg.set_relief(gtk.RELIEF_NONE)
365 arg.connect('clicked', lambda b, x = x: self.activate(x))
366 arg.show()
367 self.hbox.pack_start(arg, False, True, 0)
368 self.widgets.append(arg)
370 def activate(self, x):
371 """Start editing argument 'x'"""
372 if x is self.edit_arg:
373 return
374 try:
375 self.edit_arg.finish_edit()
376 except Warning, ex:
377 self.edit_arg.view.warning(str(ex))
378 self.edit_arg = x
379 self.build()
380 i = self.args.index(x)
381 self.widgets[i].grab_focus()
383 def space(self):
384 if not self.active_entry.flags() & gtk.HAS_FOCUS:
385 return False # Not focussed
386 if self.active_entry.get_position() != len(self.active_entry.get_text()):
387 return False # Not at end
388 i = self.args.index(self.edit_arg)
389 try:
390 self.edit_arg.finish_edit()
391 except Warning, ex:
392 self.edit_arg.view.warning(str(ex))
393 self.edit_arg = Argument(self.edit_arg.view)
394 self.args.insert(i + 1, self.edit_arg)
395 self.build()
396 self.widgets[i + 1].grab_focus()
397 return True
399 def tab(self):
400 if self.active_entry.get_position() == len(self.active_entry.get_text()):
401 self.edit_arg.tab(self.active_entry)
402 return True
404 def updown(self, delta):
405 if self.active_entry and self.active_entry.flags() & gtk.HAS_FOCUS:
406 self.edit_arg.updown(self.active_entry, delta)
407 return True
408 else:
409 return False
411 def key_press_event(self, kev):
412 if not self.active_entry:
413 return False
414 old_text = self.active_entry.get_text()
415 self.active_entry.grab_focus() # Otherwise it selects the added text
416 self.active_entry.event(kev)
417 return self.active_entry.get_text() != old_text
419 def finish_edit(self):
420 self.edit_arg.finish_edit()