@@ -1,5 +1,6 @@ | |||
/Rack | |||
/Rack.exe | |||
/Rack.res | |||
/libRack.a | |||
/autosave.json | |||
/settings.json | |||
@@ -22,7 +22,7 @@ ifeq ($(ARCH), lin) | |||
-lpthread -lGL -ldl \ | |||
$(shell pkg-config --libs gtk+-2.0) \ | |||
-Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl | |||
TARGET = Rack | |||
TARGET := Rack | |||
endif | |||
ifeq ($(ARCH), mac) | |||
@@ -31,8 +31,8 @@ ifeq ($(ARCH), mac) | |||
LDFLAGS += -stdlib=libc++ -lpthread -ldl \ | |||
-framework Cocoa -framework OpenGL -framework IOKit -framework CoreVideo \ | |||
-Ldep/lib -lGLEW -lglfw -ljansson -lspeexdsp -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl | |||
TARGET = Rack | |||
BUNDLE = dist/$(TARGET).app | |||
TARGET := Rack | |||
BUNDLE := dist/$(TARGET).app | |||
endif | |||
ifeq ($(ARCH), win) | |||
@@ -42,8 +42,8 @@ ifeq ($(ARCH), win) | |||
-lgdi32 -lopengl32 -lcomdlg32 -lole32 \ | |||
-Ldep/lib -lglew32 -lglfw3dll -lcurl -lzip -lrtaudio -lrtmidi -lcrypto -lssl \ | |||
-Wl,-Bstatic -ljansson -lspeexdsp | |||
TARGET = Rack.exe | |||
OBJECTS = Rack.res | |||
TARGET := Rack.exe | |||
OBJECTS += Rack.res | |||
endif | |||
@@ -132,8 +132,7 @@ ifeq ($(ARCH), mac) | |||
otool -L $(BUNDLE)/Contents/MacOS/$(TARGET) | |||
mkdir -p $(BUNDLE)/Contents/Resources/plugins | |||
cp -R plugins/Fundamental/dist/Fundamental $(BUNDLE)/Contents/Resources/plugins | |||
cp plugins/Fundamental/dist/Fundamental-*.zip $(BUNDLE)/Contents/Resources/Fundamental.zip | |||
# Make DMG image | |||
cd dist && ln -s /Applications Applications | |||
cd dist && hdiutil create -srcfolder . -volname Rack -ov -format UDZO Rack-$(VERSION)-$(ARCH).dmg | |||
@@ -157,8 +156,7 @@ ifeq ($(ARCH), win) | |||
cp dep/bin/librtaudio.dll dist/Rack/ | |||
cp dep/bin/libcrypto-1_1-x64.dll dist/Rack/ | |||
cp dep/bin/libssl-1_1-x64.dll dist/Rack/ | |||
mkdir -p dist/Rack/plugins | |||
cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ | |||
cp plugins/Fundamental/dist/Fundamental-*.zip dist/Rack/Fundamental.zip | |||
# Make ZIP | |||
cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | |||
# Make NSIS installer | |||
@@ -180,17 +178,16 @@ ifeq ($(ARCH), lin) | |||
cp dep/lib/librtmidi.so.4 dist/Rack/ | |||
cp dep/lib/libssl.so.1.1 dist/Rack/ | |||
cp dep/lib/libcrypto.so.1.1 dist/Rack/ | |||
mkdir -p dist/Rack/plugins | |||
cp -R plugins/Fundamental/dist/Fundamental dist/Rack/plugins/ | |||
cp plugins/Fundamental/dist/Fundamental-*.zip dist/Rack/Fundamental.zip | |||
# Make ZIP | |||
cd dist && zip -5 -r Rack-$(VERSION)-$(ARCH).zip Rack | |||
endif | |||
# Rack SDK distribution | |||
mkdir -p dist/Rack-SDK | |||
cp -R LICENSE* res dist/Rack-SDK/ | |||
cp -R include dist/Rack-SDK/ | |||
cp LICENSE* dist/Rack-SDK/ | |||
cp *.mk dist/Rack-SDK/ | |||
cp -R include dist/Rack-SDK/ | |||
mkdir -p dist/Rack-SDK/dep/ | |||
cp -R dep/include dist/Rack-SDK/dep/ | |||
ifeq ($(ARCH), win) | |||
@@ -200,7 +197,7 @@ endif | |||
# Obviously this will only work if you have the private keys to my server | |||
UPLOAD_URL = vortico@vcvrack.com:files/ | |||
UPLOAD_URL := vortico@vcvrack.com:files/ | |||
upload: dist distplugins | |||
ifeq ($(ARCH), mac) | |||
rsync dist/*.dmg $(UPLOAD_URL) -zP | |||
@@ -38,7 +38,7 @@ pacman -S git make tar unzip mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake autocon | |||
On Arch Linux: | |||
``` | |||
pacman -S git gcc make cmake tar unzip curl | |||
pacman -S git gcc make cmake tar unzip zip curl | |||
``` | |||
Other distro build instructions coming soon. | |||
@@ -51,7 +51,7 @@ Clone this repository with `git clone https://github.com/VCVRack/Rack.git` and ` | |||
Make sure there are no spaces in your path, as this breaks many build systems. | |||
The `master` branch contains the latest public code and breaks its plugin [API](https://en.wikipedia.org/wiki/Application_programming_interface) and [ABI](https://en.wikipedia.org/wiki/Application_binary_interface) frequently. | |||
If you wish to build a previous version of Rack which is API/ABI-compatible with an official Rack release, check out the desired branch with `git checkout v0.5` for example. | |||
If you wish to build a version of Rack which is API/ABI-compatible with an official Rack release, check out the desired branch with `git checkout v0.5` for example. | |||
Clone submodules. | |||
@@ -28,5 +28,10 @@ $(DEPS): export CXXFLAGS = $(DEP_CXXFLAGS) | |||
$(DEPS): export LDFLAGS = $(DEP_LDFLAGS) | |||
dep: $(DEPS) | |||
@echo | |||
@echo "#######################################" | |||
@echo "# All dependencies built successfully #" | |||
@echo "#######################################" | |||
@echo | |||
.PHONY: dep |
@@ -14,7 +14,7 @@ namespace rack { | |||
//////////////////// | |||
/** Positions children in a row/column based on their widths/heights */ | |||
struct SequentialLayout : virtual Widget { | |||
struct SequentialLayout : VirtualWidget { | |||
enum Orientation { | |||
HORIZONTAL_ORIENTATION, | |||
VERTICAL_ORIENTATION, | |||
@@ -8,6 +8,7 @@ | |||
#include <assert.h> | |||
#include <string> | |||
#include <vector> | |||
#include <condition_variable> | |||
#include <mutex> | |||
@@ -15,23 +16,28 @@ | |||
// Handy macros | |||
//////////////////// | |||
/** Surrounds raw text with quotes | |||
/** Concatenates two literals or two macros | |||
Example: | |||
printf("Hello " STRINGIFY(world)) | |||
will expand to | |||
printf("Hello " "world") | |||
and of course the C++ lexer/parser will then concatenate the string literals | |||
#define COUNT 42 | |||
CONCAT(myVariable, COUNT) | |||
expands to | |||
myVariable42 | |||
*/ | |||
#define STRINGIFY(x) #x | |||
/** Converts a macro to a string literal | |||
#define CONCAT_LITERAL(x, y) x ## y | |||
#define CONCAT(x, y) CONCAT_LITERAL(x, y) | |||
/** Surrounds raw text with quotes | |||
Example: | |||
#define NAME "world" | |||
printf("Hello " TOSTRING(NAME)) | |||
will expand to | |||
expands to | |||
printf("Hello " "world") | |||
and of course the C++ lexer/parser then concatenates the string literals. | |||
*/ | |||
#define TOSTRING(x) STRINGIFY(x) | |||
#define TOSTRING_LITERAL(x) #x | |||
#define TOSTRING(x) TOSTRING_LITERAL(x) | |||
/** Produces the length of a static array in number of elements */ | |||
#define LENGTHOF(arr) (sizeof(arr) / sizeof((arr)[0])) | |||
/** Reserve space for `count` enums starting with `name`. | |||
@@ -95,10 +101,7 @@ DeferWrapper<F> deferWrapper(F f) { | |||
return DeferWrapper<F>(f); | |||
} | |||
#define DEFER_1(x, y) x##y | |||
#define DEFER_2(x, y) DEFER_1(x, y) | |||
#define DEFER_3(x) DEFER_2(x, __COUNTER__) | |||
#define defer(code) auto DEFER_3(_defer_) = deferWrapper([&]() code) | |||
#define defer(code) auto CONCAT(x, __COUNTER__) = deferWrapper([&]() code) | |||
//////////////////// | |||
// Random number generator | |||
@@ -123,34 +126,42 @@ inline float DEPRECATED randomf() {return randomUniform();} | |||
/** Converts a printf format string and optional arguments into a std::string */ | |||
std::string stringf(const char *format, ...); | |||
std::string lowercase(std::string s); | |||
std::string uppercase(std::string s); | |||
std::string stringLowercase(std::string s); | |||
std::string stringUppercase(std::string s); | |||
/** Truncates and adds "..." to a string, not exceeding `len` characters */ | |||
std::string ellipsize(std::string s, size_t len); | |||
bool startsWith(std::string str, std::string prefix); | |||
std::string stringEllipsize(std::string s, size_t len); | |||
bool stringStartsWith(std::string str, std::string prefix); | |||
bool stringEndsWith(std::string str, std::string suffix); | |||
std::string extractDirectory(std::string path); | |||
std::string extractFilename(std::string path); | |||
std::string extractExtension(std::string path); | |||
/** Extracts portions of a path */ | |||
std::string stringDirectory(std::string path); | |||
std::string stringFilename(std::string path); | |||
std::string stringExtension(std::string path); | |||
//////////////////// | |||
// Operating-system specific utilities | |||
// system.cpp | |||
//////////////////// | |||
std::vector<std::string> systemListEntries(std::string path); | |||
bool systemIsFile(std::string path); | |||
bool systemIsDirectory(std::string path); | |||
void systemCopy(std::string srcPath, std::string destPath); | |||
/** Opens a URL, also happens to work with PDFs and folders. | |||
Shell injection is possible, so make sure the URL is trusted or hard coded. | |||
May block, so open in a new thread. | |||
*/ | |||
void openBrowser(std::string url); | |||
void systemOpenBrowser(std::string url); | |||
//////////////////// | |||
// Debug logger | |||
// logger.cpp | |||
//////////////////// | |||
extern FILE *gLogFile; | |||
void loggerInit(); | |||
void loggerDestroy(); | |||
void debug(const char *format, ...); | |||
void info(const char *format, ...); | |||
void warn(const char *format, ...); | |||
@@ -15,17 +15,17 @@ include $(RACK_DIR)/arch.mk | |||
ifeq ($(ARCH), lin) | |||
LDFLAGS += -shared | |||
TARGET = plugin.so | |||
TARGET := plugin.so | |||
endif | |||
ifeq ($(ARCH), mac) | |||
LDFLAGS += -shared -undefined dynamic_lookup | |||
TARGET = plugin.dylib | |||
TARGET := plugin.dylib | |||
endif | |||
ifeq ($(ARCH), win) | |||
LDFLAGS += -shared -L$(RACK_DIR) -lRack | |||
TARGET = plugin.dll | |||
TARGET := plugin.dll | |||
endif | |||
@@ -1,5 +1,6 @@ | |||
#include "Core.hpp" | |||
#include "midi.hpp" | |||
#include "dsp/digital.hpp" | |||
#include <algorithm> | |||
@@ -26,6 +27,9 @@ struct QuadMIDIToCVInterface : Module { | |||
enum PolyMode { | |||
ROTATE_MODE, | |||
// Added REUSE option that reuses a channel when receiving the same note. | |||
// Good when using sustain pedal so it doesn't "stack" unisons ... not sure this is the best name but it is descriptive... | |||
REUSE_MODE, | |||
RESET_MODE, | |||
REASSIGN_MODE, | |||
UNISON_MODE, | |||
@@ -39,13 +43,17 @@ struct QuadMIDIToCVInterface : Module { | |||
}; | |||
NoteData noteData[128]; | |||
std::vector<uint8_t> heldNotes; | |||
// cachedNotes : UNISON_MODE and REASSIGN_MODE cache all played notes. The other polyModes cache stolen notes (after the 4th one). | |||
std::vector<uint8_t> cachedNotes; | |||
uint8_t notes[4]; | |||
bool gates[4]; | |||
// gates set to TRUE by pedal and current gate. FALSE by pedal. | |||
bool pedalgates[4]; | |||
bool pedal; | |||
int rotateIndex; | |||
int stealIndex; | |||
QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), heldNotes(128) { | |||
QuadMIDIToCVInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS), cachedNotes(128) { | |||
onReset(); | |||
} | |||
@@ -70,71 +78,107 @@ struct QuadMIDIToCVInterface : Module { | |||
for (int i = 0; i < 4; i++) { | |||
notes[i] = 60; | |||
gates[i] = false; | |||
pedalgates[i] = false; | |||
} | |||
pedal = false; | |||
rotateIndex = 0; | |||
rotateIndex = -1; | |||
cachedNotes.clear(); | |||
} | |||
void pressNote(uint8_t note) { | |||
// Remove existing similar note | |||
auto it = std::find(heldNotes.begin(), heldNotes.end(), note); | |||
if (it != heldNotes.end()) | |||
heldNotes.erase(it); | |||
// Push note | |||
heldNotes.push_back(note); | |||
int getPolyIndex(int nowIndex) { | |||
for (int i = 0; i < 4; i++) { | |||
nowIndex++; | |||
if (nowIndex > 3) | |||
nowIndex = 0; | |||
if (!(gates[nowIndex] || pedalgates[nowIndex])) { | |||
stealIndex = nowIndex; | |||
return nowIndex; | |||
} | |||
} | |||
// All taken = steal (stealIndex always rotates) | |||
stealIndex++; | |||
if (stealIndex > 3) | |||
stealIndex = 0; | |||
if ((polyMode < REASSIGN_MODE) && (gates[stealIndex])) | |||
cachedNotes.push_back(notes[stealIndex]); | |||
return stealIndex; | |||
} | |||
void pressNote(uint8_t note) { | |||
// Set notes and gates | |||
switch (polyMode) { | |||
case ROTATE_MODE: { | |||
rotateIndex = getPolyIndex(rotateIndex); | |||
} break; | |||
case RESET_MODE: { | |||
case REUSE_MODE: { | |||
bool reuse = false; | |||
for (int i = 0; i < 4; i++) { | |||
if (notes[i] == note) { | |||
rotateIndex = i; | |||
reuse = true; | |||
break; | |||
} | |||
} | |||
if (!reuse) | |||
rotateIndex = getPolyIndex(rotateIndex); | |||
} break; | |||
case RESET_MODE: { | |||
rotateIndex = getPolyIndex(-1); | |||
} break; | |||
case REASSIGN_MODE: { | |||
cachedNotes.push_back(note); | |||
rotateIndex = getPolyIndex(-1); | |||
} break; | |||
case UNISON_MODE: { | |||
cachedNotes.push_back(note); | |||
for (int i = 0; i < 4; i++) { | |||
notes[i] = note; | |||
gates[i] = true; | |||
pedalgates[i] = pedal; | |||
// reTrigger[i].trigger(1e-3); | |||
} | |||
return; | |||
} break; | |||
default: break; | |||
} | |||
// Set notes and gates | |||
// if (gates[rotateIndex] || pedalgates[rotateIndex]) | |||
// reTrigger[rotateIndex].trigger(1e-3); | |||
notes[rotateIndex] = note; | |||
gates[rotateIndex] = true; | |||
pedalgates[rotateIndex] = pedal; | |||
} | |||
void releaseNote(uint8_t note) { | |||
// Remove the note | |||
auto it = std::find(heldNotes.begin(), heldNotes.end(), note); | |||
if (it != heldNotes.end()) | |||
heldNotes.erase(it); | |||
// Hold note if pedal is pressed | |||
if (pedal) | |||
return; | |||
// Set last note | |||
switch (polyMode) { | |||
case ROTATE_MODE: { | |||
} break; | |||
case RESET_MODE: { | |||
} break; | |||
auto it = std::find(cachedNotes.begin(), cachedNotes.end(), note); | |||
if (it != cachedNotes.end()) | |||
cachedNotes.erase(it); | |||
switch (polyMode) { | |||
case REASSIGN_MODE: { | |||
for (int i = 0; i < 4; i++) { | |||
if (i < (int) cachedNotes.size()) { | |||
if (!pedalgates[i]) | |||
notes[i] = cachedNotes[i]; | |||
pedalgates[i] = pedal; | |||
} | |||
else { | |||
gates[i] = false; | |||
} | |||
} | |||
} break; | |||
case UNISON_MODE: { | |||
if (!heldNotes.empty()) { | |||
auto it2 = heldNotes.end(); | |||
it2--; | |||
if (!cachedNotes.empty()) { | |||
uint8_t backnote = cachedNotes.back(); | |||
for (int i = 0; i < 4; i++) { | |||
notes[i] = *it2; | |||
notes[i] = backnote; | |||
gates[i] = true; | |||
} | |||
} | |||
@@ -145,18 +189,57 @@ struct QuadMIDIToCVInterface : Module { | |||
} | |||
} break; | |||
default: break; | |||
// default ROTATE_MODE REUSE_MODE RESET_MODE | |||
default: { | |||
for (int i = 0; i < 4; i++) { | |||
if (notes[i] == note) { | |||
if (pedalgates[i]) { | |||
gates[i] = false; | |||
} | |||
else if (!cachedNotes.empty()) { | |||
notes[i] = cachedNotes.back(); | |||
cachedNotes.pop_back(); | |||
} | |||
else { | |||
gates[i] = false; | |||
} | |||
} | |||
} | |||
} break; | |||
} | |||
} | |||
void pressPedal() { | |||
pedal = true; | |||
for (int i = 0; i < 4; i++) { | |||
pedalgates[i] = gates[i]; | |||
} | |||
} | |||
void releasePedal() { | |||
pedal = false; | |||
releaseNote(255); | |||
// When pedal is off, recover notes for pressed keys (if any) after they were already being "cycled" out by pedal-sustained notes. | |||
for (int i = 0; i < 4; i++) { | |||
pedalgates[i] = false; | |||
if (!cachedNotes.empty()) { | |||
if (polyMode < REASSIGN_MODE) { | |||
notes[i] = cachedNotes.back(); | |||
cachedNotes.pop_back(); | |||
gates[i] = true; | |||
} | |||
} | |||
} | |||
if (polyMode == REASSIGN_MODE) { | |||
for (int i = 0; i < 4; i++) { | |||
if (i < (int) cachedNotes.size()) { | |||
notes[i] = cachedNotes[i]; | |||
gates[i] = true; | |||
} | |||
else { | |||
gates[i] = false; | |||
} | |||
} | |||
} | |||
} | |||
void step() override { | |||
@@ -167,14 +250,19 @@ struct QuadMIDIToCVInterface : Module { | |||
for (int i = 0; i < 4; i++) { | |||
uint8_t lastNote = notes[i]; | |||
uint8_t lastGate = (gates[i] || pedalgates[i]); | |||
outputs[CV_OUTPUT + i].value = (lastNote - 60) / 12.f; | |||
outputs[GATE_OUTPUT + i].value = gates[i] ? 10.f : 0.f; | |||
outputs[GATE_OUTPUT + i].value = lastGate ? 10.f : 0.f; | |||
outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].velocity, 0, 127, 0.f, 10.f); | |||
outputs[VELOCITY_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); | |||
outputs[AFTERTOUCH_OUTPUT + i].value = rescale(noteData[lastNote].aftertouch, 0, 127, 0.f, 10.f); | |||
} | |||
} | |||
void processMessage(MidiMessage msg) { | |||
// filter MIDI channel | |||
if ((midiInput.channel > -1) && (midiInput.channel != msg.channel())) | |||
return; | |||
switch (msg.status()) { | |||
// note off | |||
case 0x8: { | |||
@@ -262,7 +350,13 @@ struct QuadMIDIToCVInterfaceWidget : ModuleWidget { | |||
menu->addChild(MenuEntry::create()); | |||
menu->addChild(MenuLabel::create("Polyphony mode")); | |||
std::vector<std::string> polyModeNames = {"Rotate", "Reset", "Reassign", "Unison"}; | |||
std::vector<std::string> polyModeNames = { | |||
"Rotate", | |||
"Reuse", | |||
"Reset", | |||
"Reassign", | |||
"Unison" | |||
}; | |||
for (int i = 0; i < QuadMIDIToCVInterface::NUM_MODES; i++) { | |||
PolyphonyItem *item = MenuItem::create<PolyphonyItem>(polyModeNames[i], CHECKMARK(module->polyMode == i)); | |||
item->module = module; | |||
@@ -18,8 +18,8 @@ static ModelTag sTagFilter = NO_TAG; | |||
bool isMatch(std::string s, std::string search) { | |||
s = lowercase(s); | |||
search = lowercase(search); | |||
s = stringLowercase(s); | |||
search = stringLowercase(search); | |||
return (s.find(search) != std::string::npos); | |||
} | |||
@@ -114,6 +114,7 @@ void ModuleWidget::fromJson(json_t *rootJ) { | |||
json_t *paramJ; | |||
json_array_foreach(paramsJ, i, paramJ) { | |||
if (legacy && legacy <= 1) { | |||
// Legacy 1 mode | |||
// The index in the array we're iterating is the index of the ParamWidget in the params vector. | |||
if (i < params.size()) { | |||
// Create upgraded version of param JSON object | |||
@@ -53,6 +53,7 @@ struct SyncButton : Button { | |||
PluginManagerWidget::PluginManagerWidget() { | |||
box.size.y = BND_WIDGET_HEIGHT; | |||
float margin = 5; | |||
@@ -62,7 +63,9 @@ PluginManagerWidget::PluginManagerWidget() { | |||
struct RegisterButton : Button { | |||
void onAction(EventAction &e) override { | |||
std::thread t(openBrowser, "https://vcvrack.com/"); | |||
std::thread t([&]() { | |||
systemOpenBrowser("https://vcvrack.com/"); | |||
}); | |||
t.detach(); | |||
} | |||
}; | |||
@@ -126,7 +129,9 @@ PluginManagerWidget::PluginManagerWidget() { | |||
struct ManageButton : Button { | |||
void onAction(EventAction &e) override { | |||
std::thread t(openBrowser, "https://vcvrack.com/"); | |||
std::thread t([&]() { | |||
systemOpenBrowser("https://vcvrack.com/"); | |||
}); | |||
t.detach(); | |||
} | |||
}; | |||
@@ -1,7 +1,6 @@ | |||
#include "app.hpp" | |||
#include "window.hpp" | |||
#include "util/request.hpp" | |||
#include "osdialog.h" | |||
#include <string.h> | |||
#include <thread> | |||
@@ -9,27 +8,6 @@ | |||
namespace rack { | |||
static std::string newVersion = ""; | |||
#if defined(RELEASE) | |||
static void checkVersion() { | |||
json_t *resJ = requestJson(METHOD_GET, gApiHost + "/version", NULL); | |||
if (resJ) { | |||
json_t *versionJ = json_object_get(resJ, "version"); | |||
if (versionJ) { | |||
const char *version = json_string_value(versionJ); | |||
if (version && strlen(version) > 0 && version != gApplicationVersion) { | |||
newVersion = version; | |||
} | |||
} | |||
json_decref(resJ); | |||
} | |||
} | |||
#endif | |||
RackScene::RackScene() { | |||
scrollWidget = new RackScrollWidget(); | |||
{ | |||
@@ -46,12 +24,6 @@ RackScene::RackScene() { | |||
gToolbar = new Toolbar(); | |||
addChild(gToolbar); | |||
scrollWidget->box.pos.y = gToolbar->box.size.y; | |||
// Check for new version | |||
#if defined(RELEASE) | |||
std::thread versionThread(checkVersion); | |||
versionThread.detach(); | |||
#endif | |||
} | |||
void RackScene::step() { | |||
@@ -68,17 +40,6 @@ void RackScene::step() { | |||
Scene::step(); | |||
zoomWidget->box.size = gRackWidget->box.size.mult(zoomWidget->zoom); | |||
// Version popup message | |||
if (!newVersion.empty()) { | |||
std::string versionMessage = stringf("Rack %s is available.\n\nYou have Rack %s.\n\nWould you like to download the new version on the website?", newVersion.c_str(), gApplicationVersion.c_str()); | |||
if (osdialog_message(OSDIALOG_INFO, OSDIALOG_YES_NO, versionMessage.c_str())) { | |||
std::thread t(openBrowser, "https://vcvrack.com/"); | |||
t.detach(); | |||
windowClose(); | |||
} | |||
newVersion = ""; | |||
} | |||
} | |||
void RackScene::draw(NVGcontext *vg) { | |||
@@ -129,7 +90,7 @@ void RackScene::onHoverKey(EventHoverKey &e) { | |||
void RackScene::onPathDrop(EventPathDrop &e) { | |||
if (e.paths.size() >= 1) { | |||
const std::string& firstPath = e.paths.front(); | |||
if (extractExtension(firstPath) == "vcv") { | |||
if (stringExtension(firstPath) == "vcv") { | |||
gRackWidget->loadPatch(firstPath); | |||
e.consumed = true; | |||
} | |||
@@ -69,7 +69,7 @@ void RackWidget::reset() { | |||
} | |||
void RackWidget::openDialog() { | |||
std::string dir = lastPath.empty() ? assetLocal("") : extractDirectory(lastPath); | |||
std::string dir = lastPath.empty() ? assetLocal("") : stringDirectory(lastPath); | |||
char *path = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); | |||
if (path) { | |||
loadPatch(path); | |||
@@ -88,13 +88,13 @@ void RackWidget::saveDialog() { | |||
} | |||
void RackWidget::saveAsDialog() { | |||
std::string dir = lastPath.empty() ? assetLocal("") : extractDirectory(lastPath); | |||
std::string dir = lastPath.empty() ? assetLocal("") : stringDirectory(lastPath); | |||
char *path = osdialog_file(OSDIALOG_SAVE, dir.c_str(), "Untitled.vcv", NULL); | |||
if (path) { | |||
std::string pathStr = path; | |||
free(path); | |||
std::string extension = extractExtension(pathStr); | |||
std::string extension = stringExtension(pathStr); | |||
if (extension.empty()) { | |||
pathStr += ".vcv"; | |||
} | |||
@@ -225,15 +225,14 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
json_t *versionJ = json_object_get(rootJ, "version"); | |||
if (versionJ) { | |||
version = json_string_value(versionJ); | |||
if (!version.empty() && gApplicationVersion != version) | |||
message += stringf("This patch was created with Rack %s. Saving it will convert it to a Rack %s patch.\n\n", version.c_str(), gApplicationVersion.c_str()); | |||
} | |||
// Detect old patches with ModuleWidget::params/inputs/outputs indices. | |||
// (We now use Module::params/inputs/outputs indices.) | |||
int legacy = 0; | |||
if (startsWith(version, "0.3.") || startsWith(version, "0.4.") || startsWith(version, "0.5.") || version == "" || version == "dev") { | |||
if (stringStartsWith(version, "0.3.") || stringStartsWith(version, "0.4.") || stringStartsWith(version, "0.5.") || version == "" || version == "dev") { | |||
legacy = 1; | |||
message += "This patch was created with Rack 0.5 or earlier. Saving it will convert it to a Rack 0.6+ patch.\n\n"; | |||
} | |||
if (legacy) { | |||
info("Loading patch using legacy mode %d", legacy); | |||
@@ -246,9 +245,10 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
size_t moduleId; | |||
json_t *moduleJ; | |||
json_array_foreach(modulesJ, moduleId, moduleJ) { | |||
// Set legacy property | |||
if (legacy) | |||
json_object_set_new(moduleJ, "legacy", json_integer(legacy)); | |||
// Add "legacy" property if in legacy mode | |||
if (legacy) { | |||
json_object_set(moduleJ, "legacy", json_integer(legacy)); | |||
} | |||
json_t *pluginSlugJ = json_object_get(moduleJ, "plugin"); | |||
if (!pluginSlugJ) continue; | |||
@@ -259,7 +259,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
Model *model = pluginGetModel(pluginSlug, modelSlug); | |||
if (!model) { | |||
message += stringf("Could not find module \"%s\" in plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | |||
message += stringf("Could not find module \"%s\" of plugin \"%s\"\n", modelSlug.c_str(), pluginSlug.c_str()); | |||
continue; | |||
} | |||
@@ -292,6 +292,8 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
Port *outputPort = NULL; | |||
Port *inputPort = NULL; | |||
if (legacy && legacy <= 1) { | |||
// Legacy 1 mode | |||
// The index of the "ports" array is the index of the Port in the `outputs` and `inputs` vector. | |||
outputPort = outputModuleWidget->outputs[outputId]; | |||
inputPort = inputModuleWidget->inputs[inputId]; | |||
} | |||
@@ -324,7 +326,7 @@ void RackWidget::fromJson(json_t *rootJ) { | |||
// Display a message if we have something to say | |||
if (!message.empty()) { | |||
osdialog_message(OSDIALOG_INFO, OSDIALOG_OK, message.c_str()); | |||
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | |||
} | |||
} | |||
@@ -25,7 +25,7 @@ namespace rack { | |||
std::string assetGlobal(std::string filename) { | |||
std::string dir; | |||
#if defined(RELEASE) | |||
#if RELEASE | |||
#if ARCH_MAC | |||
CFBundleRef bundle = CFBundleGetMainBundle(); | |||
assert(bundle); | |||
@@ -53,7 +53,7 @@ std::string assetGlobal(std::string filename) { | |||
std::string assetLocal(std::string filename) { | |||
std::string dir; | |||
#if defined(RELEASE) | |||
#if RELEASE | |||
#if ARCH_MAC | |||
// Get home directory | |||
struct passwd *pw = getpwuid(getuid()); | |||
@@ -6,19 +6,17 @@ | |||
#include "settings.hpp" | |||
#include "asset.hpp" | |||
#include "bridge.hpp" | |||
#include <unistd.h> | |||
#include "osdialog.h" | |||
#include <unistd.h> | |||
using namespace rack; | |||
int main(int argc, char* argv[]) { | |||
randomInit(); | |||
#ifdef RELEASE | |||
std::string logFilename = assetLocal("log.txt"); | |||
gLogFile = fopen(logFilename.c_str(), "w"); | |||
#endif | |||
loggerInit(); | |||
info("Rack %s", gApplicationVersion.c_str()); | |||
@@ -64,10 +62,7 @@ int main(int argc, char* argv[]) { | |||
bridgeDestroy(); | |||
engineDestroy(); | |||
pluginDestroy(); | |||
#ifdef RELEASE | |||
fclose(gLogFile); | |||
#endif | |||
loggerDestroy(); | |||
return 0; | |||
} |
@@ -1,5 +1,10 @@ | |||
#include <stdio.h> | |||
#include "plugin.hpp" | |||
#include "app.hpp" | |||
#include "asset.hpp" | |||
#include "util/request.hpp" | |||
#include "osdialog.h" | |||
#include <stdio.h> | |||
#include <assert.h> | |||
#include <string.h> | |||
#include <unistd.h> | |||
@@ -13,7 +18,7 @@ | |||
#include <zip.h> | |||
#include <jansson.h> | |||
#if defined(ARCH_WIN) | |||
#if ARCH_WIN | |||
#include <windows.h> | |||
#include <direct.h> | |||
#define mkdir(_dir, _perms) _mkdir(_dir) | |||
@@ -22,14 +27,10 @@ | |||
#endif | |||
#include <dirent.h> | |||
#include "plugin.hpp" | |||
#include "app.hpp" | |||
#include "asset.hpp" | |||
#include "util/request.hpp" | |||
namespace rack { | |||
std::list<Plugin*> gPlugins; | |||
std::string gToken; | |||
@@ -52,8 +53,11 @@ void Plugin::addModel(Model *model) { | |||
models.push_back(model); | |||
} | |||
//////////////////// | |||
// private API | |||
//////////////////// | |||
static int loadPlugin(std::string path) { | |||
static bool loadPlugin(std::string path) { | |||
std::string libraryFilename; | |||
#if ARCH_LIN | |||
libraryFilename = path + "/" + "plugin.so"; | |||
@@ -63,6 +67,12 @@ static int loadPlugin(std::string path) { | |||
libraryFilename = path + "/" + "plugin.dylib"; | |||
#endif | |||
// Check file existence | |||
if (!systemIsFile(libraryFilename)) { | |||
warn("Plugin file %s does not exist", libraryFilename.c_str()); | |||
return false; | |||
} | |||
// Load dynamic/shared library | |||
#if ARCH_WIN | |||
SetErrorMode(SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS); | |||
@@ -70,14 +80,14 @@ static int loadPlugin(std::string path) { | |||
SetErrorMode(0); | |||
if (!handle) { | |||
int error = GetLastError(); | |||
warn("Failed to load library %s: %d", libraryFilename.c_str(), error); | |||
return -1; | |||
warn("Failed to load library %s: code %d", libraryFilename.c_str(), error); | |||
return false; | |||
} | |||
#elif ARCH_LIN || ARCH_MAC | |||
#else | |||
void *handle = dlopen(libraryFilename.c_str(), RTLD_NOW); | |||
if (!handle) { | |||
warn("Failed to load library %s: %s", libraryFilename.c_str(), dlerror()); | |||
return -1; | |||
return false; | |||
} | |||
#endif | |||
@@ -86,12 +96,12 @@ static int loadPlugin(std::string path) { | |||
InitCallback initCallback; | |||
#if ARCH_WIN | |||
initCallback = (InitCallback) GetProcAddress(handle, "init"); | |||
#elif ARCH_LIN || ARCH_MAC | |||
#else | |||
initCallback = (InitCallback) dlsym(handle, "init"); | |||
#endif | |||
if (!initCallback) { | |||
warn("Failed to read init() symbol in %s", libraryFilename.c_str()); | |||
return -2; | |||
return false; | |||
} | |||
// Construct and initialize Plugin instance | |||
@@ -101,94 +111,19 @@ static int loadPlugin(std::string path) { | |||
initCallback(plugin); | |||
// Reject plugin if slug already exists | |||
for (Plugin *p : gPlugins) { | |||
if (plugin->slug == p->slug) { | |||
warn("Plugin \"%s\" is already loaded, not attempting to load it again", p->slug.c_str()); | |||
// TODO | |||
// Fix memory leak with `plugin` here | |||
return -1; | |||
} | |||
Plugin *oldPlugin = pluginGetPlugin(plugin->slug); | |||
if (oldPlugin) { | |||
warn("Plugin \"%s\" is already loaded, not attempting to load it again", plugin->slug.c_str()); | |||
// TODO | |||
// Fix memory leak with `plugin` here | |||
return false; | |||
} | |||
// Add plugin to list | |||
gPlugins.push_back(plugin); | |||
info("Loaded plugin %s", libraryFilename.c_str()); | |||
return 0; | |||
} | |||
static void loadPlugins(std::string path) { | |||
DIR *dir = opendir(path.c_str()); | |||
if (dir) { | |||
struct dirent *d; | |||
while ((d = readdir(dir))) { | |||
if (d->d_name[0] == '.') | |||
continue; | |||
loadPlugin(path + "/" + d->d_name); | |||
} | |||
closedir(dir); | |||
} | |||
} | |||
//////////////////// | |||
// plugin helpers | |||
//////////////////// | |||
static int extractZipHandle(zip_t *za, const char *dir) { | |||
int err = 0; | |||
for (int i = 0; i < zip_get_num_entries(za, 0); i++) { | |||
zip_stat_t zs; | |||
err = zip_stat_index(za, i, 0, &zs); | |||
if (err) | |||
return err; | |||
int nameLen = strlen(zs.name); | |||
char path[MAXPATHLEN]; | |||
snprintf(path, sizeof(path), "%s/%s", dir, zs.name); | |||
if (zs.name[nameLen - 1] == '/') { | |||
err = mkdir(path, 0755); | |||
if (err && errno != EEXIST) | |||
return err; | |||
} | |||
else { | |||
zip_file_t *zf = zip_fopen_index(za, i, 0); | |||
if (!zf) | |||
return 1; | |||
FILE *outFile = fopen(path, "wb"); | |||
if (!outFile) | |||
continue; | |||
while (1) { | |||
char buffer[4096]; | |||
int len = zip_fread(zf, buffer, sizeof(buffer)); | |||
if (len <= 0) | |||
break; | |||
fwrite(buffer, 1, len, outFile); | |||
} | |||
err = zip_fclose(zf); | |||
if (err) | |||
return err; | |||
fclose(outFile); | |||
} | |||
} | |||
return 0; | |||
} | |||
static int extractZip(const char *filename, const char *dir) { | |||
int err = 0; | |||
zip_t *za = zip_open(filename, 0, &err); | |||
if (!za) | |||
return 1; | |||
if (!err) { | |||
err = extractZipHandle(za, dir); | |||
} | |||
zip_close(za); | |||
return err; | |||
return true; | |||
} | |||
static bool syncPlugin(json_t *pluginJ, bool dryRun) { | |||
@@ -223,11 +158,11 @@ static bool syncPlugin(json_t *pluginJ, bool dryRun) { | |||
json_t *downloadsJ = json_object_get(pluginJ, "downloads"); | |||
if (downloadsJ) { | |||
#if defined(ARCH_WIN) | |||
#if ARCH_WIN | |||
#define DOWNLOADS_ARCH "win" | |||
#elif defined(ARCH_MAC) | |||
#elif ARCH_MAC | |||
#define DOWNLOADS_ARCH "mac" | |||
#elif defined(ARCH_LIN) | |||
#elif ARCH_LIN | |||
#define DOWNLOADS_ARCH "lin" | |||
#endif | |||
json_t *archJ = json_object_get(downloadsJ, DOWNLOADS_ARCH); | |||
@@ -281,19 +216,151 @@ static bool syncPlugin(json_t *pluginJ, bool dryRun) { | |||
} | |||
} | |||
// Unzip file | |||
int err = extractZip(zipPath.c_str(), pluginsDir.c_str()); | |||
if (!err) { | |||
// Delete zip | |||
remove(zipPath.c_str()); | |||
// Load plugin | |||
// loadPlugin(pluginPath); | |||
} | |||
downloadName = ""; | |||
return true; | |||
} | |||
static void loadPlugins(std::string path) { | |||
std::string message; | |||
for (std::string pluginPath : systemListEntries(path)) { | |||
if (!systemIsDirectory(pluginPath)) | |||
continue; | |||
if (!loadPlugin(pluginPath)) { | |||
message += stringf("Could not load plugin %s\n", pluginPath.c_str()); | |||
} | |||
} | |||
if (!message.empty()) { | |||
message += "See log for details."; | |||
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | |||
} | |||
} | |||
static int extractZipHandle(zip_t *za, const char *dir) { | |||
int err = 0; | |||
for (int i = 0; i < zip_get_num_entries(za, 0); i++) { | |||
zip_stat_t zs; | |||
err = zip_stat_index(za, i, 0, &zs); | |||
if (err) | |||
return err; | |||
int nameLen = strlen(zs.name); | |||
char path[MAXPATHLEN]; | |||
snprintf(path, sizeof(path), "%s/%s", dir, zs.name); | |||
if (zs.name[nameLen - 1] == '/') { | |||
err = mkdir(path, 0755); | |||
if (err && errno != EEXIST) | |||
return err; | |||
} | |||
else { | |||
zip_file_t *zf = zip_fopen_index(za, i, 0); | |||
if (!zf) | |||
return 1; | |||
FILE *outFile = fopen(path, "wb"); | |||
if (!outFile) | |||
continue; | |||
while (1) { | |||
char buffer[1<<15]; | |||
int len = zip_fread(zf, buffer, sizeof(buffer)); | |||
if (len <= 0) | |||
break; | |||
fwrite(buffer, 1, len, outFile); | |||
} | |||
err = zip_fclose(zf); | |||
if (err) | |||
return err; | |||
fclose(outFile); | |||
} | |||
} | |||
return 0; | |||
} | |||
static int extractZip(const char *filename, const char *path) { | |||
int err = 0; | |||
zip_t *za = zip_open(filename, 0, &err); | |||
if (!za) | |||
return 1; | |||
defer({ | |||
zip_close(za); | |||
}); | |||
if (err) | |||
return err; | |||
err = extractZipHandle(za, path); | |||
return err; | |||
} | |||
static void extractPackages(std::string path) { | |||
std::string message; | |||
for (std::string packagePath : systemListEntries(path)) { | |||
if (stringExtension(packagePath) == "zip") { | |||
info("Extracting package %s", packagePath.c_str()); | |||
// Extract package | |||
if (extractZip(packagePath.c_str(), path.c_str())) { | |||
message += stringf("Could not extract package %s\n", packagePath.c_str()); | |||
continue; | |||
} | |||
// Remove package | |||
remove(packagePath.c_str()); | |||
} | |||
} | |||
if (!message.empty()) { | |||
osdialog_message(OSDIALOG_WARNING, OSDIALOG_OK, message.c_str()); | |||
} | |||
} | |||
//////////////////// | |||
// public API | |||
//////////////////// | |||
void pluginInit() { | |||
tagsInit(); | |||
// Load core | |||
// This function is defined in core.cpp | |||
Plugin *corePlugin = new Plugin(); | |||
init(corePlugin); | |||
gPlugins.push_back(corePlugin); | |||
// Get local plugins directory | |||
std::string localPlugins = assetLocal("plugins"); | |||
mkdir(localPlugins.c_str(), 0755); | |||
#if RELEASE | |||
// Copy Fundamental package to plugins directory if folder does not exist | |||
std::string fundamentalDest = localPlugins + "/Fundamental.zip"; | |||
if (!systemIsDirectory(localPlugins + "/Fundamental") && !systemIsFile(fundamentalDest)) { | |||
systemCopy(assetGlobal("Fundamental.zip"), fundamentalDest); | |||
} | |||
#endif | |||
// Extract packages and load plugins | |||
extractPackages(localPlugins); | |||
loadPlugins(localPlugins); | |||
} | |||
void pluginDestroy() { | |||
for (Plugin *plugin : gPlugins) { | |||
// Free library handle | |||
#if ARCH_WIN | |||
if (plugin->handle) | |||
FreeLibrary((HINSTANCE)plugin->handle); | |||
#else | |||
if (plugin->handle) | |||
dlclose(plugin->handle); | |||
#endif | |||
// For some reason this segfaults. | |||
// It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. | |||
// delete plugin; | |||
} | |||
gPlugins.clear(); | |||
} | |||
bool pluginSync(bool dryRun) { | |||
if (gToken.empty()) | |||
return false; | |||
@@ -378,51 +445,6 @@ bool pluginSync(bool dryRun) { | |||
return available; | |||
} | |||
//////////////////// | |||
// plugin API | |||
//////////////////// | |||
void pluginInit() { | |||
tagsInit(); | |||
// TODO | |||
// If `<local>/plugins/Fundamental` doesn't exist, unzip global Fundamental.zip package into `<local>/plugins` | |||
// TODO | |||
// Find all ZIP packages in `<local>/plugins` and unzip them. | |||
// Display error if failure | |||
// Load core | |||
// This function is defined in core.cpp | |||
Plugin *corePlugin = new Plugin(); | |||
init(corePlugin); | |||
gPlugins.push_back(corePlugin); | |||
// Load plugins from local directory | |||
std::string localPlugins = assetLocal("plugins"); | |||
mkdir(localPlugins.c_str(), 0755); | |||
info("Loading plugins from %s", localPlugins.c_str()); | |||
loadPlugins(localPlugins); | |||
} | |||
void pluginDestroy() { | |||
for (Plugin *plugin : gPlugins) { | |||
// Free library handle | |||
#if defined(ARCH_WIN) | |||
if (plugin->handle) | |||
FreeLibrary((HINSTANCE)plugin->handle); | |||
#elif defined(ARCH_LIN) || defined(ARCH_MAC) | |||
if (plugin->handle) | |||
dlclose(plugin->handle); | |||
#endif | |||
// For some reason this segfaults. | |||
// It might be best to let them leak anyway, because "crash on exit" issues would occur with badly-written plugins. | |||
// delete plugin; | |||
} | |||
gPlugins.clear(); | |||
} | |||
void pluginLogIn(std::string email, std::string password) { | |||
json_t *reqJ = json_object(); | |||
json_object_set(reqJ, "email", json_string(email.c_str())); | |||
@@ -498,6 +520,4 @@ Model *pluginGetModel(std::string pluginSlug, std::string modelSlug) { | |||
} | |||
} // namespace rack |
@@ -1,49 +1,66 @@ | |||
#include "util/common.hpp" | |||
#include "asset.hpp" | |||
#include <stdarg.h> | |||
namespace rack { | |||
FILE *gLogFile = stderr; | |||
static FILE *logFile = stderr; | |||
static auto startTime = std::chrono::high_resolution_clock::now(); | |||
void loggerInit() { | |||
#ifdef RELEASE | |||
std::string logFilename = assetLocal("log.txt"); | |||
logFile = fopen(logFilename.c_str(), "w"); | |||
#endif | |||
} | |||
void loggerDestroy() { | |||
#ifdef RELEASE | |||
fclose(logFile); | |||
#endif | |||
} | |||
static void printTimestamp() { | |||
} | |||
static void printLog(const char *type, const char *format, va_list args) { | |||
auto nowTime = std::chrono::high_resolution_clock::now(); | |||
int duration = std::chrono::duration_cast<std::chrono::milliseconds>(nowTime - startTime).count(); | |||
printTimestamp(); | |||
fprintf(logFile, "[%.03f %s] ", duration / 1000.0, type); | |||
vfprintf(logFile, format, args); | |||
fprintf(logFile, "\n"); | |||
fflush(logFile); | |||
} | |||
void debug(const char *format, ...) { | |||
va_list args; | |||
va_start(args, format); | |||
fprintf(gLogFile, "[debug] "); | |||
vfprintf(gLogFile, format, args); | |||
fprintf(gLogFile, "\n"); | |||
fflush(gLogFile); | |||
printLog("debug", format, args); | |||
va_end(args); | |||
} | |||
void info(const char *format, ...) { | |||
va_list args; | |||
va_start(args, format); | |||
fprintf(gLogFile, "[info] "); | |||
vfprintf(gLogFile, format, args); | |||
fprintf(gLogFile, "\n"); | |||
fflush(gLogFile); | |||
printLog("info", format, args); | |||
va_end(args); | |||
} | |||
void warn(const char *format, ...) { | |||
va_list args; | |||
va_start(args, format); | |||
fprintf(gLogFile, "[warning] "); | |||
vfprintf(gLogFile, format, args); | |||
fprintf(gLogFile, "\n"); | |||
fflush(gLogFile); | |||
printLog("warn", format, args); | |||
va_end(args); | |||
} | |||
void fatal(const char *format, ...) { | |||
va_list args; | |||
va_start(args, format); | |||
fprintf(gLogFile, "[fatal] "); | |||
vfprintf(gLogFile, format, args); | |||
fprintf(gLogFile, "\n"); | |||
fflush(gLogFile); | |||
printLog("fatal", format, args); | |||
va_end(args); | |||
} | |||
@@ -24,43 +24,47 @@ std::string stringf(const char *format, ...) { | |||
return s; | |||
} | |||
std::string lowercase(std::string s) { | |||
std::string stringLowercase(std::string s) { | |||
std::transform(s.begin(), s.end(), s.begin(), ::tolower); | |||
return s; | |||
} | |||
std::string uppercase(std::string s) { | |||
std::string stringUppercase(std::string s) { | |||
std::transform(s.begin(), s.end(), s.begin(), ::toupper); | |||
return s; | |||
} | |||
std::string ellipsize(std::string s, size_t len) { | |||
std::string stringEllipsize(std::string s, size_t len) { | |||
if (s.size() <= len) | |||
return s; | |||
else | |||
return s.substr(0, len - 3) + "..."; | |||
} | |||
bool startsWith(std::string str, std::string prefix) { | |||
bool stringStartsWith(std::string str, std::string prefix) { | |||
return str.substr(0, prefix.size()) == prefix; | |||
} | |||
std::string extractDirectory(std::string path) { | |||
bool stringEndsWith(std::string str, std::string suffix) { | |||
return str.substr(str.size() - suffix.size(), suffix.size()) == suffix; | |||
} | |||
std::string stringDirectory(std::string path) { | |||
char *pathDup = strdup(path.c_str()); | |||
std::string directory = dirname(pathDup); | |||
free(pathDup); | |||
return directory; | |||
} | |||
std::string extractFilename(std::string path) { | |||
std::string stringFilename(std::string path) { | |||
char *pathDup = strdup(path.c_str()); | |||
std::string filename = basename(pathDup); | |||
free(pathDup); | |||
return filename; | |||
} | |||
std::string extractExtension(std::string path) { | |||
const char *ext = strrchr(path.c_str(), '.'); | |||
std::string stringExtension(std::string path) { | |||
const char *ext = strrchr(stringFilename(path).c_str(), '.'); | |||
if (!ext) | |||
return ""; | |||
return ext + 1; | |||
@@ -1,18 +1,81 @@ | |||
#include "util/common.hpp" | |||
#include <dirent.h> | |||
#include <sys/stat.h> | |||
#if ARCH_WIN | |||
#include <windows.h> | |||
#include <shellapi.h> | |||
#include <windows.h> | |||
#include <shellapi.h> | |||
#endif | |||
namespace rack { | |||
void openBrowser(std::string url) { | |||
std::vector<std::string> systemListEntries(std::string path) { | |||
std::vector<std::string> filenames; | |||
DIR *dir = opendir(path.c_str()); | |||
if (dir) { | |||
struct dirent *d; | |||
while ((d = readdir(dir))) { | |||
std::string filename = d->d_name; | |||
if (filename == "." || filename == "..") | |||
continue; | |||
filenames.push_back(path + "/" + filename); | |||
} | |||
closedir(dir); | |||
} | |||
return filenames; | |||
} | |||
bool systemExists(std::string path) { | |||
struct stat statbuf; | |||
return (stat(path.c_str(), &statbuf) == 0); | |||
} | |||
bool systemIsFile(std::string path) { | |||
struct stat statbuf; | |||
if (stat(path.c_str(), &statbuf)) | |||
return false; | |||
return S_ISREG(statbuf.st_mode); | |||
} | |||
bool systemIsDirectory(std::string path) { | |||
struct stat statbuf; | |||
if (stat(path.c_str(), &statbuf)) | |||
return false; | |||
return S_ISDIR(statbuf.st_mode); | |||
} | |||
void systemCopy(std::string srcPath, std::string destPath) { | |||
// Open files | |||
FILE *source = fopen(srcPath.c_str(), "rb"); | |||
if (!source) return; | |||
defer({ | |||
fclose(source); | |||
}); | |||
FILE *dest = fopen(destPath.c_str(), "wb"); | |||
if (!dest) return; | |||
defer({ | |||
fclose(dest); | |||
}); | |||
// Copy buffer | |||
const int bufferSize = (1<<15); | |||
char buffer[bufferSize]; | |||
while (1) { | |||
size_t size = fread(buffer, 1, bufferSize, source); | |||
if (size == 0) | |||
break; | |||
size = fwrite(buffer, 1, size, dest); | |||
if (size == 0) | |||
break; | |||
} | |||
} | |||
void systemOpenBrowser(std::string url) { | |||
#if ARCH_LIN | |||
std::string command = "xdg-open " + url; | |||
(void)system(command.c_str()); | |||
(void) system(command.c_str()); | |||
#endif | |||
#if ARCH_MAC | |||
std::string command = "open " + url; | |||
@@ -439,7 +439,7 @@ void windowRun() { | |||
windowTitle += gApplicationVersion; | |||
if (!gRackWidget->lastPath.empty()) { | |||
windowTitle += " - "; | |||
windowTitle += extractFilename(gRackWidget->lastPath); | |||
windowTitle += stringFilename(gRackWidget->lastPath); | |||
} | |||
if (windowTitle != lastWindowTitle) { | |||
glfwSetWindowTitle(gWindow, windowTitle.c_str()); | |||