Press , to edit the command.
[rox-shell.git] / rox / shell / shell.py
blob23b91a36f372de75c6fc1c14c2144746724e59ff
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 from gtk import keysyms
12 import vte
14 icon_size = 48
16 gtk_theme = gtk.icon_theme_get_default()
17 icon_text_plain = gtk_theme.load_icon('text-x-generic', icon_size, 0)
18 icon_dir = gtk_theme.load_icon('folder', icon_size, 0)
20 RETURN_KEYS = (keysyms.Return, keysyms.KP_Enter, keysyms.ISO_Enter)
22 class DirUpdated(tasks.Blocker):
23 changes = []
25 class ShellController:
26 def __init__(self, file):
27 self.file = file
28 self.updated = None
29 self.build_contents()
31 def build_contents(self):
32 contents = {}
33 e = self.file.enumerate_children('standard::*', 0)
34 if e:
35 while True:
36 info = e.next_file()
37 if not info:
38 break
39 contents[info.get_name()] = info
40 self.contents = contents
42 if self.updated:
43 self.updated.trigger()
44 self.updated = DirUpdated("Updates for " + self.file.get_uri())
46 def item_activated(self, name):
47 item_info = self.contents[name]
48 child = self.file.get_child(name)
50 if item_info.get_file_type() == gio.FILE_TYPE_DIRECTORY:
51 self.file = child
52 self.build_contents()
53 else:
54 self.view.run_in_terminal(['cat', child.get_path()], self.file.get_path())
56 def cd_parent(self):
57 parent = self.file.get_parent()
58 if parent:
59 self.file = parent
60 self.build_contents()
62 def gtk_tree_model(contents):
63 tm = gtk.ListStore(str, gtk.gdk.Pixbuf)
64 tm.set_sort_column_id(0, gtk.SORT_ASCENDING)
65 for name, info in contents.iteritems():
66 if not name.startswith('.'):
67 new = tm.append(None)
68 if info.get_file_type() == gio.FILE_TYPE_DIRECTORY:
69 icon = icon_dir
70 else:
71 icon = icon_text_plain
72 tm[new] = [name, icon]
73 return tm
75 FILER_PAGE = 0
76 TERMINAL_PAGE = 1
78 class Argument:
79 def __init__(self, view):
80 self.view = view
81 self.pattern = ''
83 def entry_changed(self, entry):
84 self.pattern = entry.get_text()
85 if not self.pattern:
86 return
87 iv = self.view.iv
88 model = iv.get_model()
90 pattern_star = self.pattern + '*'
92 # If the user only entered lower-case letters do a case insensitive match
93 case_insensitive = (self.pattern == self.pattern.lower())
95 items = []
96 already_selected = []
97 to_select = []
98 prefix_matches = []
99 def match(m, path, iter):
100 name = m[iter][0]
101 if case_insensitive:
102 name = name.lower()
103 if fnmatch.fnmatch(name, self.pattern):
104 to_select.append(path)
105 elif fnmatch.fnmatch(name, pattern_star):
106 prefix_matches.append(path)
107 if iv.path_is_selected(path):
108 already_selected.append(path)
109 model.foreach(match)
111 iv.unselect_all()
113 cursor_path = (iv.get_cursor() or (None, None))[0]
115 if to_select == [] and prefix_matches:
116 # No matches, but put the cursor on the closest
117 if cursor_path in prefix_matches:
118 to_select = [cursor_path]
119 else:
120 to_select = [prefix_matches[0]]
122 if to_select:
123 for path in to_select:
124 iv.select_path(path)
125 if cursor_path not in to_select:
126 iv.set_cursor(to_select[0])
127 return
129 def get_entry_text(self):
130 return self.pattern
132 def get_button_label(self):
133 return self.pattern
135 class CommandArgument(Argument):
136 def get_button_label(self):
137 return "Open"
139 class ArgvView:
140 def __init__(self, hbox):
141 self.hbox = hbox
142 self.args = []
143 self.widgets = []
145 def set_args(self, args):
146 self.args = args
147 self.edit_arg = self.args[-1]
148 self.build()
150 def build(self):
151 for w in self.widgets:
152 w.destroy()
153 self.widgets = []
154 self.active_entry = None
156 for x in self.args:
157 if x is self.edit_arg:
158 arg = gtk.Entry()
159 arg.set_text(x.get_entry_text())
160 arg.connect('changed', x.entry_changed)
161 self.active_entry = arg
162 else:
163 arg = gtk.Button(x.get_button_label())
164 arg.set_relief(gtk.RELIEF_NONE)
165 arg.connect('clicked', lambda b: self.activate(x))
166 arg.show()
167 self.hbox.pack_start(arg, False, True, 0)
168 self.widgets.append(arg)
170 def activate(self, x):
171 self.edit_arg = x
172 self.build()
173 i = self.args.index(x)
174 self.widgets[i].grab_focus()
176 def key_press_event(self, kev):
177 if not self.active_entry:
178 return False
179 old_text = self.active_entry.get_text()
180 self.active_entry.grab_focus() # Otherwise it selects the added text
181 self.active_entry.event(kev)
182 return self.active_entry.get_text() != old_text
184 class ShellView:
185 terminal = None
186 user_seen_terminal_contents = False
188 def __init__(self, shell):
189 self.shell = shell
190 shell.view = self
192 builder = gtk.Builder()
193 builder.add_from_file(os.path.join(os.path.dirname(__file__), "ui.xml"))
194 self.window = builder.get_object('directory')
196 cd_parent = builder.get_object('cd-parent')
197 cd_parent.connect('activate', lambda a: self.shell.cd_parent())
199 # Must show window before adding icons, or we randomly get:
200 # The error was 'BadAlloc (insufficient resources for operation)'
201 self.window.show()
203 self.window_destroyed = tasks.Blocker('Window destroyed')
204 self.window.connect('destroy', lambda w: self.window_destroyed.trigger())
206 self.window.connect('key-press-event', self.key_press_event)
208 self.iv = builder.get_object('iconview')
209 self.iv.set_text_column(0)
210 self.iv.set_pixbuf_column(1)
211 self.iv.set_selection_mode(gtk.SELECTION_MULTIPLE)
213 self.iv.connect('item-activated', self.item_activated)
215 command_area = builder.get_object('command')
216 self.command_argv = ArgvView(command_area)
217 self.reset()
219 self.notebook = builder.get_object('notebook')
221 self.window.show_all()
223 def show_terminal(self):
224 if not self.terminal:
225 def terminal_contents_changed(vte):
226 self.user_seen_terminal_contents = False
228 def terminal_child_exited():
229 if self.user_seen_terminal_contents:
230 self.notebook.set_current_page(FILER_PAGE)
231 else:
232 self.terminal.feed('\r\nProcess complete. Press Return to return to filer view.\r\n')
233 self.waiting_for_return = True
234 return False
236 self.terminal = vte.Terminal()
237 self.terminal.connect('contents-changed', terminal_contents_changed)
238 self.terminal.connect('child-exited', lambda vte: gobject.timeout_add(100, terminal_child_exited))
239 self.terminal.show()
241 self.notebook.add(self.terminal)
242 self.notebook.set_current_page(TERMINAL_PAGE)
243 self.waiting_for_return = False
245 def reset(self):
246 self.command_argv.set_args([CommandArgument(self), Argument(self)])
247 self.iv.grab_focus()
248 self.iv.unselect_all()
250 def key_press_event(self, window, kev):
251 if self.terminal and self.terminal.flags() & gtk.HAS_FOCUS:
252 if kev.keyval in RETURN_KEYS and self.waiting_for_return:
253 self.notebook.set_current_page(FILER_PAGE)
254 return True
255 self.user_seen_terminal_contents = True
256 return False
258 if kev.keyval == keysyms.Escape:
259 self.reset()
260 return True
261 elif kev.keyval in RETURN_KEYS:
262 selected = self.iv.get_selected_items()
263 if len(selected) == 1:
264 self.iv.item_activated(selected[0])
265 return True
267 # Are we ready for special characters?
268 if self.command_argv.active_entry and self.command_argv.active_entry.flags() & gtk.HAS_FOCUS:
269 accept_special = True # TODO: check cursor is at end
270 else:
271 accept_special = True
273 if accept_special:
274 if kev.keyval == keysyms.comma:
275 self.command_argv.activate(self.command_argv.args[0])
276 return True
278 if self.iv.flags() & gtk.HAS_FOCUS:
279 if self.iv.event(kev):
280 # Handled by IconView (e.g. cursor motion)
281 return True
282 elif kev.keyval == keysyms.BackSpace:
283 self.shell.cd_parent()
284 else:
285 if not self.command_argv.key_press_event(kev):
286 self.iv.grab_focus() # Restore focus to IconView
287 return True
289 def run_in_terminal(self, argv, cwd):
290 self.show_terminal()
291 self.user_seen_terminal_contents = True
292 self.terminal.fork_command(argv[0], argv, None, cwd, False, False, False)
294 @tasks.async
295 def run(self):
296 self.iv.grab_focus()
297 while True:
298 tree_model = gtk_tree_model(self.shell.contents)
299 self.iv.set_model(tree_model)
300 if self.shell.contents:
301 self.iv.set_cursor((0,))
302 self.window.set_title(self.shell.file.get_uri())
303 blockers = [self.shell.updated, self.window_destroyed]
304 yield blockers
305 tasks.check(blockers)
306 if self.window_destroyed.happened:
307 break
309 def item_activated(self, iv, path):
310 self.reset()
312 tm = iv.get_model()
313 name = tm[tm.get_iter(path)][0]
314 self.shell.item_activated(name)