core.loader: fix bug with chardet module
[ranger.git] / ranger / core / loader.py
blob59d3e6c01f082a862a60b89ff6d0274f2ba90433
1 # Copyright (C) 2009, 2010, 2011 Roman Zimbelmann <romanz@lavabit.com>
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation, either version 3 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 from collections import deque
17 from time import time, sleep
18 from subprocess import Popen, PIPE
19 from ranger.core.shared import FileManagerAware
20 from ranger.ext.signals import SignalDispatcher
21 import os
22 import sys
23 import select
24 try:
25 import chardet
26 HAVE_CHARDET = True
27 except:
28 HAVE_CHARDET = False
30 class Loadable(object):
31 paused = False
32 def __init__(self, gen, descr):
33 self.load_generator = gen
34 self.description = descr
36 def get_description(self):
37 return self.description
39 def pause(self):
40 self.paused = True
42 def unpause(self):
43 try:
44 del self.paused
45 except:
46 pass
48 def destroy(self):
49 pass
52 class CommandLoader(Loadable, SignalDispatcher, FileManagerAware):
53 """
54 Run an external command with the loader.
56 Output from stderr will be reported. Ensure that the process doesn't
57 ever ask for input, otherwise the loader will be blocked until this
58 object is removed from the queue (type ^C in ranger)
59 """
60 finished = False
61 process = None
62 def __init__(self, args, descr, silent=False, read=False):
63 SignalDispatcher.__init__(self)
64 Loadable.__init__(self, self.generate(), descr)
65 self.args = args
66 self.silent = silent
67 self.read = read
68 self.stdout_buffer = ""
70 def generate(self):
71 null = open(os.devnull, 'r')
72 self.process = process = Popen(self.args,
73 stdout=PIPE, stderr=PIPE, stdin=null)
74 self.signal_emit('before', process=process, loader=self)
75 if self.silent and not self.read:
76 while process.poll() is None:
77 yield
78 sleep(0.03)
79 else:
80 py3 = sys.version >= '3'
81 selectlist = []
82 if self.read:
83 selectlist.append(process.stdout)
84 if not self.silent:
85 selectlist.append(process.stderr)
86 while process.poll() is None:
87 yield
88 try:
89 rd, _, __ = select.select(selectlist, [], [], 0.03)
90 if rd:
91 rd = rd[0]
92 if rd == process.stderr:
93 read = rd.readline()
94 if py3:
95 read = safeDecode(read)
96 if read:
97 self.fm.notify(read, bad=True)
98 elif rd == process.stdout:
99 read = rd.read(512)
100 if py3:
101 read = safeDecode(read)
102 if read:
103 self.stdout_buffer += read
104 except select.error:
105 sleep(0.03)
106 if not self.silent:
107 for l in process.stderr.readlines():
108 if py3:
109 l = safeDecode(l)
110 self.fm.notify(l, bad=True)
111 if self.read:
112 read = process.stdout.read()
113 if py3:
114 read = safeDecode(read)
115 self.stdout_buffer += read
116 null.close()
117 self.finished = True
118 self.signal_emit('after', process=process, loader=self)
120 def pause(self):
121 if not self.finished and not self.paused:
122 try:
123 self.process.send_signal(20)
124 except:
125 pass
126 Loadable.pause(self)
127 self.signal_emit('pause', process=self.process, loader=self)
129 def unpause(self):
130 if not self.finished and self.paused:
131 try:
132 self.process.send_signal(18)
133 except:
134 pass
135 Loadable.unpause(self)
136 self.signal_emit('unpause', process=self.process, loader=self)
138 def destroy(self):
139 self.signal_emit('destroy', process=self.process, loader=self)
140 if self.process:
141 self.process.kill()
144 def safeDecode(string):
145 try:
146 return string.decode("utf-8")
147 except (UnicodeDecodeError):
148 if HAVE_CHARDET:
149 return string.decode(chardet.detect(string)["encoding"])
150 else:
151 return ""
154 class Loader(FileManagerAware):
155 seconds_of_work_time = 0.03
156 throbber_chars = r'/-\|'
158 def __init__(self):
159 self.queue = deque()
160 self.item = None
161 self.load_generator = None
162 self.throbber_status = 0
163 self.rotate()
164 self.old_item = None
166 def rotate(self):
167 """Rotate the throbber"""
168 # TODO: move all throbber logic to UI
169 self.throbber_status = \
170 (self.throbber_status + 1) % len(self.throbber_chars)
171 self.status = self.throbber_chars[self.throbber_status]
173 def add(self, obj):
175 Add an object to the queue.
176 It should have a load_generator method.
178 while obj in self.queue:
179 self.queue.remove(obj)
180 self.queue.appendleft(obj)
182 def move(self, _from, to):
183 try:
184 item = self.queue[_from]
185 except IndexError:
186 return
188 del self.queue[_from]
190 if to == 0:
191 self.queue.appendleft(item)
192 if _from != 0:
193 self.queue[1].pause()
194 elif to == -1:
195 self.queue.append(item)
196 else:
197 raise NotImplementedError
199 def remove(self, item=None, index=None):
200 if item is not None and index is None:
201 for i, test in enumerate(self.queue):
202 if test == item:
203 index = i
204 break
205 else:
206 return
208 if index is not None:
209 if item is None:
210 item = self.queue[index]
211 if hasattr(item, 'unload'):
212 item.unload()
213 item.destroy()
214 del self.queue[index]
216 def work(self):
218 Load items from the queue if there are any.
219 Stop after approximately self.seconds_of_work_time.
221 while True:
222 # get the first item with a proper load_generator
223 try:
224 item = self.queue[0]
225 if item.load_generator is None:
226 self.queue.popleft()
227 else:
228 break
229 except IndexError:
230 return
232 self.rotate()
233 if item != self.old_item:
234 if self.old_item:
235 self.old_item.pause()
236 self.old_item = item
237 item.unpause()
239 end_time = time() + self.seconds_of_work_time
241 try:
242 while time() < end_time:
243 next(item.load_generator)
244 except StopIteration:
245 item.load_generator = None
246 self.queue.remove(item)
247 except Exception as err:
248 self.fm.notify(err)
250 def has_work(self):
251 """Is there anything to load?"""
252 return bool(self.queue)
254 def destroy(self):
255 while self.queue:
256 self.queue.pop().destroy()