Renamed rox.shell to roxshell.
[rox-shell.git] / roxshell / shell.py
blob1b6d73398ac8272938efe992b2459401c7886a50
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 def get_default_command(args):
25 if len(args) > 1:
26 raise Warning("No defaults for multiple arguments!")
28 arg = args[0]
29 if arg.type == 'newfile':
30 return 'mkdir'
31 else:
32 return 'rox:open'
34 FILER_PAGE = 0
35 TERMINAL_PAGE = 1
37 WRAP_WIDTH = 20 # Wrap width in chars (approx)
39 class ShellView:
40 terminal = None
41 user_seen_terminal_contents = False
42 warning_timeout = None
43 cwd = None
44 view_dir = None
46 def __init__(self, cwd_file):
47 builder = gtk.Builder()
48 builder.add_from_file(os.path.join(os.path.dirname(__file__), "ui.xml"))
49 self.window = builder.get_object('directory')
50 self.notebook = builder.get_object('notebook')
52 cd_parent = builder.get_object('cd-parent')
53 cd_parent.connect('activate', lambda a: self.cd_parent())
55 cd_home = builder.get_object('cd-home')
56 cd_home.connect('activate', lambda a: self.cd_home())
58 trash = builder.get_object('trash')
59 trash.connect('activate', lambda a: self.trash())
61 # Must show window before adding icons, or we randomly get:
62 # The error was 'BadAlloc (insufficient resources for operation)'
63 self.window.show()
65 self.window_destroyed = tasks.Blocker('Window destroyed')
66 self.window.connect('destroy', lambda w: self.window_destroyed.trigger())
68 self.window.connect('key-press-event', self.key_press_event)
70 self.iv = builder.get_object('iconview')
71 self.iv.set_text_column(0)
72 self.iv.set_pixbuf_column(1)
73 self.iv.set_selection_mode(gtk.SELECTION_MULTIPLE)
75 text = self.iv.get_cells()[0]
76 self.iv.set_attributes(text, text = DirModel.NAME, foreground = DirModel.COLOUR)
77 pango_context = self.iv.get_pango_context()
78 font_metrics = pango_context.get_metrics(self.iv.style.font_desc, pango_context.get_language())
79 text.set_property('wrap-width', WRAP_WIDTH * font_metrics.get_approximate_char_width() / pango.SCALE)
81 ui = builder.get_object('uimanager')
83 accelgroup = ui.get_accel_group()
84 self.window.add_accel_group(accelgroup)
86 popup = ui.get_widget('ui/main-popup')
88 self.iv.connect('item-activated', self.item_activated)
89 def iv_button_press(widget, bev):
90 if bev.type == gtk.gdk.BUTTON_PRESS and bev.button == 3:
91 selected = self.iv.get_selected_items()
92 pointer_path = self.iv.get_path_at_pos(int(bev.x), int(bev.y))
93 if pointer_path and pointer_path not in selected:
94 if pointer_path:
95 self.iv.unselect_all()
96 self.iv.select_path(pointer_path)
97 self.iv.set_cursor(pointer_path)
99 popup.popup(None, None, None, bev.button, bev.time)
100 self.iv.connect('button-press-event', iv_button_press)
102 command_area = builder.get_object('command')
103 self.command_argv = line.ArgvView(command_area)
105 self.status_msg = builder.get_object('status_msg')
107 self.set_cwd(cwd_file)
108 self.reset()
110 self.window.show_all()
112 def iter_contents(self):
113 m = self.iv.get_model()
114 i = m.get_iter_root()
115 while i:
116 yield i, m[i]
117 i = m.iter_next(i)
119 def warning(self, msg):
120 def hide_warning():
121 self.status_msg.set_text('')
122 return False
123 if self.warning_timeout is not None:
124 gobject.source_remove(self.warning_timeout)
125 self.status_msg.set_text(msg)
126 self.warning_timeout = gobject.timeout_add(2000, hide_warning)
128 def show_terminal(self):
129 # Actually, don't show it until we get some output...
130 if not self.terminal:
131 def terminal_contents_changed(vte):
132 if self.notebook.get_current_page() == FILER_PAGE:
133 self.notebook.set_current_page(TERMINAL_PAGE)
134 self.user_seen_terminal_contents = False
136 def terminal_child_exited():
137 if self.user_seen_terminal_contents:
138 self.notebook.set_current_page(FILER_PAGE)
139 else:
140 self.terminal.feed('\r\nProcess complete. Press Return to return to filer view.\r\n')
141 self.waiting_for_return = True
142 return False
144 self.terminal = vte.Terminal()
145 self.terminal.connect('contents-changed', terminal_contents_changed)
146 self.terminal.connect('child-exited', lambda vte: gobject.timeout_add(100, terminal_child_exited))
148 # Should be configurable.
149 # Hint:
150 # cp /usr/share/fonts/X11/misc/9x15B-ISO8859-1.pcf.gz ~/.fonts/
151 self.terminal.set_font(pango.FontDescription("Fixed 12"))
153 self.terminal.show()
155 self.notebook.add(self.terminal)
156 self.waiting_for_return = False
158 def reset(self):
159 self.view_cwd()
160 self.command_argv.set_args([line.CommandArgument(self), line.Argument(self)])
161 if self.notebook.get_current_page() == FILER_PAGE:
162 self.iv.grab_focus()
163 self.iv.unselect_all()
165 def key_press_event(self, window, kev):
166 #for x in dir(keysyms):
167 # if getattr(keysyms, x) == kev.keyval:
168 # print x
170 if self.terminal and self.terminal.flags() & gtk.HAS_FOCUS:
171 if kev.keyval in RETURN_KEYS and self.waiting_for_return:
172 self.notebook.set_current_page(FILER_PAGE)
173 return True
174 self.user_seen_terminal_contents = True
175 return False
177 if kev.keyval == keysyms.space:
178 if self.command_argv.space():
179 return True
181 if kev.keyval == keysyms.Tab:
182 if self.command_argv.tab():
183 return True
185 if kev.keyval == keysyms.Up:
186 if self.command_argv.updown(-1):
187 return True
189 if kev.keyval == keysyms.Down:
190 if self.command_argv.updown(1):
191 return True
193 if kev.keyval == keysyms.Escape:
194 self.reset()
195 return True
196 elif kev.keyval in RETURN_KEYS:
197 self.execute_command()
198 return True
200 # Are we ready for special characters?
201 if self.command_argv.active_entry and self.command_argv.active_entry.flags() & gtk.HAS_FOCUS:
202 accept_special = True # TODO: check cursor is at end
203 else:
204 accept_special = True
206 if accept_special:
207 if kev.keyval == keysyms.comma:
208 self.command_argv.activate(self.command_argv.args[0])
209 return True
210 elif kev.keyval == keysyms.semicolon and len(self.command_argv.args) == 2:
211 self.command_argv.set_args([line.CommandArgument(self)])
212 self.command_argv.widgets[0].grab_focus()
213 return True
215 if self.iv.flags() & gtk.HAS_FOCUS:
216 if self.iv.event(kev):
217 # Handled by IconView (e.g. cursor motion)
218 return True
219 elif kev.keyval == keysyms.BackSpace:
220 self.cd_parent()
221 else:
222 if not self.command_argv.key_press_event(kev):
223 self.iv.grab_focus() # Restore focus to IconView
224 return False
225 return True
227 def run_in_terminal(self, argv):
228 cmd = support.find_in_path(argv[0])
229 if not cmd:
230 raise Warning("Command '%s' not found in $PATH" % argv[0])
231 if not os.path.exists(cmd):
232 raise Warning("Command '%s' does not exist!" % argv[0])
234 self.show_terminal()
235 self.user_seen_terminal_contents = True
236 self.terminal.fork_command(cmd, argv, None, self.cwd.file.get_path(), False, False, False)
238 def execute_command(self, override_command = None):
239 try:
240 self.command_argv.finish_edit()
241 args = self.command_argv.args
242 if override_command:
243 override = line.CommandArgument(self)
244 override.command = override_command
245 args = [override] + args[1:]
246 self.run_command(args)
247 except Warning, ex:
248 self.warning(str(ex))
249 else:
250 self.reset()
252 def view_cwd(self):
253 """Make the IconView show the cwd."""
254 if self.view_dir != self.cwd:
255 self.set_view_dir(self.cwd.file)
257 def set_view_dir(self, dir_file):
258 if self.view_dir:
259 self.view_dir.del_ref(self)
260 self.view_dir = None
261 self.view_dir = directory.get_dir_model(dir_file)
262 self.view_dir.add_ref(self)
264 # This segfaults. See GTK bug #523724.
265 #tree_model = gtk.TreeModelSort(self.view_dir.model)
266 #tree_model.set_sort_column_id(DirModel.SORT, gtk.SORT_ASCENDING)
267 tree_model = self.view_dir.model
269 self.iv.set_model(tree_model)
270 if tree_model.get_iter_root():
271 self.iv.set_cursor((0,))
273 if self.view_dir.error:
274 self.warning(str(self.view_dir.error))
276 def set_cwd(self, cwd_file):
277 if self.cwd:
278 self.cwd.del_ref(self)
279 self.cwd = None
280 self.cwd = directory.get_dir_model(cwd_file)
281 self.cwd.add_ref(self)
282 self.view_cwd()
283 self.window.set_title(self.cwd.file.get_uri())
284 self.reset()
286 @tasks.async
287 def run(self):
288 self.iv.grab_focus()
289 while True:
290 blockers = [self.window_destroyed]
291 yield blockers
292 tasks.check(blockers)
293 if self.window_destroyed.happened:
294 break
296 def get_iter(self, name):
297 for i, row in self.iter_contents():
298 if row[DirModel.NAME] == name:
299 return i
300 raise Exception("File '%s' not found!" % name)
302 def get_cursor_path(self):
303 return (self.iv.get_cursor() or (None, None))[0]
305 def item_activated(self, iv, path):
306 # Open a single item
307 tm = iv.get_model()
308 row = tm[tm.get_iter(path)]
309 name = row[DirModel.NAME]
310 item_info = row[DirModel.INFO]
312 child = self.view_dir.file.get_child(name)
314 self.reset()
315 self.open_item(child)
317 def open_item(self, item_file):
318 item_info = item_file.query_info('standard::*', 0)
319 if item_info.get_file_type() == gio.FILE_TYPE_DIRECTORY:
320 self.set_cwd(item_file)
321 else:
322 self.run_in_terminal(['gvim', item_file.get_path()])
324 def trash(self):
325 self.execute_command('rox:trash')
327 def cd_home(self):
328 self.set_cwd(gio.file_new_for_path(os.path.expanduser('~')))
330 def cd_parent(self):
331 leaf = self.cwd.file.get_basename()
332 parent = self.cwd.file.get_parent()
333 if parent:
334 self.set_cwd(parent)
335 for i, row in self.iter_contents():
336 if row[DirModel.NAME] == leaf:
337 path = self.iv.get_model().get_path(i)
338 self.iv.select_path(path)
339 self.iv.set_cursor(path)
340 break
342 def run_command(self, args):
343 argv = []
344 for a in args:
345 argv += a.expand_to_argv()
346 if not argv[0]:
347 argv[0] = get_default_command(args[1:])
349 builtin = commands.builtin_commands.get(argv[0], None)
350 if builtin:
351 msg = builtin(self, argv[1:])
352 if msg:
353 self.warning(msg) # Not really a warning, just info
354 else:
355 self.run_in_terminal(argv)