71b8e77935
The user can add an arbitrary number of highly configurable shell commands through the setting panel. The commands can be saved as user preferences, inside the .fl file, or exported to an external file. Shell scripts can be limited to individual platforms, can have shortcut keys, etc. . * documentation will follow * support to call `fltk-config` will follow
943 lines
29 KiB
C++
943 lines
29 KiB
C++
//
|
|
// FLUID main entry for the Fast Light Tool Kit (FLTK).
|
|
//
|
|
// Copyright 1998-2023 by Bill Spitzak and others.
|
|
//
|
|
// This library is free software. Distribution and use rights are outlined in
|
|
// the file "COPYING" which should have been included with this file. If this
|
|
// file is missing or damaged, see the license at:
|
|
//
|
|
// https://www.fltk.org/COPYING.php
|
|
//
|
|
// Please see the following page on how to report bugs and issues:
|
|
//
|
|
// https://www.fltk.org/bugs.php
|
|
//
|
|
|
|
// in progress:
|
|
// FLUID comes with example shell commands to build the current project file
|
|
// and run the project. This is accomplished by calling `fltk-config` on the
|
|
// files generated by FLUID, and by calling the executable directly.
|
|
//
|
|
// If the user wants more complex commands, he can add or modify them in the
|
|
// "Shell" settings panel. Modified shell commands are saved with the .fl
|
|
// file.
|
|
|
|
// The Shell panel has a list of shell commands in the upper half. Under the
|
|
// list are buttons to add, duplicate, and delete shell commands. A popup
|
|
// menu offers import and export functionality and a list of sample scripts.
|
|
// We may want to add up and down buttons, so the user can change the
|
|
// order of commands.
|
|
|
|
// Selecting any shell command in the list fills in and activates a list of
|
|
// options in the lower half of the panel. Those settings are:
|
|
// - Name: the name of the shell command in the list
|
|
// - Label: the label in the pulldown menu (could be the same as name?)
|
|
// - Shortcut: shortcut key to launch the command
|
|
// - Storage: where to store this shell command
|
|
// - Condition: pulldown menu to make the entry conditional for various
|
|
// target platforms, for example, a "Windows only" entry would only be added
|
|
// to the Shell menu on a Windows machine. Other options could be:
|
|
// - Linux only, macOS only, never (to make a list header!?), inactive?
|
|
// - Command: a multiline input for the actual shell command
|
|
// - Variables: a pulldown menu that insert variable names like $<sourcefile>
|
|
// - options to save project, code, and strings before running
|
|
// - test-run button
|
|
|
|
// TODO: add @APPDIR@?
|
|
// TODO: get a macro to find `fltk-config` @FLTK_CONFIG@
|
|
// TODO: add an input field so the user can insert their preferred file and path for fltk-config (user setting)
|
|
// `fltk-config` is actually tricky to find
|
|
// for live builds, we could check the program launch directory
|
|
// if we know where build/Xcode/bin/Debug/fluid is, we
|
|
// may or may not find ./build/Xcode/fltk-config
|
|
// on macOS with homebrew, we find /opt/homebrew/bin/fltk-config but the user
|
|
// can set their own install path.
|
|
// We can query the shell path, but that requires knowing the users shell (echo $SHELL).
|
|
// We can run the shell as a login shell with `-l`, so the user $PTH is set: /bin/bash -l -c 'fltk-config'
|
|
// The shell should output the path of the fltk-config that it found and why it is using that one.
|
|
// This can also output the fltk-config version.
|
|
// TODO: add a bunch of sensible sample shell commands
|
|
// TODO: when this new feature is used for the very first time, import two or three samples as initial user setting
|
|
// TODO: make the settings dialog resizable
|
|
// TODO: make g_shell_config static, not a pointer, but don't load anything in batch mode
|
|
|
|
// FEATURE: Fd_Tool_Store icons are currently redundant with @file and @save and could be improved
|
|
// FEATURE: hostname, username, getenv support?
|
|
// FEATURE: ad the files ./fluid.prefs and ./fluid.user.prefs as tool locations
|
|
|
|
/*
|
|
Some ideas:
|
|
|
|
default shell is in $SHELL on linux and macOS
|
|
|
|
On macOS, we can write Apple Scripts:
|
|
|
|
#!/usr/bin/env osascript
|
|
say "@BASENAME@"
|
|
|
|
osascript <<EOD
|
|
say "spark"
|
|
EOD
|
|
|
|
osascript <<EOD
|
|
tell application "Xcode"
|
|
build workspace document 1
|
|
end tell
|
|
EOD
|
|
*/
|
|
|
|
#include "shell_command.h"
|
|
|
|
#include "fluid.h"
|
|
#include "file.h"
|
|
#include "alignment_panel.h"
|
|
|
|
#include <FL/Fl_Double_Window.H>
|
|
#include <FL/Fl_Menu_Bar.H>
|
|
#include <FL/fl_message.H>
|
|
#include <FL/fl_string_functions.h>
|
|
|
|
#include <errno.h>
|
|
|
|
static Fl_String fltk_config_cmd;
|
|
static Fl_Process s_proc;
|
|
|
|
|
|
/** \class Fl_Process
|
|
Launch an external shell command.
|
|
*/
|
|
|
|
/**
|
|
Create a process manager
|
|
*/
|
|
Fl_Process::Fl_Process() {
|
|
_fpt= NULL;
|
|
}
|
|
|
|
/**
|
|
Destroy the project manager.
|
|
*/
|
|
Fl_Process::~Fl_Process() {
|
|
// TODO: check what we need to do if a task is still running
|
|
if (_fpt) close();
|
|
}
|
|
|
|
/**
|
|
Open a process.
|
|
|
|
\param[in] cmd the shell command that we want to run
|
|
\param[in] mode "r" or "w" for creating a stream that can read or write
|
|
\return a stream that is redirected from the shell command stdout
|
|
*/
|
|
FILE * Fl_Process::popen(const char *cmd, const char *mode) {
|
|
#if defined(_WIN32) && !defined(__CYGWIN__)
|
|
// PRECONDITIONS
|
|
if (!mode || !*mode || (*mode!='r' && *mode!='w') ) return NULL;
|
|
if (_fpt) close(); // close first before reuse
|
|
|
|
ptmode = *mode;
|
|
pin[0] = pin[1] = pout[0] = pout[1] = perr[0] = perr[1] = INVALID_HANDLE_VALUE;
|
|
// stderr to stdout wanted ?
|
|
int fusion = (strstr(cmd,"2>&1") !=NULL);
|
|
|
|
// Create windows pipes
|
|
if (!createPipe(pin) || !createPipe(pout) || (!fusion && !createPipe(perr) ) )
|
|
return freeHandles(); // error
|
|
|
|
// Initialize Startup Info
|
|
ZeroMemory(&si, sizeof(STARTUPINFO));
|
|
si.cb = sizeof(STARTUPINFO);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdInput = pin[0];
|
|
si.hStdOutput = pout[1];
|
|
si.hStdError = fusion ? pout[1] : perr [1];
|
|
|
|
if ( CreateProcess(NULL, (LPTSTR) cmd,NULL,NULL,TRUE,
|
|
DETACHED_PROCESS,NULL,NULL, &si, &pi)) {
|
|
// don't need theses handles inherited by child process:
|
|
clean_close(pin[0]); clean_close(pout[1]); clean_close(perr[1]);
|
|
HANDLE & h = *mode == 'r' ? pout[0] : pin[1];
|
|
_fpt = _fdopen(_open_osfhandle((fl_intptr_t) h,_O_BINARY),mode);
|
|
h= INVALID_HANDLE_VALUE; // reset the handle pointer that is shared
|
|
// with _fpt so we don't free it twice
|
|
}
|
|
|
|
if (!_fpt) freeHandles();
|
|
return _fpt;
|
|
#else
|
|
_fpt=::popen(cmd,mode);
|
|
return _fpt;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
Close the current process.
|
|
*/
|
|
int Fl_Process::close() {
|
|
#if defined(_WIN32) && !defined(__CYGWIN__)
|
|
if (_fpt) {
|
|
fclose(_fpt);
|
|
clean_close(perr[0]);
|
|
clean_close(pin[1]);
|
|
clean_close(pout[0]);
|
|
_fpt = NULL;
|
|
return 0;
|
|
}
|
|
return -1;
|
|
#else
|
|
int ret = ::pclose(_fpt);
|
|
_fpt=NULL;
|
|
return ret;
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
non-null if file is open.
|
|
|
|
\return the current file descriptor of the process' stdout
|
|
*/
|
|
FILE *Fl_Process::desc() const {
|
|
return _fpt;
|
|
}
|
|
|
|
/**
|
|
Receive a single line from the current process.
|
|
|
|
\param[out] line buffer to receive the line
|
|
\param[in] s size of the provided buffer
|
|
\return NULL if an error occurred, otherwise a pointer to the string
|
|
*/
|
|
char *Fl_Process::get_line(char * line, size_t s) const {
|
|
return _fpt ? fgets(line, (int)s, _fpt) : NULL;
|
|
}
|
|
|
|
// returns fileno(FILE*):
|
|
// (file must be open, i.e. _fpt must be non-null)
|
|
// *FIXME* we should find a better solution for the 'fileno' issue
|
|
// non null if file is open
|
|
int Fl_Process::get_fileno() const {
|
|
#ifdef _MSC_VER
|
|
return _fileno(_fpt); // suppress MSVC warning
|
|
#else
|
|
return fileno(_fpt);
|
|
#endif
|
|
}
|
|
|
|
#if defined(_WIN32) && !defined(__CYGWIN__)
|
|
|
|
bool Fl_Process::createPipe(HANDLE * h, BOOL bInheritHnd) {
|
|
SECURITY_ATTRIBUTES sa;
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = bInheritHnd;
|
|
return CreatePipe (&h[0],&h[1],&sa,0) ? true : false;
|
|
}
|
|
|
|
FILE *Fl_Process::freeHandles() {
|
|
clean_close(pin[0]); clean_close(pin[1]);
|
|
clean_close(pout[0]); clean_close(pout[1]);
|
|
clean_close(perr[0]); clean_close(perr[1]);
|
|
return NULL; // convenient for error management
|
|
}
|
|
|
|
void Fl_Process::clean_close(HANDLE& h) {
|
|
if (h!= INVALID_HANDLE_VALUE) CloseHandle(h);
|
|
h = INVALID_HANDLE_VALUE;
|
|
}
|
|
|
|
#endif
|
|
|
|
/**
|
|
Prepare FLUID for running a shell command according to the command flags.
|
|
|
|
\param[in] flags set various flags to save the project, code, and string before running the command
|
|
\return false if the previous command is still running
|
|
*/
|
|
static bool prepare_shell_command(int flags) {
|
|
// settings_window->hide();
|
|
if (s_proc.desc()) {
|
|
fl_alert("Previous shell command still running!");
|
|
return false;
|
|
}
|
|
if (flags & Fd_Shell_Command::SAVE_PROJECT) {
|
|
save_cb(0, 0);
|
|
}
|
|
if (flags & Fd_Shell_Command::SAVE_SOURCECODE) {
|
|
write_code_files();
|
|
}
|
|
if (flags & Fd_Shell_Command::SAVE_STRINGS) {
|
|
write_strings_cb(0, 0);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
Called by the file handler when the command is finished.
|
|
*/
|
|
void shell_proc_done() {
|
|
shell_run_terminal->append("... END SHELL COMMAND ...\n");
|
|
shell_run_button->activate();
|
|
shell_run_window->label("FLUID Shell");
|
|
fl_beep();
|
|
}
|
|
|
|
void shell_timer_cb(void*) {
|
|
if (!s_proc.desc()) {
|
|
shell_proc_done();
|
|
} else {
|
|
Fl::add_timeout(0.25, shell_timer_cb);
|
|
}
|
|
}
|
|
|
|
// Support the full piped shell command...
|
|
void shell_pipe_cb(FL_SOCKET, void*) {
|
|
char line[1024]=""; // Line from command output...
|
|
|
|
if (s_proc.get_line(line, sizeof(line)) != NULL) {
|
|
// Add the line to the output list...
|
|
shell_run_terminal->append(line);
|
|
} else {
|
|
// End of file; tell the parent...
|
|
Fl::remove_timeout(shell_timer_cb);
|
|
Fl::remove_fd(s_proc.get_fileno());
|
|
s_proc.close();
|
|
shell_proc_done();
|
|
}
|
|
}
|
|
|
|
/** Find the script `fltk-config` that most closely relates to this version of FLUID.
|
|
This is not implemented yet.
|
|
*/
|
|
//static void find_fltk_config() {
|
|
//
|
|
//}
|
|
|
|
static void expand_macro(Fl_String &cmd, const Fl_String ¯o, const Fl_String &content) {
|
|
for (int i=0;;) {
|
|
i = cmd.find(macro, i);
|
|
if (i==Fl_String::npos) break;
|
|
cmd.replace(i, macro.size(), content);
|
|
}
|
|
}
|
|
|
|
static void expand_macros(Fl_String &cmd) {
|
|
expand_macro(cmd, "@BASENAME@", g_project.basename());
|
|
expand_macro(cmd, "@PROJECTFILE_PATH@", g_project.projectfile_path());
|
|
expand_macro(cmd, "@PROJECTFILE_NAME@", g_project.projectfile_name());
|
|
expand_macro(cmd, "@CODEFILE_PATH@", g_project.codefile_path());
|
|
expand_macro(cmd, "@CODEFILE_NAME@", g_project.codefile_name());
|
|
expand_macro(cmd, "@HEADERFILE_PATH@", g_project.headerfile_path());
|
|
expand_macro(cmd, "@HEADERFILE_NAME@", g_project.headerfile_name());
|
|
expand_macro(cmd, "@TEXTFILE_PATH@", g_project.stringsfile_path());
|
|
expand_macro(cmd, "@TEXTFILE_NAME@", g_project.stringsfile_name());
|
|
// TODO: implement finding the script `fltk-config` for all platforms
|
|
// if (cmd.find("@FLTK_CONFIG@") != Fl_String::npos) {
|
|
// find_fltk_config();
|
|
// expand_macro(cmd, "@FLTK_CONFIG@", fltk_config_cmd.c_str());
|
|
// }
|
|
if (cmd.find("@TMPDIR@") != Fl_String::npos)
|
|
expand_macro(cmd, "@TMPDIR@", get_tmpdir());
|
|
}
|
|
|
|
/**
|
|
Prepare for and run a shell command.
|
|
|
|
\param[in] cmd the command that is sent to `/bin/sh -c ...` or `cmd.exe` on Windows machines
|
|
\param[in] flags various flags in preparation of the command
|
|
*/
|
|
void run_shell_command(const Fl_String &cmd, int flags) {
|
|
if (cmd.empty()) {
|
|
fl_alert("No shell command entered!");
|
|
return;
|
|
}
|
|
|
|
if (!prepare_shell_command(flags)) return;
|
|
|
|
Fl_String expanded_cmd = cmd;
|
|
expand_macros(expanded_cmd);
|
|
|
|
if (!shell_run_window->visible()) {
|
|
Fl_Preferences pos(fluid_prefs, "shell_run_Window_pos");
|
|
int x, y, w, h;
|
|
pos.get("x", x, -1);
|
|
pos.get("y", y, 0);
|
|
pos.get("w", w, 640);
|
|
pos.get("h", h, 480);
|
|
if (x!=-1) {
|
|
shell_run_window->resize(x, y, w, h);
|
|
}
|
|
shell_run_window->show();
|
|
}
|
|
|
|
// Show the output window and clear things...
|
|
shell_run_terminal->printf("\033[0;32m%s\033[0m\n", expanded_cmd.c_str());
|
|
shell_run_window->label(expanded_cmd.c_str());
|
|
|
|
if (s_proc.popen((char *)expanded_cmd.c_str()) == NULL) {
|
|
shell_run_terminal->printf("\033[1;31mUnable to run shell command: %s\033[0m\n",
|
|
strerror(errno));
|
|
shell_run_window->label("FLUID Shell");
|
|
return;
|
|
}
|
|
shell_run_button->deactivate();
|
|
|
|
// if the function below does not for some reason, we will check periodically
|
|
// to see if the command is done
|
|
Fl::add_timeout(0.25, shell_timer_cb);
|
|
// this will tell us when the shell command is done
|
|
Fl::add_fd(s_proc.get_fileno(), shell_pipe_cb);
|
|
}
|
|
|
|
/**
|
|
Create an empty shell command structure.
|
|
*/
|
|
Fd_Shell_Command::Fd_Shell_Command()
|
|
: shortcut(0),
|
|
storage(FD_STORE_USER),
|
|
condition(0),
|
|
flags(0),
|
|
shell_menu_item_(NULL)
|
|
{
|
|
}
|
|
|
|
/**
|
|
Copy the aspects of a shell command dataset into a new shell command.
|
|
|
|
\param[in] rhs copy from this prototype
|
|
*/
|
|
Fd_Shell_Command::Fd_Shell_Command(const Fd_Shell_Command *rhs)
|
|
: name(rhs->name),
|
|
label(rhs->label),
|
|
shortcut(rhs->shortcut),
|
|
storage(rhs->storage),
|
|
condition(rhs->condition),
|
|
condition_data(rhs->condition_data),
|
|
command(rhs->command),
|
|
flags(rhs->flags),
|
|
shell_menu_item_(NULL)
|
|
{
|
|
}
|
|
|
|
/**
|
|
Create a default storage for a shell command and how it is accessible in FLUID.
|
|
|
|
\param[in] name is used as a stand-in for the command name and label
|
|
*/
|
|
Fd_Shell_Command::Fd_Shell_Command(const Fl_String &in_name)
|
|
: name(in_name),
|
|
label(in_name),
|
|
shortcut(0),
|
|
storage(FD_STORE_USER),
|
|
condition(Fd_Shell_Command::ALWAYS),
|
|
command("echo \"Hello, FLUID!\""),
|
|
flags(Fd_Shell_Command::SAVE_PROJECT|Fd_Shell_Command::SAVE_SOURCECODE),
|
|
shell_menu_item_(NULL)
|
|
{
|
|
}
|
|
|
|
/**
|
|
Create a storage for a shell command and how it is accessible in FLUID.
|
|
|
|
\param[in] in_name name of this command in the command list in the settings panel
|
|
\param[in] in_label label text in the main pulldown menu
|
|
\param[in] in_shortcut a keyboard shortcut that will also appear in the main menu
|
|
\param[in] in_storage storage location for this command
|
|
\param[in] in_condition commands can be hidden for certain platforms by setting a condition
|
|
\param[in] in_condition_data more details for future conditions, i.e. per user, per host, etc.
|
|
\param[in] in_command the shell command that we want to run
|
|
\param[in] in_flags some flags to tell FLUID to save the project, code, or strings before running the command
|
|
*/
|
|
Fd_Shell_Command::Fd_Shell_Command(const Fl_String &in_name,
|
|
const Fl_String &in_label,
|
|
Fl_Shortcut in_shortcut,
|
|
Fd_Tool_Store in_storage,
|
|
int in_condition,
|
|
const Fl_String &in_condition_data,
|
|
const Fl_String &in_command,
|
|
int in_flags)
|
|
: name(in_name),
|
|
label(in_label),
|
|
shortcut(in_shortcut),
|
|
storage(in_storage),
|
|
condition(in_condition),
|
|
condition_data(in_condition_data),
|
|
command(in_command),
|
|
flags(in_flags),
|
|
shell_menu_item_(NULL)
|
|
{
|
|
}
|
|
|
|
/**
|
|
Run this command now.
|
|
|
|
Will open the Shell Panel and execute the command if no other command is
|
|
currently running.
|
|
*/
|
|
void Fd_Shell_Command::run() {
|
|
if (!command.empty())
|
|
run_shell_command(command, flags);
|
|
}
|
|
|
|
/**
|
|
Update the shell submenu in main menu with the shortcut and a copy of the label.
|
|
*/
|
|
void Fd_Shell_Command::update_shell_menu() {
|
|
if (shell_menu_item_) {
|
|
const char *old_label = shell_menu_item_->label(); // can be NULL
|
|
const char *new_label = label.c_str(); // never NULL
|
|
if (!old_label || (old_label && strcmp(old_label, new_label))) {
|
|
if (old_label) ::free((void*)old_label);
|
|
shell_menu_item_->label(fl_strdup(new_label));
|
|
}
|
|
shell_menu_item_->shortcut(shortcut);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Check if the set condition is met.
|
|
|
|
\return true if this command appears in the main menu
|
|
*/
|
|
bool Fd_Shell_Command::is_active() {
|
|
switch (condition) {
|
|
case ALWAYS: return true;
|
|
case NEVER: return false;
|
|
#ifdef _WIN32
|
|
case MAC_ONLY: return false;
|
|
case UX_ONLY: return false;
|
|
case WIN_ONLY: return true;
|
|
case MAC_AND_UX_ONLY: return false;
|
|
#elif defined(__APPLE__)
|
|
case MAC_ONLY: return true;
|
|
case UX_ONLY: return false;
|
|
case WIN_ONLY: return false;
|
|
case MAC_AND_UX_ONLY: return true;
|
|
#else
|
|
case MAC_ONLY: return false;
|
|
case UX_ONLY: return true;
|
|
case WIN_ONLY: return false;
|
|
case MAC_AND_UX_ONLY: return true;
|
|
#endif
|
|
case USER_ONLY: return false; // TODO: get user name
|
|
case HOST_ONLY: return false; // TODO: get host name
|
|
case ENV_ONLY: {
|
|
const char *value = fl_getenv(condition_data.c_str());
|
|
if (value && *value) return true;
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Fd_Shell_Command::read(Fl_Preferences &prefs) {
|
|
int tmp;
|
|
prefs.get("name", name, "<unnamed>");
|
|
prefs.get("label", label, "<no label>");
|
|
prefs.get("shortcut", tmp, 0);
|
|
shortcut = (Fl_Shortcut)tmp;
|
|
prefs.get("storage", tmp, -1);
|
|
if (tmp != -1) storage = (Fd_Tool_Store)tmp;
|
|
prefs.get("condition", condition, ALWAYS);
|
|
prefs.get("condition_data", condition_data, "");
|
|
prefs.get("command", command, "");
|
|
prefs.get("flags", flags, 0);
|
|
}
|
|
|
|
void Fd_Shell_Command::write(Fl_Preferences &prefs, bool save_location) {
|
|
prefs.set("name", name);
|
|
prefs.set("label", label);
|
|
if (shortcut != 0) prefs.set("shortcut", (int)shortcut);
|
|
if (save_location) prefs.set("storage", (int)storage);
|
|
if (condition != ALWAYS) prefs.set("condition", condition);
|
|
if (!condition_data.empty()) prefs.set("condition_data", condition_data);
|
|
if (!command.empty()) prefs.set("command", command);
|
|
if (flags != 0) prefs.set("flags", flags);
|
|
}
|
|
|
|
void Fd_Shell_Command::read(class Fd_Project_Reader *in) {
|
|
const char *c = in->read_word(1);
|
|
if (strcmp(c, "{")!=0) return; // expecting start of group
|
|
storage = FD_STORE_PROJECT;
|
|
for (;;) {
|
|
c = in->read_word(1);
|
|
if (strcmp(c, "}")==0) break; // end of command list
|
|
else if (strcmp(c, "name")==0)
|
|
name = in->read_word();
|
|
else if (strcmp(c, "label")==0)
|
|
label = in->read_word();
|
|
else if (strcmp(c, "shortcut")==0)
|
|
shortcut = in->read_int();
|
|
else if (strcmp(c, "condition")==0)
|
|
condition = in->read_int();
|
|
else if (strcmp(c, "condition_data")==0)
|
|
condition_data = in->read_word();
|
|
else if (strcmp(c, "command")==0)
|
|
command = in->read_word();
|
|
else if (strcmp(c, "flags")==0)
|
|
flags = in->read_int();
|
|
else
|
|
in->read_word(); // skip an unknown word
|
|
}
|
|
}
|
|
|
|
void Fd_Shell_Command::write(class Fd_Project_Writer *out) {
|
|
out->write_string("\n command {");
|
|
out->write_string("\n name "); out->write_word(name.c_str());
|
|
out->write_string("\n label "); out->write_word(label.c_str());
|
|
if (shortcut) out->write_string("\n shortcut %d", shortcut);
|
|
if (condition) out->write_string("\n condition %d", condition);
|
|
if (!condition_data.empty()) {
|
|
out->write_string("\n condition_data "); out->write_word(condition_data.c_str());
|
|
}
|
|
if (!command.empty()) {
|
|
out->write_string("\n command "); out->write_word(command.c_str());
|
|
}
|
|
if (flags) out->write_string("\n flags %d", flags);
|
|
out->write_string("\n }");
|
|
}
|
|
|
|
|
|
/**
|
|
Manage a list of shell commands and their parameters.
|
|
*/
|
|
Fd_Shell_Command_List::Fd_Shell_Command_List()
|
|
: list(NULL),
|
|
list_size(0),
|
|
list_capacity(0),
|
|
shell_menu_(NULL)
|
|
{
|
|
}
|
|
|
|
/**
|
|
Release all shell commands and destroy this class.
|
|
*/
|
|
Fd_Shell_Command_List::~Fd_Shell_Command_List() {
|
|
clear();
|
|
}
|
|
|
|
/**
|
|
Return the shell command at the given index.
|
|
|
|
\param[in] index must be between 0 and list_size-1
|
|
\return a pointer to the shell command data
|
|
*/
|
|
Fd_Shell_Command *Fd_Shell_Command_List::at(int index) const {
|
|
return list[index];
|
|
}
|
|
|
|
/**
|
|
Clear all shell commands.
|
|
*/
|
|
void Fd_Shell_Command_List::clear() {
|
|
if (list) {
|
|
for (int i=0; i<list_size; i++) {
|
|
delete list[i];
|
|
}
|
|
::free(list);
|
|
list_size = 0;
|
|
list_capacity = 0;
|
|
list = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
remove all shell commands of the given storage location from the list.
|
|
*/
|
|
void Fd_Shell_Command_List::clear(Fd_Tool_Store storage) {
|
|
for (int i=list_size-1; i>=0; i--) {
|
|
if (list[i]->storage == storage) {
|
|
remove(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Read shell configuration from a preferences group.
|
|
*/
|
|
void Fd_Shell_Command_List::read(Fl_Preferences &prefs, Fd_Tool_Store storage) {
|
|
// import the old shell commands from previous user settings
|
|
if (&fluid_prefs == &prefs) {
|
|
int version;
|
|
prefs.get("shell_commands_version", version, 0);
|
|
if (version == 0) {
|
|
int save_fl, save_code, save_strings;
|
|
Fd_Shell_Command *cmd = new Fd_Shell_Command();
|
|
cmd->storage = FD_STORE_USER;
|
|
cmd->name = "Sample Shell Command";
|
|
cmd->label = "Sample Shell Command";
|
|
cmd->shortcut = FL_ALT+'g';
|
|
fluid_prefs.get("shell_command", cmd->command, "echo \"Sample Shell Command\"");
|
|
fluid_prefs.get("shell_savefl", save_fl, 1);
|
|
fluid_prefs.get("shell_writecode", save_code, 1);
|
|
fluid_prefs.get("shell_writemsgs", save_strings, 0);
|
|
if (save_fl) cmd->flags |= Fd_Shell_Command::SAVE_PROJECT;
|
|
if (save_code) cmd->flags |= Fd_Shell_Command::SAVE_SOURCECODE;
|
|
if (save_strings) cmd->flags |= Fd_Shell_Command::SAVE_STRINGS;
|
|
add(cmd);
|
|
}
|
|
version = 1;
|
|
prefs.set("shell_commands_version", version);
|
|
}
|
|
Fl_Preferences shell_commands(prefs, "shell_commands");
|
|
int n = shell_commands.groups();
|
|
for (int i=0; i<n; i++) {
|
|
Fl_Preferences cmd_prefs(shell_commands, Fl_Preferences::Name(i));
|
|
Fd_Shell_Command *cmd = new Fd_Shell_Command();
|
|
cmd->storage = FD_STORE_USER;
|
|
cmd->read(cmd_prefs);
|
|
add(cmd);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Write shell configuration to a preferences group.
|
|
*/
|
|
void Fd_Shell_Command_List::write(Fl_Preferences &prefs, Fd_Tool_Store storage) {
|
|
Fl_Preferences shell_commands(prefs, "shell_commands");
|
|
shell_commands.delete_all_groups();
|
|
int index = 0;
|
|
for (int i=0; i<list_size; i++) {
|
|
if (list[i]->storage == FD_STORE_USER) {
|
|
Fl_Preferences cmd(shell_commands, Fl_Preferences::Name(index++));
|
|
list[i]->write(cmd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Read shell configuration from a project file.
|
|
*/
|
|
void Fd_Shell_Command_List::read(Fd_Project_Reader *in) {
|
|
const char *c = in->read_word(1);
|
|
if (strcmp(c, "{")!=0) return; // expecting start of group
|
|
clear(FD_STORE_PROJECT);
|
|
for (;;) {
|
|
c = in->read_word(1);
|
|
if (strcmp(c, "}")==0) break; // end of command list
|
|
else if (strcmp(c, "command")==0) {
|
|
Fd_Shell_Command *cmd = new Fd_Shell_Command();
|
|
add(cmd);
|
|
cmd->read(in);
|
|
} else {
|
|
in->read_word(); // skip an unknown group
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Write shell configuration to a project file.
|
|
*/
|
|
void Fd_Shell_Command_List::write(Fd_Project_Writer *out) {
|
|
int n_in_project_file = 0;
|
|
for (int i=0; i<list_size; i++) {
|
|
if (list[i]->storage == FD_STORE_PROJECT)
|
|
n_in_project_file++;
|
|
}
|
|
if (n_in_project_file > 0) {
|
|
out->write_string("\nshell_commands {");
|
|
for (int i=0; i<list_size; i++) {
|
|
if (list[i]->storage == FD_STORE_PROJECT)
|
|
list[i]->write(out);
|
|
}
|
|
out->write_string("\n}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
Add a previously created shell command to the end of the list.
|
|
|
|
\param[in] cmd a pointer to the command that we want to add
|
|
*/
|
|
void Fd_Shell_Command_List::add(Fd_Shell_Command *cmd) {
|
|
if (list_size == list_capacity) {
|
|
list_capacity += 16;
|
|
list = (Fd_Shell_Command**)::realloc(list, list_capacity * sizeof(Fd_Shell_Command**));
|
|
}
|
|
list[list_size++] = cmd;
|
|
}
|
|
|
|
/**
|
|
Insert a newly created shell command at the given position in the list.
|
|
|
|
\param[in] index must be between 0 and list_size-1
|
|
\param[in] cmd a pointer to the command that we want to add
|
|
*/
|
|
void Fd_Shell_Command_List::insert(int index, Fd_Shell_Command *cmd) {
|
|
if (list_size == list_capacity) {
|
|
list_capacity += 16;
|
|
list = (Fd_Shell_Command**)::realloc(list, list_capacity * sizeof(Fd_Shell_Command**));
|
|
}
|
|
::memmove(list+index+1, list+index, (list_size-index)*sizeof(Fd_Shell_Command**));
|
|
list_size++;
|
|
list[index] = cmd;
|
|
}
|
|
|
|
/**
|
|
Remove and delete the command at the given index.
|
|
|
|
\param[in] index must be between 0 and list_size-1
|
|
*/
|
|
void Fd_Shell_Command_List::remove(int index) {
|
|
delete list[index];
|
|
list_size--;
|
|
::memmove(list+index, list+index+1, (list_size-index)*sizeof(Fd_Shell_Command**));
|
|
}
|
|
|
|
/**
|
|
This is called whenever the user clicks a shell command menu in the main menu.
|
|
|
|
\param[in] u cast tp long to get the index of the shell command
|
|
*/
|
|
void menu_shell_cmd_cb(Fl_Widget*, void *u) {
|
|
long index = (long)(fl_intptr_t)u;
|
|
g_shell_config->list[index]->run();
|
|
}
|
|
|
|
/**
|
|
This is called when the user selects the menu to edit the shell commands.
|
|
It pops up the setting panel at the shell settings tab.
|
|
*/
|
|
void menu_shell_customize_cb(Fl_Widget*, void*) {
|
|
settings_window->show();
|
|
w_settings_tabs->value(w_settings_shell_tab);
|
|
}
|
|
|
|
/**
|
|
Rebuild the entire shell submenu from scratch and replace the old menu.
|
|
*/
|
|
void Fd_Shell_Command_List::rebuild_shell_menu() {
|
|
static Fl_Menu_Item *shell_submenu = NULL;
|
|
if (!shell_submenu)
|
|
shell_submenu = (Fl_Menu_Item*)main_menubar->find_item(menu_marker);
|
|
|
|
int i, j, num_active_items = 0;
|
|
// count the active commands
|
|
for (i=0; i<list_size; i++) {
|
|
if (list[i]->is_active()) num_active_items++;
|
|
}
|
|
// allocate a menu item array
|
|
Fl_Menu_Item *mi = (Fl_Menu_Item*)::calloc(num_active_items+2, sizeof(Fl_Menu_Item));
|
|
// set the menu item pointer for all active commands
|
|
for (i=j=0; i<list_size; i++) {
|
|
Fd_Shell_Command *cmd = list[i];
|
|
if (cmd->is_active()) {
|
|
cmd->shell_menu_item_ = mi + j;
|
|
mi[j].callback(menu_shell_cmd_cb);
|
|
mi[j].argument(i);
|
|
cmd->update_shell_menu();
|
|
j++;
|
|
}
|
|
}
|
|
if (j>0) mi[j-1].flags |= FL_MENU_DIVIDER;
|
|
mi[j].label(fl_strdup("Customize..."));
|
|
mi[j].shortcut(FL_ALT+'x');
|
|
mi[j].callback(menu_shell_customize_cb);
|
|
// replace the old menu array with the new one
|
|
Fl_Menu_Item *mi_old = shell_menu_;
|
|
shell_menu_ = mi;
|
|
shell_submenu->user_data(shell_menu_);
|
|
// free all resources from the old menu
|
|
if (mi_old && (mi_old != default_menu)) {
|
|
for (i=0; ; i++) {
|
|
const char *label = mi_old[i].label();
|
|
if (!label) break;
|
|
::free((void*)label);
|
|
}
|
|
::free(mi_old);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Tell the settings dialog to query this list and update its GUI elements.
|
|
*/
|
|
void Fd_Shell_Command_List::update_settings_dialog() {
|
|
if (w_settings_shell_tab)
|
|
w_settings_shell_tab->do_callback(w_settings_shell_tab, LOAD);
|
|
}
|
|
|
|
/**
|
|
The default shell submenu in batch mode.
|
|
*/
|
|
Fl_Menu_Item Fd_Shell_Command_List::default_menu[] = {
|
|
{ "Customize...", FL_ALT+'x', menu_shell_customize_cb },
|
|
{ NULL }
|
|
};
|
|
|
|
/**
|
|
Used to find the shell submenu within the main menu tree.
|
|
*/
|
|
void Fd_Shell_Command_List::menu_marker(Fl_Widget*, void*) {
|
|
// intentionally left empty
|
|
}
|
|
|
|
/**
|
|
Export all selected shell commands to an external file.
|
|
|
|
Verify that g_shell_config and w_settings_shell_list are not NULL. Open a
|
|
file chooser and export all items that are selected in w_settings_shell_list
|
|
into an external file.
|
|
*/
|
|
void Fd_Shell_Command_List::export_selected() {
|
|
if (!g_shell_config || (g_shell_config->list_size == 0)) return;
|
|
if (!w_settings_shell_list) return;
|
|
|
|
Fl_Native_File_Chooser dialog;
|
|
dialog.title("Export selected shell commands:");
|
|
dialog.type(Fl_Native_File_Chooser::BROWSE_SAVE_FILE);
|
|
dialog.filter("FLUID Files\t*.flcmd\n");
|
|
dialog.directory(g_project.projectfile_path().c_str());
|
|
dialog.preset_file((g_project.basename() + ".flcmd").c_str());
|
|
if (dialog.show() != 0) return;
|
|
|
|
Fl_Preferences file(dialog.filename(), "flcmd.fluid.fltk.org", NULL, (Fl_Preferences::Root)(Fl_Preferences::C_LOCALE|Fl_Preferences::CLEAR));
|
|
Fl_Preferences shell_commands(file, "shell_commands");
|
|
int i, index = 0, n = w_settings_shell_list->size();
|
|
for (i = 0; i < n; i++) {
|
|
if (w_settings_shell_list->selected(i+1)) {
|
|
Fl_Preferences cmd(shell_commands, Fl_Preferences::Name(index++));
|
|
g_shell_config->list[i]->write(cmd, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Import shell commands from an external file and add them to the list.
|
|
|
|
Verify that g_shell_config and w_settings_shell_list are not NULL. Open a
|
|
file chooser and import all items.
|
|
*/
|
|
void Fd_Shell_Command_List::import_from_file() {
|
|
if (!g_shell_config || (g_shell_config->list_size == 0)) return;
|
|
if (!w_settings_shell_list) return;
|
|
|
|
Fl_Native_File_Chooser dialog;
|
|
dialog.title("Import shell commands:");
|
|
dialog.type(Fl_Native_File_Chooser::BROWSE_FILE);
|
|
dialog.filter("FLUID Files\t*.flcmd\n");
|
|
dialog.directory(g_project.projectfile_path().c_str());
|
|
dialog.preset_file((g_project.basename() + ".flcmd").c_str());
|
|
if (dialog.show() != 0) return;
|
|
|
|
Fl_Preferences file(dialog.filename(), "flcmd.fluid.fltk.org", NULL, Fl_Preferences::C_LOCALE);
|
|
Fl_Preferences shell_commands(file, "shell_commands");
|
|
int i, n = shell_commands.groups();
|
|
for (i = 0; i < n; i++) {
|
|
Fl_Preferences cmd_prefs(shell_commands, Fl_Preferences::Name(i));
|
|
Fd_Shell_Command *cmd = new Fd_Shell_Command();
|
|
cmd->storage = FD_STORE_USER;
|
|
cmd->read(cmd_prefs);
|
|
g_shell_config->add(cmd);
|
|
}
|
|
w_settings_shell_list->do_callback(w_settings_shell_list, LOAD);
|
|
w_settings_shell_cmd->do_callback(w_settings_shell_cmd, LOAD);
|
|
w_settings_shell_toolbox->do_callback(w_settings_shell_toolbox, LOAD);
|
|
g_shell_config->rebuild_shell_menu();
|
|
}
|
|
|
|
/**
|
|
A pointer to the list of shell commands if we are not in batch mode.
|
|
*/
|
|
Fd_Shell_Command_List *g_shell_config = NULL;
|
|
|