summaryrefslogtreecommitdiff
path: root/fluid/shell_command.cxx
diff options
context:
space:
mode:
authorMatthias Melcher <github@matthiasm.com>2023-09-26 15:01:03 +0100
committerGitHub <noreply@github.com>2023-09-26 16:01:03 +0200
commit71b8e77935bc9b0a3afc97d322037f2549165d2f (patch)
treea7e5e0cfec35b0ab30d6369d7b9ecd9eb3ddd48b /fluid/shell_command.cxx
parent2bbdd4946561444fffba2bf047b0926a2cf9f5fe (diff)
FLUID: adds greatly enhanced Shell Commands (#774)
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
Diffstat (limited to 'fluid/shell_command.cxx')
-rw-r--r--fluid/shell_command.cxx832
1 files changed, 709 insertions, 123 deletions
diff --git a/fluid/shell_command.cxx b/fluid/shell_command.cxx
index cd764ca24..c557c6e54 100644
--- a/fluid/shell_command.cxx
+++ b/fluid/shell_command.cxx
@@ -1,7 +1,7 @@
//
// FLUID main entry for the Fast Light Tool Kit (FLTK).
//
-// Copyright 1998-2021 by Bill Spitzak and others.
+// 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
@@ -14,125 +14,122 @@
// 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;
-/// Shell settings in the .fl file
-Shell_Settings shell_settings_windows = { };
-Shell_Settings shell_settings_linux = { };
-Shell_Settings shell_settings_macos = { };
-
-/// Current shell command, stored in .fl file for each platform, and in app prefs
-Fl_String g_shell_command;
-
-/// Save .fl file before running, stored in .fl file for each platform, and in app prefs
-int g_shell_save_fl = 1;
-
-/// Save code file before running, stored in .fl file for each platform, and in app prefs
-int g_shell_save_code = 1;
-
-/// Save strings file before running, stored in .fl file for each platform, and in app prefs
-int g_shell_save_strings = 0;
-
-/// Use these settings from .fl files, stored in app prefs
-int g_shell_use_fl_settings = 1;
-/**
- Read the default shell settings from the app preferences.
+/** \class Fl_Process
+ Launch an external shell command.
*/
-void shell_prefs_get()
-{
- fluid_prefs.get("shell_command", g_shell_command, "echo \"Custom Shell Command\"");
- fluid_prefs.get("shell_savefl", g_shell_save_fl, 1);
- fluid_prefs.get("shell_writecode", g_shell_save_code, 1);
- fluid_prefs.get("shell_writemsgs", g_shell_save_strings, 0);
- fluid_prefs.get("shell_use_fl", g_shell_use_fl_settings, 1);
-}
/**
- Write the current shell settings to the app preferences.
+ Create a process manager
*/
-void shell_prefs_set()
-{
- fluid_prefs.set("shell_command", g_shell_command);
- fluid_prefs.set("shell_savefl", g_shell_save_fl);
- fluid_prefs.set("shell_writecode", g_shell_save_code);
- fluid_prefs.set("shell_writemsgs", g_shell_save_strings);
- fluid_prefs.set("shell_use_fl", g_shell_use_fl_settings);
+Fl_Process::Fl_Process() {
+ _fpt= NULL;
}
/**
- Copy shell settings from the .fl buffer if use_fl_settings is set.
+ Destroy the project manager.
*/
-void shell_settings_read()
-{
- if (g_shell_use_fl_settings==0)
- return;
-#if defined(_WIN32)
- Shell_Settings &shell_settings = shell_settings_windows;
-#elif defined(__APPLE__)
- Shell_Settings &shell_settings = shell_settings_macos;
-#else
- Shell_Settings &shell_settings = shell_settings_linux;
-#endif
- g_shell_command = shell_settings.command;
- g_shell_save_fl = ((shell_settings.flags&1)==1);
- g_shell_save_code = ((shell_settings.flags&2)==2);
- g_shell_save_strings = ((shell_settings.flags&4)==4);
+Fl_Process::~Fl_Process() {
+ // TODO: check what we need to do if a task is still running
+ if (_fpt) close();
}
/**
- Copy current shell settings to the .fl buffer if use_fl_settings is set.
- */
-void shell_settings_write()
-{
- if (g_shell_use_fl_settings==0)
- return;
-#if defined(_WIN32)
- Shell_Settings &shell_settings = shell_settings_windows;
-#elif defined(__APPLE__)
- Shell_Settings &shell_settings = shell_settings_macos;
-#else
- Shell_Settings &shell_settings = shell_settings_linux;
-#endif
- if (shell_settings.command)
- free((void*)shell_settings.command);
- shell_settings.command = NULL;
- if (!g_shell_command.empty())
- shell_settings.command = fl_strdup(g_shell_command.c_str());
- shell_settings.flags = 0;
- if (g_shell_save_fl)
- shell_settings.flags |= 1;
- if (g_shell_save_code)
- shell_settings.flags |= 2;
- if (g_shell_save_strings)
- shell_settings.flags |= 4;
-}
+ Open a process.
-/** \class Fl_Process
- \todo Explain.
+ \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
*/
-
-Fl_Process::Fl_Process() {
- _fpt= NULL;
-}
-
-Fl_Process::~Fl_Process() {
- if (_fpt) close();
-}
-
-// FIXME: popen needs the UTF-8 equivalent fl_popen
-// portable open process:
FILE * Fl_Process::popen(const char *cmd, const char *mode) {
#if defined(_WIN32) && !defined(__CYGWIN__)
// PRECONDITIONS
@@ -174,6 +171,9 @@ FILE * Fl_Process::popen(const char *cmd, const char *mode) {
#endif
}
+/**
+ Close the current process.
+ */
int Fl_Process::close() {
#if defined(_WIN32) && !defined(__CYGWIN__)
if (_fpt) {
@@ -192,11 +192,22 @@ int Fl_Process::close() {
#endif
}
-// non-null if file is open
+/**
+ 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;
}
@@ -237,31 +248,33 @@ void Fl_Process::clean_close(HANDLE& h) {
#endif
+/**
+ Prepare FLUID for running a shell command according to the command flags.
-// Shell command support...
-
-static bool prepare_shell_command() {
+ \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 (g_shell_command.empty()) {
- fl_alert("No shell command entered!");
- return false;
- }
- if (g_shell_save_fl) {
+ if (flags & Fd_Shell_Command::SAVE_PROJECT) {
save_cb(0, 0);
}
- if (g_shell_save_code) {
+ if (flags & Fd_Shell_Command::SAVE_SOURCECODE) {
write_code_files();
}
- if (g_shell_save_strings) {
+ 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();
@@ -293,8 +306,56 @@ void shell_pipe_cb(FL_SOCKET, void*) {
}
}
-void do_shell_command(Fl_Return_Button*, void*) {
- if (!prepare_shell_command()) return;
+/** 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 &macro, 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");
@@ -310,10 +371,10 @@ void do_shell_command(Fl_Return_Button*, void*) {
}
// Show the output window and clear things...
- shell_run_terminal->printf("\033[0;32m%s\033[0m\n", g_shell_command.c_str());
- shell_run_window->label(g_shell_command.c_str());
+ 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 *)g_shell_command.c_str()) == NULL) {
+ 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");
@@ -329,28 +390,553 @@ void do_shell_command(Fl_Return_Button*, void*) {
}
/**
- Show a dialog box to run an external shell command.
+ 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);
+ }
+ }
+}
- Copies the current settings into the dialog box.
+/**
+ 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);
+ }
+}
- This dialog box offers a field for a command line and three check buttons
- to generate and save various files before the command is run.
+/**
+ 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);
+ }
+ }
+}
- If the fourth checkbox, "use settings in .fl design files" is checked,
- all shell settings will be store in the current .fl file, and they will
- be read and restored when the .fl is loaded again.
+/**
+ 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
+ }
+ }
+}
- Fluid will save different shell settings for different operating system as
- it is common that a different OS requires a different shell command.
+/**
+ 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}");
+ }
+}
- Fluid comes with default shell settings. Pressing the "save as default" button
- will store the current setting in the Fluid app settings and are used for new
- designs, or if the "use settings..." box is not checked.
+/**
+ Add a previously created shell command to the end of the list.
- Fluid app settings are saved per user and per machine.
+ \param[in] cmd a pointer to the command that we want to add
*/
-void show_shell_window() {
+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;
+