deprecate SCViewHolder-layRight
[supercollider.git] / SCClassLibrary / Common / Streams / History.sc
bloba3ca93d0289f7345ba6271918311a1f3bf317b0b
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;
11         *timeStamp {
12                         // hope it works on linux?
13                 if (thisProcess.platform.isKindOf(UnixPlatform)) {
14                         ^Date.getDate.stamp
15                 } {
16                         // "// temporary kludge to fix Date's brokenness on windows".postln;
17                         ^Main.elapsedTime.round(0.01)
18                 };
19         }
21         *dateString {
22                         // hope it works on linux?
23                 if (thisProcess.platform.isKindOf(UnixPlatform)) {
24                         ^Date.getDate.asString
25                 } {
26                         // temporary kludge to fix Date's brokenness on windows
27                         ^"__date_time_please__"
28                 };
29         }
31         *initClass {
32                 Class.initClassTree(TaskProxy);
33                 this.makeLogFolder;
34                 current = this.new;
36                 listenFunc = { |str, val, func|
37                         if ( func.notNil
38                                         and: { str.notEmpty }
39                                         and: { str != "\n" }
40                                         and: { str.keep(7) != "History" })
41                                 {
42                                         if (this.verbose, { [str, val, func].postcs });
43                                         if (this.recordLocally, {
44                                                 this.enter(thisProcess.interpreter.cmdLine)
45                                         });
46                                         this.forwardFunc.value(str, val, func);
47                                 }
48                 };
49         }
51                 // top level interface :
53         *start {
54                 var interp = thisProcess.interpreter;
55                 if(started.not) {
56                         interp.codeDump = interp.codeDump.addFunc(listenFunc);
57                         this.hasMovedOn_(true);
58                         started = true;
59                         this.startLog;
60                 } {
61                         "History has started already.".postln;
62                 }
63         }
65         *end {
66                 thisProcess.interpreter.codeDump = thisProcess.interpreter.codeDump.removeFunc(listenFunc);
67                 this.endLog;
68                 this.hasMovedOn_(true);
69                 started = false;
70         }
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|
79                 var dateNow, now;
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;
89                 };
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);
97         }
99                 // forward to current for backwards compat...
100         *lines { ^current.lines }
101         *lineShorts { ^current.lineShorts }
102                 // editing
103         *removeAt {|index| current.removeAt(index) }
104         *removeLast { current.removeLast }
105         *keep {|num| current.keep(num) }
106         *drop {|num| current.drop(num) }
108                 // instance methods:
109         *new { |lines|
110                 ^super.new.init(lines);
111         }
113         hasMovedOn_ { |flag=true| hasMovedOn = flag; }
115         lines_ { |inLines|
116                 lines.array_(inLines);
117                 lineShorts.array_(lines.collect { |line| this.class.shorten(line) });
118                 keys.clear;
119                 keys.addAll(lines.collect(_[1]));
120         }
122         clear {
123                 lines = List[];
124                 lineShorts = List[];
125                 hasMovedOn = true;
126         }
127         *clear { current.clear }
129         init { |inLines|
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;
146                                         waittime.wait;
147                                         if (e.verbose) { code.postln };
148                                         code.compile.value;     // so it does not change cmdLine.
149                                 };
150                         };
151                         0.5.wait;
152                         "history is over.".postln;
153                 }).set(\startLine, 0, \endLine, 0);
154         }
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);
165                 player.play;
166         }
168         stop { player.stop; }
170         addLine { |now, authID, lineStr|
171                 var line = [ now, authID, lineStr ];
172                 if (lines.isEmpty) {
173                         lines.add(line);
174                         lineShorts.add(this.class.shorten(line));
175                 } {
176                         lines.addFirst(line);
177                         lineShorts.addFirst(this.class.shorten(line));
178                 };
179                 keys.add(authID);
180         }
181                 // simple editing
182         removeAt { |index|
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));              };
187                 hasMovedOn = true;
188         }
189         removeLast { this.removeAt(lines.size - 1) }
190         keep { |num|
191                 lines = lines.keep(num);
192                 lineShorts = lineShorts.keep(num);
193                 hasMovedOn = true;
194         }
195         drop { |num|
196                 lines = lines.drop(num);
197                 lineShorts = lineShorts.drop(num);
198                 hasMovedOn = true;
199         }
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);
210                 file.close;
211         }
213         *loadCS { |path, forward=false| current.loadCS(path, forward) }
215         loadCS { |path, forward=false|
216                 var file, ll;
217                 protect {
218                         file = File(path.standardizePath, "r");
219                         ll = file.readAllString;
220                 } {
221                         file.close;
222                 };
223                 ll !? {
224                         ll = ll.compile.value;
225                         if (forward) { ll = ll.reverse };
226                         this.lines_(ll)
227                 };
228         }
230                         // network setups support
231         *network { }
232         *localOn { recordLocally = true }
233         *localOff { recordLocally = false }
235                         // string formatting utils
236         storyString {
237                 var alone = lines.collectAs({ |line| line[1] }, IdentitySet).size == 1;
238                 var str;
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;
247                                 str = str ++
248                                 format("// - % - % \n",
249                                         this.class.formatTime(now),
250                                         if(alone) { "" } { "(" ++ id ++ ")" }
251                                 );
252                         if(cmdLine.find("\n").notNil and: { cmdLine[0] != $( }) {
253                                 cmdLine = format("(\n%\n);", cmdLine)
254                         };
255                         str = str ++ cmdLine ++ "\n\n";
256                 };
257                 ^str;
258         }
260         *saveStory { |path| current.saveStory(path) }
262         saveStory { |path|
263                 var file;
264                 path = path ?? { saveFolder ++ "History_" ++ this.class.timeStamp ++ ".scd" };
266                 file = File(path.standardizePath, "w");
267                 file.write(this.storyString);
268                 file.close;
269         }
271         *formatTime { arg val;
272                         var h, m, s;
273                         h = val div: (60 * 60);
274                         val = val - (h * 60 * 60);
275                         m = val div: 60;
276                         val = val - (m * 60);
277                         s = val;
278                         ^"%:%:%".format(h, m, s.round(0.01))
279         }
280         *unformatTime { arg str;
281                         var h, m, s;
282                         #h, m, s = str.split($:).collect(_.interpret);
283                         ^h * 60 + m * 60 + s
284         }
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];
294                 };
295                 // [startIndex, lastCharIndex].postln;
296                 ^str.copyRange(startIndex, lastCharIndex);
297         }
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);
306         }
308                 // not used yet
309         *getTimeFromString { arg str;
310                 var ts, i;
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]
316         }
317                 // not used yet
318         *asLines { arg str;
319                 var indices;
320                 indices = str.findAll("// -");
321                 ^str.clumps(indices.differentiate)
322         }
324         /*
325         // problem: interpreter cancels backslashes etc.
326         *stream { arg str, func;
327                 var lastTime=0, time;
328                 func = func ?? {
329                         {|str|
330                                 var dt = ~prev / str.size;
331                                 fork {
332                                         0.2.wait; // wait until result from last evaluation is printed
333                                         str.do {|char|
334                                                 char.post;
335                                                 dt.wait;
336                                         };
337                                         str.compile.value;
339                                 };
340                         }
342                 };
343                 ^Routine {
344                         this.asLines(str).do { |line|
345                                 time = this.getTimeFromString(line) ? lastTime;
346                                 (prev:lastTime, delta: time - lastTime, play: { func.(line); }).yield;
347                                 lastTime = time;
348                         }
349                 }
350         }
351         *play { arg str, clock;
352                 str = str ? Document.current.string;
353                 ^this.stream(str).asEventStreamPlayer.play(clock);
354         }
355         *playDocument {
357         }
358         */
360         *cmdPeriod { this.enter("// thisProcess.cmdPeriod"); }
362                                 // log file support - global only
363         *makeLogFolder {
364                 var supportDir = thisProcess.platform.userAppSupportDir;
365                 var specialFolder = supportDir ++ "/HistoryLogs";
367                 if (pathMatch(supportDir).isEmpty) { logFolder = ""; ^this };
369                 if (pathMatch(specialFolder).isEmpty) {
370                         specialFolder.mkdir;
371                         if (pathMatch(specialFolder).isEmpty) {
372                                 logFolder = supportDir; // if not there, put it in flat
373                         }
374                 } {
375                         logFolder = specialFolder;
376                 };
378                 // ("// History.logFolder:" +  logFolder).postln;
379         }
381         *showLogFolder { openOS(logFolder) }
383         *checkLogStarted {
384                 var isOpen;
385                 if (logFile.isNil) { this.startLog };
387                 isOpen = logFile.isOpen;
388                 ^if (isOpen.not) { this.startLog; ^logFile.isOpen } { true };
389         }
391         *startLog {
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;
401                 } {
402                         "// History: could not open logFile!".warn;
403                 };
404         }
406         *addToLog { |line|
407                 // add a single line
408                 if (this.checkLogStarted) {
409                         try {
410                                 logFile.write(line.asCompileString ++ ",\n")
411                         } {
412                                 "// History: could not write to logFile!".warn;
413                         }
414                 } {
415                         warn("// History: logFile is not open!");
416                 };
417         }
419         *endLog {
420                 // close file
421                 try { logFile.write( /*[*/ "];") };
422                 try { logFile.close; "// History.logFile closed.".inform; };
423         }
425         *showLogFile { Document.open(this.logPath) }
427         matchKeys { |key|
428                 var indices = [];
429                 if (key == \all) { ^(0..lines.size - 1) };
430                 if (key.isNil) { ^nil };
432                         // list of keys:
433                 if (key.isArray) {
434                         lines.do { |line, i| if (key.includes(line[1])) { indices = indices.add(i) } }
435                 } {
436                         lines.do { |line, i| if (line[1] == key) { indices = indices.add(i) } }
437                 };
438                 ^indices
439         }
441         matchString { |str, ignoreCase=true|
442                 var indices = [];
443                 if (str.notNil and: (str != "")) {
444                         lines.do { |line, i| if (line[2].find(str, ignoreCase).notNil) { indices = indices.add(i) } };
445                         ^indices
446                 } { ^nil }
447         }
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 }
457                 } {
458                         if (indicesS.notNil) { indicesS } { (0.. lines.size - 1) }
459                 };
460                 ^indicesFound
461         }
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 ...
470                 var docTitle;
471                 Platform.case(
472                         \windows, {
473                                 this.storyString.newTextWindow("History_documented");
474                         },
475                         {
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.
479                         }
480                 )
481         }
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;
489                         ^false
490                 };
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);
497                         };
498                         count = count + 1;
499                 };
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];
516                 };
517         }
518         *checkPath { |path|
519                 var ext = path.splitext[1];
520                 if ([\sc, \scd, \txt, \nil, \rtf].includes(ext.asSymbol)) {
521                         ^true
522                 } {
523                         warn("History: file format" + ext + "for story files likely not supported!                              Please use .txt, .scd, or other text format.");
524                         ^false
525                 };
526         }
527                 // load file saved with saveStory
528         *loadStory { |path| current.loadStory(path) }
530         loadStory { |path|
531                 var lines;
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.")
536                         } {
537                                 this.lines_(lines.reverse)
538                         }
539                 };
540         }
542         *rewrite { |path, open = true|
543                 var lines, time, repath, file2;
544                 lines = path.load;
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]);
550                 }.join($ );
552                 repath = path.splitext.collect { |str, i| str ++ ["_rewritten.", ""][i] }.join;
554                 file2 = File.open(repath, "w");
555                 file2.write("// History rewritten from" + time);
556                 lines.do { |line|
557                         var time, tag, code;
558                         #time, tag, code = line;
559                         file2.write("\n\n\n// when: % - who: % \n\n(\n%\n)\n".format(time, tag, code));
560                 };
561                 file2.close;
562                 if (open) { repath.openTextFile };
563         }