[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: [sc-users] ToolsMenu and Server gui niceties
So, you mean that i need something more then files from toolsMenu.zip file? i
uzipped it, and put toolsMenu folder inside of extensions folder. All needed
quarks are also installed.
I'm sorry, but i can't find any attachment in your post, only link to zip
file...
Thank you!
Miguel C. Negrão wrote:
>
> then you probably haven't put the file in your extensions folder. The
> file is in the original post as an attachment.
>
> Miguel
>
> Timur Kuyanov escreveu:
>> Yes, now i have Tools menu next to help, but still no gui...ERROR:
>> Message
>> 'makeWindows' not understood.
>>
>>
>> Miguel C. Negrão wrote:
>>> Hi
>>>
>>> Timur Kuyanov escreveu:
>>>> Hello!
>>>>
>>>> Interesting to try your tools, but i'm new to SC, can you explain steps
>>>> need
>>>> to be taken to get it work?
>>>>
>>>> I've put ToolsMenu folder to SCClassLibrary folder, now have this in
>>>> post
>>>> window:
>>>>
>>> Don't worry about it, it's not a problem, I got millions of those.
>>>
>>>> And anyway, i don't really understand what i need to do to use it? is
>>>> it
>>>> supposed to appear in menu of SC itself?
>>> Yes, it should appear in the menu next to help. Are you placing this in
>>> your startup file ? (you can change the paths to something else off
>>> course)
>>>
>>> ToolsMenu.add(
>>> [
>>> Platform.userAppSupportDir +/+ "startup.rtf",
>>> Platform.userAppSupportDir +/+ Extensions/"
>>> ];
>>> );
>>>
>>>
>>> What code i need to put in
>>>> startup.rtf to get "black gui"?))
>>>>
>>> Server.makeWindows. altogh even if you don't put this you will get the
>>> black server guis, just not the main volume slider. If you have place
>>> the class file in your extension folder it will overide the normal
>>> server window. Are you not seeing the black gui ? Where did you put the
>>> class files (.sc) ?
>>>
>>> Miguel
>>>
>>>
>>>> Sorry for stupid questions.
>>>>
>>>> Thanx!
>>>>
>>>>
>>>> Miguel C. Negrão wrote:
>>>>> Hi
>>>>>
>>>>> I've made a class to add some common used functions of supercollider
>>>>> in
>>>>> a menu using SCMenuItem.
>>>>> It allows to choose soundcard for each server or all at the same time,
>>>>> EQ, record window, auto sintax colorize on current document, color
>>>>> picker, open quarks, Init Midi, check current midi messages, run a
>>>>> file
>>>>> with cmd^r .
>>>>> Finally it adds a menu tree with all files in predetermined folders
>>>>> for easy access. When working on a project I'm always editing the same
>>>>> files, closing and opening the files, and I got fed up and decided to
>>>>> just put them in the menu bar for easy access.
>>>>> I hope this might be useful for someone else, I think specially
>>>>> newbies will appreciate being able to change the sound card (thanks
>>>>> for
>>>>> someone who posted the code in the list) and record from a gui (thanks
>>>>> wouter for ServerRecordWindow !).
>>>>>
>>>>> http://www.friendlyvirus.org/artists/zlb/code/ToolsMenu.zip
>>>>>
>>>>> Also included here is a nice minimal black gui for the server window I
>>>>> got a from a friend and that I refined a bit. It puts all server guis
>>>>> as
>>>>> a black strip in the bottom of the screen plus a big fader that
>>>>> changes
>>>>> the volume of all servers. To use this addon one must call
>>>>> Server.makeWindows each time a server is created and in the
>>>>> startup.rtf.
>>>>> It uses the gui version of queryAllNodes by Scott Wilson that I added
>>>>> as
>>>>> the method queryAllNodesGui.
>>>>>
>>>>> I hope this is useful.
>>>>> --
>>>>> Miguel Negrão // ZLB
>>>>> http://www.friendlyvirus.org/artists/zlb/
>>>>>
>>>>>
>>>>>
>>>>>
>>>>>
>>>>> + Server {
>>>>>
>>>>> // shortcut server record
>>>>> rek {|chans=1, filename="test.aif", format="int24"|
>>>>> var path, sched;
>>>>> path = "~/Desktop/".standardizePath ++ filename;
>>>>> this.recChannels = chans;
>>>>> this.recSampleFormat = format;
>>>>> this.prepareForRecord( path );
>>>>> AppClock.sched(1.0, { this.record; nil });
>>>>> }
>>>>>
>>>>> stoprek { this.stopRecording }
>>>>>
>>>>> *makeWindows{
>>>>> var window,w;
>>>>> var bgColor= Color.grey(0.0, 0.9);
>>>>> var stringColor= Color.grey(0.8);
>>>>> var runningColor= Color.magenta;
>>>>> var bootingColor= Color.yellow(0.9);
>>>>> var bundlingColor= Color.new255(237, 157, 196);
>>>>> var width, font;
>>>>>
>>>>> Server.set.do({|se| if(se.window != nil)
>>>>> {
>>>>> Routine({
>>>>> se.window.close;
>>>>> 0.1.wait;
>>>>> se.makeWindow;
>>>>> }).play(AppClock)
>>>>> }{
>>>>> se.makeWindow
>>>>> }});
>>>>>
>>>>> width = SCWindow.screenBounds.width - 2;
>>>>> w = window = Window( "Main Volume",
>>>>> Rect(1, 1, width, 19),
>>>>> resizable: false, border: false);
>>>>> w.view.background_(bgColor);
>>>>> w.view.decorator = FlowLayout(w.view.bounds);
>>>>> w.front;
>>>>> Slider.new(w,Rect(0, 0, width-2, 16))
>>>>> .action_{|v| Server.set.do{ |server|
>>>>> server.volume.volume_([server.volume.min,server.volume.max,
>>>>> \db].asSpec.map(v.value))
>>>>> }
>>>>> }
>>>>> .canFocus_(false);
>>>>>
>>>>> }
>>>>> // server window customize
>>>>> makeWindow { arg w;
>>>>> var active, booter, killer, makeDefault, running, booting, stopped,
>>>>> bundling;
>>>>> var scoper;
>>>>> var countsViews, ctlr;
>>>>> var dumping=false, label, gui, volumeNum;
>>>>>
>>>>> var bgColor= Color.grey(0.0, 0.9);
>>>>> var stringColor= Color.grey(0.8);
>>>>> var runningColor= Color.magenta;
>>>>> var bootingColor= Color.yellow(0.9);
>>>>> var bundlingColor= Color.new255(237, 157, 196);
>>>>> var hideLocal= false;
>>>>> var hideInternal= false;
>>>>> var hideButt;
>>>>> var width, font;
>>>>>
>>>>> gui = GUI.current;
>>>>>
>>>>> if (window.notNil, { ^window.front });
>>>>>
>>>>> width = SCWindow.screenBounds.width - 2;
>>>>> font = Font("Monaco", 9);
>>>>>
>>>>> if(w.isNil) {
>>>>> label = name.asString + "server";
>>>>> w = window = Window( label,
>>>>> Rect(1,
>>>>> ((named.values.size-1)-named.values.indexOf(this)+1)*20+1,
>>>>> width, 19),
>>>>> resizable: false, border: false);
>>>>> w.view.background_(bgColor);
>>>>> w.view.decorator = FlowLayout(w.view.bounds);
>>>>> } { label = w.name };
>>>>>
>>>>> active = StaticText(w, Rect(0, 0, 78, 24));
>>>>> active.string = this.name.asString;
>>>>> active.align = \center;
>>>>> active.font = font;
>>>>> active.background = Color.clear;
>>>>> if(serverRunning,running,stopped);
>>>>>
>>>>> hideButt= Button(w, Rect(0, 0, 19, 19));
>>>>> hideButt.states= [["x", stringColor, Color.clear], ["", stringColor,
>>>>> Color.clear]];
>>>>> hideButt.action_({|view|
>>>>> if(view.value==1, {
>>>>> w.bounds_(Rect(1,
>>>>> ((named.values.size-1)-named.values.indexOf(this)+1)*20+1, 108, 19));
>>>>> w.view.children.do{|x|
>>>>> if([active, hideButt].includes(x).not, {
>>>>> x.visible= false;
>>>>> });
>>>>> };
>>>>> }, {
>>>>> w.bounds_(Rect(1,
>>>>> ((named.values.size-1)-named.values.indexOf(this)+1)*20+1, width,
>>>>> 19));
>>>>> w.view.children.do{|x| x.visible= true};
>>>>> });
>>>>> });
>>>>> if(isLocal&&hideLocal, { hideButt.valueAction_(1) });
>>>>> if(this.name==\internal&&hideInternal, { hideButt.valueAction_(1)
>>>>> });
>>>>>
>>>>>
>>>>> if(isLocal,{
>>>>> booter = Button(w, Rect(0, 0, 48, 24));
>>>>> booter.states = [["Boot", stringColor, Color.clear],
>>>>> ["Quit", stringColor, Color.clear]];
>>>>>
>>>>> booter.action = { arg view;
>>>>> if(view.value == 1, {
>>>>> booting.value;
>>>>> this.boot;
>>>>> });
>>>>> if(view.value == 0,{
>>>>> this.quit;
>>>>> });
>>>>> };
>>>>> booter.setProperty(\value,serverRunning.binaryValue);
>>>>>
>>>>> killer = Button(w, Rect(0,0, 24, 24));
>>>>> killer.states = [["K", stringColor, Color.clear]];
>>>>>
>>>>> killer.action = { Server.killAll };
>>>>> });
>>>>>
>>>>>
>>>>> makeDefault = Button(w, Rect(0, 0, 80, 24));
>>>>> makeDefault.states = [["-> default", stringColor, Color.clear]];
>>>>> makeDefault.action = {
>>>>> thisProcess.interpreter.s = this;
>>>>> Server.default = this;
>>>>> };
>>>>>
>>>>> w.view.keyDownAction = { arg ascii, char;
>>>>> var startDump, stopDump, stillRunning;
>>>>>
>>>>> case
>>>>> {char === $n} { this.queryAllNodesGui }
>>>>> {char === $l} { this.meter }
>>>>> {char === $N} { this.queryAllNodes(true) }
>>>>> {char === $ } { if(serverRunning.not) { this.boot } }
>>>>> {char === $s and: { Stethoscope.isValidServer( this )}} {
>>>>> this.scope
>>>>> }
>>>>> {char == $d} {
>>>>> if(this.isLocal or: { this.inProcess }) {
>>>>> stillRunning = {
>>>>> SystemClock.sched(0.2, { this.stopAliveThread });
>>>>> };
>>>>> startDump = {
>>>>> this.dumpOSC(1);
>>>>> this.stopAliveThread;
>>>>> dumping = true;
>>>>> w.name = "dumping osc: " ++ name.asString;
>>>>> CmdPeriod.add(stillRunning);
>>>>> };
>>>>> stopDump = {
>>>>> this.dumpOSC(0);
>>>>> this.startAliveThread;
>>>>> dumping = false;
>>>>> w.name = label;
>>>>> CmdPeriod.remove(stillRunning);
>>>>> };
>>>>> if(dumping, stopDump, startDump)
>>>>> } {
>>>>> "cannot dump a remote server's messages".inform
>>>>> }
>>>>>
>>>>> };
>>>>> };
>>>>>
>>>>> if (isLocal, {
>>>>>
>>>>> running = {
>>>>> active.stringColor_(runningColor);
>>>>> booter.setProperty(\value,1);
>>>>> };
>>>>> stopped = {
>>>>> active.stringColor_(stringColor);
>>>>> booter.setProperty(\value,0);
>>>>> };
>>>>> booting = {
>>>>> active.stringColor_(bootingColor);
>>>>> };
>>>>> bundling = {
>>>>> active.stringColor_(bundlingColor);
>>>>> booter.setProperty(\value,1);
>>>>> };
>>>>>
>>>>> w.onClose = {
>>>>> window = nil;
>>>>> ctlr.remove;
>>>>> };
>>>>> },{
>>>>> running = {
>>>>> active.stringColor = runningColor;
>>>>> active.background = Color.red;
>>>>> };
>>>>> stopped = {
>>>>> active.stringColor = stringColor;
>>>>> active.background = Color.black;
>>>>> };
>>>>> booting = {
>>>>> active.stringColor = bootingColor;
>>>>> active.background = Color.yellow;
>>>>> };
>>>>>
>>>>> bundling = {
>>>>> active.stringColor = bundlingColor;
>>>>> active.background = Color.red(0.5);
>>>>> booter.setProperty(\value,1);
>>>>> };
>>>>>
>>>>> w.onClose = {
>>>>> // but do not remove other responders
>>>>> this.stopAliveThread;
>>>>> ctlr.remove;
>>>>> };
>>>>> });
>>>>> if(serverRunning,running,stopped);
>>>>>
>>>>>
>>>>> countsViews =
>>>>> #[
>>>>> "Avg CPU :", "Peak CPU :",
>>>>> "UGens :", "Synths :", "Groups :", "SynthDefs :"
>>>>> ].collect({ arg name, i;
>>>>> var label,numView, pctView;
>>>>> label = StaticText(w, Rect(0,0, 80, 14));
>>>>> label.stringColor_(stringColor);
>>>>> label.string = name;
>>>>> label.align = \right;
>>>>>
>>>>> if (i < 2, {
>>>>> numView = StaticText(w, Rect(0,0, 38, 14));
>>>>> numView.stringColor_(stringColor);
>>>>> numView.string = "?";
>>>>> numView.align = \left;
>>>>>
>>>>> pctView = StaticText(w, Rect(0,0, 12, 14));
>>>>> pctView.stringColor_(stringColor);
>>>>> pctView.string = "%";
>>>>> pctView.align = \left;
>>>>> },{
>>>>> numView = StaticText(w, Rect(0,0, 50, 14));
>>>>> numView.stringColor_(stringColor);
>>>>> numView.string = "?";
>>>>> numView.align = \left;
>>>>> });
>>>>> numView
>>>>> });
>>>>>
>>>>> if(isLocal or: { options.remoteControlVolume }) {
>>>>> {
>>>>> var volSpec, cpVol;
>>>>> var volumeSlider, muteButton, muteActions, volController;
>>>>> muteActions = [{this.unmute}, {this.mute}];
>>>>> volSpec = [volume.min, volume.max, \db].asSpec;
>>>>>
>>>>> StaticText.new(w, Rect(0,0, 44, 18))
>>>>> .font_(font)
>>>>> .stringColor_(stringColor)
>>>>> .string_(" volume :");
>>>>>
>>>>> muteButton = gui.button.new(w, Rect(0, 0, 20, 16))
>>>>> .font_(font)
>>>>> .canFocus_(false)
>>>>> .states_([
>>>>> ["M", stringColor, Color.clear],
>>>>> ["M", Color.black, Color.red.alpha_(0.3)]
>>>>> ])
>>>>> .action_({arg me;
>>>>> this.serverRunning.if({
>>>>> muteActions[me.value].value;
>>>>> }, {
>>>>> "The server must be booted to mute it".warn;
>>>>> me.value_(0);
>>>>> })
>>>>> });
>>>>>
>>>>> volumeNum = gui.numberBox.new(w, Rect(0, 0, 28, 15))
>>>>> .font_(font)
>>>>> .value_(0.0)
>>>>> .align_(\center)
>>>>> .stringColor_(stringColor)
>>>>> .action_({arg me;
>>>>> var newdb;
>>>>> newdb = me.value.clip(-90, 6);
>>>>> this.volume_(newdb);
>>>>> volumeSlider.value_(volSpec.unmap(newdb));
>>>>> });
>>>>>
>>>>> volumeSlider = gui.slider.new(w, Rect(0, 0, 172, 16))
>>>>> .value_(volSpec.unmap(0))
>>>>> .onClose_{volController.remove}
>>>>> .action_({arg me;
>>>>> var newdb;
>>>>> newdb = volSpec.map(me.value).round(0.1);
>>>>> this.volume_(newdb);
>>>>> volumeNum.value_(newdb);
>>>>> })
>>>>> .keyDownAction_({arg slider, char, modifiers, unicode, keycode;
>>>>> if (char == $], { slider.increment; });
>>>>> if (char == $[, { slider.decrement; });
>>>>> if (unicode == 16rF700, { slider.increment; });
>>>>> if (unicode == 16rF703, { slider.increment; });
>>>>> if (unicode == 16rF701, { slider.decrement; });
>>>>> if (unicode == 16rF702, { slider.decrement; }); nil;
>>>>> });
>>>>> volController = SimpleController(volume)
>>>>> .put(\amp, {|changer, what, vol|
>>>>> {
>>>>> volumeNum.value_(vol.round(0.01));
>>>>> volumeSlider.value_(volSpec.unmap(vol));
>>>>> }.defer
>>>>> })
>>>>> .put(\mute, {|changer, what, flag|
>>>>> {
>>>>> muteButton.value_(flag.binaryValue);
>>>>> }.defer
>>>>> })
>>>>> .put(\ampRange, {|changer, what, min, max|
>>>>> volSpec = [min, max, \db].asSpec;
>>>>> volumeSlider.value_(volSpec.unmap(volume.volume));
>>>>> })
>>>>> }.value;
>>>>> };
>>>>>
>>>>>
>>>>> w.front;
>>>>>
>>>>> ctlr = SimpleController(this)
>>>>> .put(\serverRunning, { if(serverRunning,running,stopped) })
>>>>> .put(\counts,{
>>>>> countsViews.at(0).string = avgCPU.round(0.1);
>>>>> countsViews.at(1).string = peakCPU.round(0.1);
>>>>> countsViews.at(2).string = numUGens;
>>>>> countsViews.at(3).string = numSynths;
>>>>> countsViews.at(4).string = numGroups;
>>>>> countsViews.at(5).string = numSynthDefs;
>>>>> })
>>>>> .put(\bundling, bundling);
>>>>> this.startAliveThread;
>>>>>
>>>>> w.view.children.do{|view|
>>>>> view.bounds_(Rect(view.bounds.left, view.bounds.top,
>>>>> view.bounds.width,
>>>>> 14));
>>>>> if(view.respondsTo(\font)) { view.font_(font) };
>>>>> view.canFocus_(false);
>>>>> };
>>>>> }
>>>>>
>>>>> queryAllNodesGui {
>>>>> var resp, done = false;
>>>>>
>>>>> // msg[1] controls included
>>>>> // msg[2] nodeID of queried group
>>>>> // initial number of children
>>>>> resp = OSCresponderNode(this.addr, '/g_queryTree.reply', { arg time,
>>>>> responder, msg;
>>>>>
>>>>> //var finalEvent;
>>>>> var i = 2, j, controls, printControls = false, dumpFunc;
>>>>> if(msg[1] != 0, {printControls = true});
>>>>> dumpFunc = {|numChildren|
>>>>> var event, children;
>>>>> event = ().group;
>>>>> event.id = msg[i];
>>>>> event.instrument = nil; // need to know it's a group
>>>>> i = i + 2;
>>>>> children = Array.fill(numChildren, {
>>>>> var id, child;
>>>>> // i = id
>>>>> // i + 1 = numChildren
>>>>> // i + 2 = def (if synth)
>>>>> id = msg[i];
>>>>> if(msg[i+1] >=0, {
>>>>> child = dumpFunc.value(msg[i+1]);
>>>>> }, {
>>>>> j = 4;
>>>>> child = ().synth.instrument_(msg[i+2]);
>>>>> if(printControls, {
>>>>>
>>>>> controls = ();
>>>>> msg[i+3].do({
>>>>> controls[msg[i + j]] = msg[i + j + 1];
>>>>> j = j + 2;
>>>>> });
>>>>> child.controls = controls;
>>>>> i = i + 4 + (2 * controls.size);
>>>>> }, {i = i + 3 });
>>>>> });
>>>>> child.id = id;
>>>>> });
>>>>> event.children = children;
>>>>> event;
>>>>> };
>>>>> ~finalEvent = dumpFunc.value(msg[3]);
>>>>>
>>>>> done = true;
>>>>> {
>>>>> var collectChildren, levels, countSize;
>>>>> var window, view, bounds;
>>>>> var tabSize = 25;
>>>>> collectChildren = {|group|
>>>>> group.children.collect({|child|
>>>>> if(child.children.notNil,{
>>>>> child.id -> collectChildren.value(child);
>>>>> }, {
>>>>> child.id -> child.instrument;
>>>>> });
>>>>> });
>>>>> };
>>>>> levels = collectChildren.value(~finalEvent);
>>>>>
>>>>> countSize = {|array|
>>>>> var size = 0;
>>>>> array.do({|elem|
>>>>> if(elem.value.isArray, { size = size +
>>>>> countSize.value(elem.value) + 2}, {size = size + 1;});
>>>>> });
>>>>> size
>>>>> };
>>>>> //countSize.value(levels);
>>>>>
>>>>> window = Window.new(this.asString,scroll:true).front;
>>>>>
>>>>> window.view.hasHorizontalScroller_(false).background_(Color.black);
>>>>>
>>>>> bounds = Rect(0, 0, 400, tabSize * (countSize.value(levels)
>>>>> +
>>>>> 2));
>>>>> view = UserView.new(window, bounds);
>>>>>
>>>>> view.drawFunc = {
>>>>> var xtabs = 0, ytabs = 0, drawFunc;
>>>>>
>>>>> drawFunc = {|group|
>>>>> var thisSize, rect, endYTabs;
>>>>> xtabs = xtabs + 1;
>>>>> ytabs = ytabs + 1;
>>>>> group.do({|node|
>>>>> if(node.value.isArray, {
>>>>>
>>>>> thisSize = countSize.value(node);
>>>>> endYTabs = ytabs + thisSize + 0.2;
>>>>> rect = Rect(xtabs * tabSize,
>>>>> ytabs * tabSize,
>>>>> window.view.bounds.width - (xtabs *
>>>>> tabSize
>>>>> * 2),
>>>>> thisSize * tabSize;
>>>>> );
>>>>> Pen.fillColor = Color.green.alpha_(0.5);
>>>>> Pen.fillRect(rect);
>>>>> Pen.strokeRect(rect);
>>>>> (" Group" + node.key.asString + (node.key ==
>>>>> 1).if("- default group", "")).drawInRect(rect,
>>>>> Font("Helvetica", 11),
>>>>> Color.black);
>>>>> drawFunc.value(node.value);
>>>>> ytabs = endYTabs;
>>>>> //ytabs.postln;
>>>>> },{
>>>>> rect = Rect(xtabs * tabSize,
>>>>> ytabs * tabSize,
>>>>> 7 * tabSize,
>>>>> 0.8 * tabSize
>>>>> );
>>>>> //rect.postln;
>>>>> Pen.fillColor = Color.red;
>>>>> Pen.fillRect(rect);
>>>>> Pen.strokeRect(rect);
>>>>> (" " ++ node.key.asString +
>>>>> node.value.asString).drawInRect(rect, Font("Helvetica", 11),
>>>>> Color.black);
>>>>> ytabs = ytabs + 1;
>>>>> });
>>>>> });
>>>>> xtabs = xtabs - 1;
>>>>> };
>>>>> drawFunc.value(levels);
>>>>> };
>>>>> }.defer
>>>>> }).add.removeWhenDone;
>>>>> this.sendMsg("/g_queryTree", 0, 0);
>>>>> SystemClock.sched(3, {
>>>>> done.not.if({
>>>>> resp.remove;
>>>>> "Server failed to respond to Group:queryTree!".warn;
>>>>> });
>>>>> });
>>>>>
>>>>> }
>>>>>
>>>>> }
>>>>>
>>>>>
>>>>>
>>>>>
>>>
>>> --
>>> Miguel Negrão // ZLB
>>> http://www.friendlyvirus.org/artists/zlb/
>>>
>>> _______________________________________________
>>> sc-users mailing list
>>>
>>> info (subscription, etc.):
>>> http://www.beast.bham.ac.uk/research/sc_mailing_lists.shtml
>>> archive: https://listarc.bham.ac.uk/marchives/sc-users/
>>> search: https://listarc.bham.ac.uk/lists/sc-users/search/
>>>
>>>
>>
>
>
> --
> Miguel Negrão // ZLB
> http://www.friendlyvirus.org/artists/zlb/
>
>
> _______________________________________________
> sc-users mailing list
>
> info (subscription, etc.):
> http://www.beast.bham.ac.uk/research/sc_mailing_lists.shtml
> archive: https://listarc.bham.ac.uk/marchives/sc-users/
> search: https://listarc.bham.ac.uk/lists/sc-users/search/
>
>
--
View this message in context: http://n2.nabble.com/ToolsMenu-and-Server-gui-niceties-tp2914037p3081957.html
Sent from the SuperCollider Users New (Use this!!!!) mailing list archive at Nabble.com.
_______________________________________________
sc-users mailing list
info (subscription, etc.): http://www.beast.bham.ac.uk/research/sc_mailing_lists.shtml
archive: https://listarc.bham.ac.uk/marchives/sc-users/
search: https://listarc.bham.ac.uk/lists/sc-users/search/