summaryrefslogtreecommitdiff
path: root/editty/source.py
diff options
context:
space:
mode:
authorJames E. Blair <corvus@gnu.org>2019-04-27 09:35:10 -0700
committerJames E. Blair <corvus@gnu.org>2019-04-27 09:35:10 -0700
commitf166277b69e07a942a70101a8d79032aac6be4d1 (patch)
treede4a4a8e631cbb3c3d36f2ccd9b1c75fb2dd9cb8 /editty/source.py
Initial commitHEAD0.0.1master
Diffstat (limited to 'editty/source.py')
-rw-r--r--editty/source.py177
1 files changed, 177 insertions, 0 deletions
diff --git a/editty/source.py b/editty/source.py
new file mode 100644
index 0000000..1a26407
--- /dev/null
+++ b/editty/source.py
@@ -0,0 +1,177 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2019 James E. Blair <corvus@gnu.org>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18import logging
19import time
20import struct
21import uuid
22
23import urwid
24
25class Frame:
26 def __init__(self, term, timeline_color, content=None, cursor=None):
27 if content is not None:
28 self.content = content
29 else:
30 self.content = [line[:] for line in term.content()]
31 if cursor is not None:
32 self.cursor = cursor
33 else:
34 self.cursor = term.term_cursor[:]
35 self.timeline_color = timeline_color
36 # TODO: cursor visibility, current color
37
38class SourceClip:
39 def __init__(self, size, title, file_type, stream_fn, timing_fn, timeline_color):
40 self.title = 'Untitled'
41 self.size = size
42 self.frames = []
43 self.times = []
44 self.length = 0.0
45 self.file_type = file_type
46 self.stream_fn = stream_fn
47 self.timing_fn = timing_fn
48 self.uuid = str(uuid.uuid4())
49 self.timeline_color = timeline_color
50
51 def addFrame(self, timecode, frame):
52 self.frames.append(frame)
53 self.times.append(timecode)
54 self.length = timecode
55
56 def toJSON(self):
57 return dict(type=self.file_type,
58 uuid=self.uuid,
59 size=self.size,
60 stream=self.stream_fn,
61 timing=self.timing_fn,
62 color=self.timeline_color)
63
64 @classmethod
65 def fromJSON(cls, data):
66 ft = getFileType(data['type'])
67 sc = ft.load(data['size'], data['stream'], data['timing'], data['color'])
68 sc.uuid = data['uuid']
69 return sc
70
71 def getFrames(self, start, end):
72 # In case we need to supply the frame before the start:
73 prev = None
74 yielded = False
75 for fi in zip(self.times, self.frames):
76 if end is not None and fi[0] > end:
77 if not yielded and prev is not None:
78 yield (start, prev[1])
79 return
80 if start is not None:
81 if fi[0] < start:
82 prev = fi
83 continue
84 if prev is not None and fi[0] > start:
85 yield (start, prev[1])
86 yielded = True
87 start = None
88 yield fi
89 yielded = True
90
91class FileType:
92 timing = False
93
94 def __init__(self):
95 self.log = logging.getLogger('file')
96
97 def _loadCanvas(self, size, stream_fn, timing_fn, timeline_color):
98 title = os.path.split(stream_fn)[-1]
99 source_clip = SourceClip(size, title, self.name, stream_fn, timing_fn, timeline_color)
100 class LoadWidget(urwid.Widget):
101 term_modes = urwid.TermModes()
102 def beep(self): pass
103 def set_title(self, title): pass
104 canv = urwid.TermCanvas(size[0], size[1], LoadWidget())
105 canv.modes.main_charset = urwid.vterm.CHARSET_UTF8
106
107 return (source_clip, canv)
108
109class ScriptFile(FileType):
110 name = 'Script'
111 timing = True
112
113 def load(self, size, stream_fn, timing_fn, timeline_color):
114 self.log.debug('Loading %s %s', stream_fn, timing_fn)
115 source_clip, canvas = self._loadCanvas(size, stream_fn, timing_fn, timeline_color)
116 start = time.time()
117 buffer_pos = 0
118 timecode = 0.0
119 with open(stream_fn, 'rb') as s:
120 stream = s.read()
121 i = stream.find(b'\n')
122 stream = stream[i+1:]
123 with open(timing_fn) as f:
124 for i, line in enumerate(f):
125 delay, count = line.strip().split(' ')
126 delay = float(delay)
127 count = int(count)
128 timecode += delay
129 data = stream[buffer_pos:buffer_pos+count]
130 canvas.addstr(data)
131 buffer_pos += count
132 source_clip.addFrame(timecode, Frame(canvas, timeline_color))
133 end = time.time()
134 self.log.debug('Finished loading %s', end-start)
135 return source_clip
136
137class TtyrecFile(FileType):
138 name = 'Ttyrec'
139
140 def load(self, size, stream_fn, timing_fn, timeline_color):
141 self.log.debug('Loading %s %s', stream_fn, timing_fn)
142 source_clip, canvas = self._loadCanvas(size, stream_fn, timing_fn, timeline_color)
143 with open(stream_fn, 'rb') as ttyrec_in:
144 start_time = None
145 while True:
146 header = ttyrec_in.read(12)
147 if not header:
148 self.log.debug("no header")
149 break
150 tc_secs, tc_usecs, dlen = struct.unpack('<III', header)
151 timecode = tc_secs + tc_usecs/1000000.0
152 if start_time is None:
153 # If the first frame is less than a year from the
154 # epoch, assume we are 0-based, otherwise, assume
155 # it's epoch-based and use the first timecode as
156 # the basis.
157 if timecode > 365*24*60*60:
158 start_time = timecode
159 else:
160 start_time = 0.0
161 data = ttyrec_in.read(dlen)
162 if len(data) != dlen:
163 raise Exception("short read")
164 canvas.addstr(data)
165 self.log.debug("Frame %0.6f %i" % (timecode, dlen))
166 source_clip.addFrame(timecode-start_time, Frame(canvas, timeline_color))
167 return source_clip
168
169all_types = [
170 TtyrecFile(),
171 ScriptFile(),
172]
173
174def getFileType(name):
175 for ft in all_types:
176 if ft.name == name:
177 return ft