1 History { // adc 2006, Birmingham; rewrite 2007.
3 classvar <>forwardFunc, <date, <startTimeStamp, <time0, listenFunc;
4 classvar <>verbose = false, <>recordLocally = true, <started=false;
6 classvar <>saveFolder = "~/Desktop/", <logFolder, <logFile, <logPath, <>keepsLog = true;
7 classvar <>current, <>maxShortLength=65;
9 var <lines, <lineShorts, <keys, <player, <hasMovedOn;
12 // hope it works on linux?
13 if (thisProcess.platform.isKindOf(UnixPlatform)) {
16 // "// temporary kludge to fix Date's brokenness on windows".postln;
17 ^Main.elapsedTime.round(0.01)
22 // hope it works on linux?
23 if (thisProcess.platform.isKindOf(UnixPlatform)) {
24 ^Date.getDate.asString
26 // temporary kludge to fix Date's brokenness on windows
27 ^"__date_time_please__"
32 Class.initClassTree(TaskProxy);
36 listenFunc = { |str, val, func|
40 and: { str.keep(7) != "History" })
42 if (this.verbose, { [str, val, func].postcs });
43 if (this.recordLocally, {
44 this.enter(thisProcess.interpreter.cmdLine)
46 this.forwardFunc.value(str, val, func);
51 // top level interface :
54 var interp = thisProcess.interpreter;
56 interp.codeDump = interp.codeDump.addFunc(listenFunc);
57 this.hasMovedOn_(true);
61 "History has started already.".postln;
66 thisProcess.interpreter.codeDump = thisProcess.interpreter.codeDump.removeFunc(listenFunc);
68 this.hasMovedOn_(true);
72 *hasMovedOn_ { |flag=true| current.hasMovedOn_(flag) }
73 *hasMovedOn { ^current.hasMovedOn }
75 *play { |start=0, end, verbose=true| ^current.play(start, end, verbose) }
76 *stop { ^current.stop }
78 *enter { |lineStr, id=\me|
80 lineStr = this.prettyString(lineStr);
82 if (lineStr.isEmpty) { ^this }; // nothing worth remembering
84 if (current.lines.isEmpty) { // get startTime if first entry
85 startTimeStamp = this.timeStamp;
86 date = this.dateString;
87 time0 = Main.elapsedTime;
91 this.hasMovedOn_(true);
92 now = (Main.elapsedTime - time0);
93 if (now < 1e-04) { now = 0 }; // on start
95 if (keepsLog) { this.addToLog([now, id, lineStr]) };
96 current.addLine(now, id, lineStr);
99 // forward to current for backwards compat...
100 *lines { ^current.lines }
101 *lineShorts { ^current.lineShorts }
103 *removeAt {|index| current.removeAt(index) }
104 *removeLast { current.removeLast }
105 *keep {|num| current.keep(num) }
106 *drop {|num| current.drop(num) }
110 ^super.new.init(lines);
113 hasMovedOn_ { |flag=true| hasMovedOn = flag; }
116 lines.array_(inLines);
117 lineShorts.array_(lines.collect { |line| this.class.shorten(line) });
119 keys.addAll(lines.collect(_[1]));
127 *clear { current.clear }
130 keys = IdentitySet.new;
131 this.clear.lines_(inLines);
133 player = TaskProxy.new.source_({ |e|
134 var linesSize, lineIndices, lastTimePlayed;
135 linesSize = lines.size;
137 if (linesSize > 0) { // reverse indexing
138 lineIndices = (e.endLine.min(linesSize) .. e.startLine.min(linesSize));
140 lineIndices.do { |index|
141 var time, id, code, waittime;
142 #time, id, code = lines[index];
144 waittime = time - (lastTimePlayed ? time);
145 lastTimePlayed = time;
147 if (e.verbose) { code.postln };
148 code.compile.value; // so it does not change cmdLine.
152 "history is over.".postln;
153 }).set(\startLine, 0, \endLine, 0);
156 makeCurrent { History.current = this; hasMovedOn = true }
157 isCurrent { ^this === History.current }
159 play { |start=0, end, verbose=true| // line numbers;
160 // starting from past 0 may not work.
161 start = start.clip(0, lines.lastIndex);
162 end = (end ? lines.lastIndex).clip(0, lines.lastIndex);
164 player.set(\startLine, start, \endLine, end, \verbose, verbose);
168 stop { player.stop; }
170 addLine { |now, authID, lineStr|
171 var line = [ now, authID, lineStr ];
174 lineShorts.add(this.class.shorten(line));
176 lines.addFirst(line);
177 lineShorts.addFirst(this.class.shorten(line));
183 if (index.isKindOf(Collection)) { index.sort.reverseDo (this.removeAt(_)); ^this };
185 if (index < lines.size) { // ignore out of range indices, keep lists synced.
186 [lines, lineShorts].do(_.removeAt(index)); };
189 removeLast { this.removeAt(lines.size - 1) }
191 lines = lines.keep(num);
192 lineShorts = lineShorts.keep(num);
196 lines = lines.drop(num);
197 lineShorts = lineShorts.drop(num);
200 // loading from and saving to files
201 *saveCS { |path, forward=false| current.saveCS(path, forward) }
202 saveCS { |path, forward=false|
203 var file, lines2write;
205 lines2write = if (forward) { lines.reverse } { lines };
206 path = path ?? { saveFolder ++ "history_" ++ this.class.timeStamp ++ ".scd" };
207 file = File(path.standardizePath, "w");
208 file.write(lines2write.asCompileString);
209 inform("History written to:" + path);
213 *loadCS { |path, forward=false| current.loadCS(path, forward) }
215 loadCS { |path, forward=false|
218 file = File(path.standardizePath, "r");
219 ll = file.readAllString;
224 ll = ll.compile.value;
225 if (forward) { ll = ll.reverse };
230 // network setups support
232 *localOn { recordLocally = true }
233 *localOff { recordLocally = false }
235 // string formatting utils
237 var alone = lines.collectAs({ |line| line[1] }, IdentitySet).size == 1;
240 str = "///////////////////////////////////////////////////\n";
241 str = str ++ format("// History, as it was on %.\n", this.class.dateString);
242 str = str ++ "///////////////////////////////////////////////////\n\n";
244 lines.reverseDo { |x|
245 var now, id, cmdLine;
246 #now, id, cmdLine = x;
248 format("// - % - % \n",
249 this.class.formatTime(now),
250 if(alone) { "" } { "(" ++ id ++ ")" }
252 if(cmdLine.find("\n").notNil and: { cmdLine[0] != $( }) {
253 cmdLine = format("(\n%\n);", cmdLine)
255 str = str ++ cmdLine ++ "\n\n";
260 *saveStory { |path| current.saveStory(path) }
264 path = path ?? { saveFolder ++ "History_" ++ this.class.timeStamp ++ ".scd" };
266 file = File(path.standardizePath, "w");
267 file.write(this.storyString);
271 *formatTime { arg val;
273 h = val div: (60 * 60);
274 val = val - (h * 60 * 60);
276 val = val - (m * 60);
278 ^"%:%:%".format(h, m, s.round(0.01))
280 *unformatTime { arg str;
282 #h, m, s = str.split($:).collect(_.interpret);
286 *prettyString { |str|
287 // remove returns at beginning or end of the string
288 var startIndex = str.detectIndex({ |ch| ch != $\n });
289 var endChar = str.last;
290 var lastCharIndex = str.lastIndex;
291 while { endChar == $\n } {
292 lastCharIndex = lastCharIndex - 1;
293 endChar = str[lastCharIndex];
295 // [startIndex, lastCharIndex].postln;
296 ^str.copyRange(startIndex, lastCharIndex);
299 // convert to shortline for gui
300 *shorten { |line, maxLength|
301 var time, id, lineStr, head, length;
302 #time, id, lineStr = line;
303 head = (this.formatTime(time) + "-" + id + "- ");
304 maxLength = maxLength ? maxShortLength;
305 ^head ++ lineStr.keep(maxLength - head.size);
309 *getTimeFromString { arg str;
311 if(str.beginsWith("// - ").not) { ^nil };
312 i = str.find(" - ", offset: 4);
313 if(i.isNil) { i = 10 }; // assume it's ok.
314 ts = str[5..i+2].postln.split($:).collect(_.asFloat);
315 ^ts[0] * (60 * 60) + (ts[1] * 60) + ts[2]
320 indices = str.findAll("// -");
321 ^str.clumps(indices.differentiate)
325 // problem: interpreter cancels backslashes etc.
326 *stream { arg str, func;
327 var lastTime=0, time;
330 var dt = ~prev / str.size;
332 0.2.wait; // wait until result from last evaluation is printed
344 this.asLines(str).do { |line|
345 time = this.getTimeFromString(line) ? lastTime;
346 (prev:lastTime, delta: time - lastTime, play: { func.(line); }).yield;
351 *play { arg str, clock;
352 str = str ? Document.current.string;
353 ^this.stream(str).asEventStreamPlayer.play(clock);
360 *cmdPeriod { this.enter("// thisProcess.cmdPeriod"); }
362 // log file support - global only
364 var supportDir = thisProcess.platform.userAppSupportDir;
365 var specialFolder = supportDir ++ "/HistoryLogs";
367 if (pathMatch(supportDir).isEmpty) { logFolder = ""; ^this };
369 if (pathMatch(specialFolder).isEmpty) {
371 if (pathMatch(specialFolder).isEmpty) {
372 logFolder = supportDir; // if not there, put it in flat
375 logFolder = specialFolder;
378 // ("// History.logFolder:" + logFolder).postln;
381 *showLogFolder { openOS(logFolder) }
385 if (logFile.isNil) { this.startLog };
387 isOpen = logFile.isOpen;
388 ^if (isOpen.not) { this.startLog; ^logFile.isOpen } { true };
392 var timeStamp = this.timeStamp;
393 var timeString = this.dateString;
394 // open file with current date
395 logPath = logFolder ++ "/log_History_" ++ timeStamp ++ ".scd";
396 logFile = File(logPath, "w");
397 if (logFile.isOpen) {
398 logFile.write(format("// History, as it was on %.\n\n",
399 timeString) ++ "[\n" /*]*/ );
400 "// History.logFile opened.".inform;
402 "// History: could not open logFile!".warn;
408 if (this.checkLogStarted) {
410 logFile.write(line.asCompileString ++ ",\n")
412 "// History: could not write to logFile!".warn;
415 warn("// History: logFile is not open!");
421 try { logFile.write( /*[*/ "];") };
422 try { logFile.close; "// History.logFile closed.".inform; };
425 *showLogFile { Document.open(this.logPath) }
429 if (key == \all) { ^(0..lines.size - 1) };
430 if (key.isNil) { ^nil };
434 lines.do { |line, i| if (key.includes(line[1])) { indices = indices.add(i) } }
436 lines.do { |line, i| if (line[1] == key) { indices = indices.add(i) } }
441 matchString { |str, ignoreCase=true|
443 if (str.notNil and: (str != "")) {
444 lines.do { |line, i| if (line[2].find(str, ignoreCase).notNil) { indices = indices.add(i) } };
449 indicesFor { |keys, string=""|
450 var indicesK, indicesS, indicesFound;
451 indicesK = this.matchKeys(keys);
452 indicesS = this.matchString(string);
453 // [\indicesK, indicesK, \indicesS, indicesS].postln;
455 indicesFound = if (indicesK.notNil) {
456 if (indicesS.notNil) { indicesK.sect(indicesS) } { indicesK }
458 if (indicesS.notNil) { indicesS } { (0.. lines.size - 1) }
463 *makeWin { |where, textHeight=12| ^current.makeWin(where, textHeight) }
465 makeWin { |where, textHeight=12| ^HistoryGui(this, where, textHeight) }
467 *document { current.document }
469 document { arg title=""; // platform dependent ...
473 this.storyString.newTextWindow("History_documented");
476 docTitle = title ++ Date.getDate.format("%Y-%m-%e-%Hh%M-History");
477 Document.new(docTitle, this.storyString)
478 .path_(docTitle); // don't lose title.
483 *readFromDoc { |path|
484 var file, line, count = 0, lineStrings = [], comLineIndices = [], splitPoints;
485 file = File(path.standardizePath, "r");
487 if (file.isOpen.not) {
488 ("History: file" + path + "not found!").warn;
491 // read all lines, keep indices of commentlines
492 while { line = file.getLine; line.notNil } {
493 lineStrings = lineStrings.add(line);
494 if (line.beginsWith("// - ")) {
495 splitPoints = line.findAll(" - ");
496 comLineIndices = comLineIndices.add([count] ++ splitPoints);
501 ^comLineIndices.collect { |list, i|
502 var lineIndex, sep1, sep2, nextComIndex;
503 var comLine, timeStr, time, key, codeStr;
504 #lineIndex, sep1, sep2 = list;
506 comLine = lineStrings[lineIndex];
507 timeStr = comLine.copyRange(sep1 + 3, sep2 - 1);
508 time = History.unformatTime(timeStr);
510 key = comLine.copyToEnd(sep2 + 3).select(_.isAlphaNum).asSymbol;
511 nextComIndex = (comLineIndices[i + 1] ? [lineStrings.size]).first;
513 codeStr = lineStrings.copyRange(lineIndex + 1, nextComIndex - 2).join;
515 [time, key, codeStr];
519 var ext = path.splitext[1];
520 if ([\sc, \scd, \txt, \nil, \rtf].includes(ext.asSymbol)) {
523 warn("History: file format" + ext + "for story files likely not supported! Please use .txt, .scd, or other text format.");
527 // load file saved with saveStory
528 *loadStory { |path| current.loadStory(path) }
532 if (this.class.checkPath(path)) {
533 lines = this.class.readFromDoc(path);
534 if (lines == false) {
535 warn("History: no lines, so could not be loaded.")
537 this.lines_(lines.reverse)
542 *rewrite { |path, open = true|
543 var lines, time, repath, file2;
546 if (lines.isNil) { "no history, no future.".warn; ^this };
548 time = path.basename.splitext.first.keep(-13).split($_).collect { |str, i|
549 str.clump(2).join("-:"[i]);
552 repath = path.splitext.collect { |str, i| str ++ ["_rewritten.", ""][i] }.join;
554 file2 = File.open(repath, "w");
555 file2.write("// History rewritten from" + time);
558 #time, tag, code = line;
559 file2.write("\n\n\n// when: % - who: % \n\n(\n%\n)\n".format(time, tag, code));
562 if (open) { repath.openTextFile };