Fixed argument expansion for quoted arguments.
[rox-shell.git] / roxshell / shell.py
blob29d4b3d2c0df525a8ce829eadf8857f4bba64868
1 """
2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
4 """
5 import os, sys, fnmatch
6 from zeroinstall.support import tasks # tmp
8 import _gio as gio
9 import gobject
10 import gtk
11 import pango
12 from gtk import keysyms
13 import vte
14 import support
16 import commands
17 import directory
18 import line
20 DirModel = directory.DirModel
22 RETURN_KEYS = (keysyms.Return, keysyms.KP_Enter, keysyms.ISO_Enter)
24 FILER_PAGE = 0
25 TERMINAL_PAGE = 1
27 WRAP_WIDTH = 20 # Wrap width in chars (approx)
29 class ShellView:
30 terminal = None
31 user_seen_terminal_contents = False
32 warning_timeout = None
33 cwd = None
34 view_dir = None
36 def __init__(self, cwd_file):
37 builder = gtk.Builder()
38 builder.add_from_file(os.path.join(os.path.dirname(__file__), "ui.xml"))
39 self.window = builder.get_object('directory')
40 self.notebook = builder.get_object('notebook')
42 cd_parent = builder.get_object('cd-parent')
43 cd_parent.connect('activate', lambda a: self.cd_parent())
45 cd_home = builder.get_object('cd-home')
46 cd_home.connect('activate', lambda a: self.cd_home())
48 trash = builder.get_object('trash')
49 trash.connect('activate', lambda a: self.trash())
51 # Must show window before adding icons, or we randomly get:
52 # The error was 'BadAlloc (insufficient resources for operation)'
53 self.window.show()
55 self.window_destroyed = tasks.Blocker('Window destroyed')
56 self.window.connect('destroy', lambda w: self.window_destroyed.trigger())
58 self.window.connect('key-press-event', self.key_press_event)
60 self.iv = builder.get_object('iconview')
61 self.iv.set_text_column(0)
62 self.iv.set_pixbuf_column(1)
63 self.iv.set_selection_mode(gtk.SELECTION_MULTIPLE)
65 text = self.iv.get_cells()[0]
66 self.iv.set_attributes(text, text = DirModel.NAME, foreground = DirModel.COLOUR)
67 pango_context = self.iv.get_pango_context()
68 font_metrics = pango_context.get_metrics(self.iv.style.font_desc, pango_context.get_language())
69 text.set_property('wrap-width', WRAP_WIDTH * font_metrics.get_approximate_char_width() / pango.SCALE)
71 ui = builder.get_object('uimanager')
73 accelgroup = ui.get_accel_group()
74 self.window.add_accel_group(accelgroup)
76 popup = ui.get_widget('ui/main-popup')
78 self.iv.connect('item-activated', self.item_activated)
79 def iv_button_press(widget, bev):
80 if bev.type == gtk.gdk.BUTTON_PRESS and bev.button == 3:
81 selected = self.iv.get_selected_items()
82 pointer_path = self.iv.get_path_at_pos(int(bev.x), int(bev.y))
83 if pointer_path and pointer_path not in selected:
84 if pointer_path:
85 self.iv.unselect_all()
86 self.iv.select_path(pointer_path)
87 self.iv.set_cursor(pointer_path)
89 popup.popup(None, None, None, bev.button, bev.time)
90 self.iv.connect('button-press-event', iv_button_press)
92 command_area = builder.get_object('command')
93 self.command_argv = line.ArgvView(command_area)
95 self.status_msg = builder.get_object('status_msg')
97 self.set_cwd(cwd_file)
98 self.reset()
100 self.window.show_all()
102 def iter_contents(self):
103 m = self.iv.get_model()
104 i = m.get_iter_root()
105 while i:
106 yield i, m[i]
107 i = m.iter_next(i)
109 def warning(self, msg):
110 def hide_warning():
111 self.status_msg.set_text('')
112 return False
113 if self.warning_timeout is not None:
114 gobject.source_remove(self.warning_timeout)
115 self.status_msg.set_text(msg)
116 self.warning_timeout = gobject.timeout_add(2000, hide_warning)
118 def show_terminal(self):
119 # Actually, don't show it until we get some output...
120 if not self.terminal:
121 def terminal_contents_changed(vte):
122 if self.notebook.get_current_page() == FILER_PAGE:
123 self.notebook.set_current_page(TERMINAL_PAGE)
124 self.user_seen_terminal_contents = False
126 def terminal_child_exited():
127 if self.user_seen_terminal_contents:
128 self.notebook.set_current_page(FILER_PAGE)
129 else:
130 self.terminal.feed('\r\nProcess complete. Press Return to return to filer view.\r\n')
131 self.waiting_for_return = True
132 return False
134 self.terminal = vte.Terminal()
135 self.terminal.connect('contents-changed', terminal_contents_changed)
136 self.terminal.connect('child-exited', lambda vte: gobject.timeout_add(100, terminal_child_exited))
138 # Should be configurable.
139 # Hint:
140 # cp /usr/share/fonts/X11/misc/9x15B-ISO8859-1.pcf.gz ~/.fonts/
141 self.terminal.set_font(pango.FontDescription("Fixed 12"))
143 self.terminal.show()
145 self.notebook.add(self.terminal)
146 self.waiting_for_return = False
148 def reset(self):
149 self.view_cwd()
150 self.command_argv.set_args([line.CommandArgument(self), line.Argument(self)])
151 if self.notebook.get_current_page() == FILER_PAGE:
152 self.iv.grab_focus()
153 self.iv.unselect_all()
155 def key_press_event(self, window, kev):
156 #for x in dir(keysyms):
157 # if getattr(keysyms, x) == kev.keyval:
158 # print x
160 if self.terminal and self.terminal.flags() & gtk.HAS_FOCUS:
161 if kev.keyval in RETURN_KEYS and self.waiting_for_return:
162 self.notebook.set_current_page(FILER_PAGE)
163 return True
164 self.user_seen_terminal_contents = True
165 return False
167 if kev.keyval == keysyms.space:
168 if self.command_argv.space():
169 return True
171 if kev.keyval == keysyms.Tab:
172 if self.command_argv.tab():
173 return True
175 if kev.keyval == keysyms.Up:
176 if self.command_argv.updown(-1):
177 return True
179 if kev.keyval == keysyms.Down:
180 if self.command_argv.updown(1):
181 return True
183 if kev.keyval == keysyms.Escape:
184 self.reset()
185 return True
186 elif kev.keyval in RETURN_KEYS:
187 self.execute_command()
188 return True
190 # Are we ready for special characters?
191 if self.command_argv.active_entry and self.command_argv.active_entry.flags() & gtk.HAS_FOCUS:
192 accept_special = True # TODO: check cursor is at end
193 else:
194 accept_special = True
196 if accept_special:
197 if kev.keyval == keysyms.comma:
198 self.command_argv.activate(self.command_argv.args[0])
199 return True
200 elif kev.keyval == keysyms.semicolon and len(self.command_argv.args) == 2:
201 self.command_argv.set_args([line.CommandArgument(self)])
202 self.command_argv.widgets[0].grab_focus()
203 return True
205 if self.iv.flags() & gtk.HAS_FOCUS:
206 if self.iv.event(kev):
207 # Handled by IconView (e.g. cursor motion)
208 return True
209 elif kev.keyval == keysyms.BackSpace:
210 self.cd_parent()
211 else:
212 if not self.command_argv.key_press_event(kev):
213 self.iv.grab_focus() # Restore focus to IconView
214 return False
215 return True
217 def run_in_terminal(self, argv):
218 cmd = support.find_in_path(argv[0])
219 if not cmd:
220 raise Warning("Command '%s' not found in $PATH" % argv[0])
221 if not os.path.exists(cmd):
222 raise Warning("Command '%s' does not exist!" % argv[0])
224 self.show_terminal()
225 self.user_seen_terminal_contents = True
226 self.terminal.fork_command(cmd, argv, None, self.cwd.file.get_path(), False, False, False)
228 def execute_command(self, override_command = None):
229 try:
230 self.command_argv.finish_edit()
231 args = self.command_argv.args
232 if override_command:
233 override = line.CommandArgument(self)
234 override.command = override_command
235 args = [override] + args[1:]
236 self.run_command(args)
237 except Warning, ex:
238 self.warning(str(ex))
239 else:
240 self.reset()
242 def view_cwd(self):
243 """Make the IconView show the cwd."""
244 if self.view_dir != self.cwd:
245 self.set_view_dir(self.cwd.file)
247 def set_view_dir(self, dir_file):
248 if self.view_dir:
249 self.view_dir.del_ref(self)
250 self.view_dir = None
251 self.view_dir = directory.get_dir_model(dir_file)
252 self.view_dir.add_ref(self)
254 self.window.set_title(self.view_dir.file.get_uri())
256 # This segfaults. See GTK bug #523724.
257 #tree_model = gtk.TreeModelSort(self.view_dir.model)
258 #tree_model.set_sort_column_id(DirModel.SORT, gtk.SORT_ASCENDING)
259 tree_model = self.view_dir.model
261 self.iv.set_model(tree_model)
262 if tree_model.get_iter_root():
263 self.iv.set_cursor((0,))
265 if self.view_dir.error:
266 self.warning(str(self.view_dir.error))
268 def set_cwd(self, cwd_file):
269 if self.cwd:
270 self.cwd.del_ref(self)
271 self.cwd = None
272 self.cwd = directory.get_dir_model(cwd_file)
273 self.cwd.add_ref(self)
274 self.view_cwd()
275 self.reset()
277 @tasks.async
278 def run(self):
279 self.iv.grab_focus()
280 while True:
281 blockers = [self.window_destroyed]
282 yield blockers
283 tasks.check(blockers)
284 if self.window_destroyed.happened:
285 break
287 def get_iter(self, name):
288 for i, row in self.iter_contents():
289 if row[DirModel.NAME] == name:
290 return i
291 raise Exception("File '%s' not found!" % name)
293 def get_cursor_path(self):
294 return (self.iv.get_cursor() or (None, None))[0]
296 def item_activated(self, iv, path):
297 # Open a single item
298 tm = iv.get_model()
299 row = tm[tm.get_iter(path)]
300 name = row[DirModel.NAME]
301 item_info = row[DirModel.INFO]
303 child = self.view_dir.file.get_child(name)
305 self.reset()
306 self.open_item(child)
308 def open_item(self, item_file):
309 item_info = item_file.query_info('standard::*', 0)
310 if item_info.get_file_type() == gio.FILE_TYPE_DIRECTORY:
311 self.set_cwd(item_file)
312 else:
313 self.run_in_terminal(['gvim', item_file.get_path()])
315 def trash(self):
316 self.execute_command('rox:trash')
318 def cd_home(self):
319 self.set_cwd(gio.file_new_for_path(os.path.expanduser('~')))
321 def cd_parent(self):
322 leaf = self.cwd.file.get_basename()
323 parent = self.cwd.file.get_parent()
324 if parent:
325 self.set_cwd(parent)
326 for i, row in self.iter_contents():
327 if row[DirModel.NAME] == leaf:
328 path = self.iv.get_model().get_path(i)
329 self.iv.select_path(path)
330 self.iv.set_cursor(path)
331 break
333 def run_command(self, args):
334 argv = []
335 for a in args:
336 argv += a.expand_to_argv()
337 if not argv[0]:
338 argv[0] = 'rox:open'
340 builtin = commands.builtin_commands.get(argv[0], None)
341 if builtin:
342 msg = builtin(self, argv[1:])
343 if msg:
344 self.warning(msg) # Not really a warning, just info
345 else:
346 self.run_in_terminal(argv)