mirror of
https://github.com/fltk/fltk.git
synced 2026-08-18 08:58:25 +08:00
1029 lines
31 KiB
C++
1029 lines
31 KiB
C++
//
|
||
// Shell Command database code for the Fast Light Tool Kit (FLTK).
|
||
//
|
||
// Copyright 1998-2026 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
|
||
//
|
||
|
||
// 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.
|
||
//
|
||
// 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.
|
||
|
||
// 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: add a bunch of sensible sample shell commands, add to 'import' pulldown menu
|
||
// 1: Build generated app
|
||
// Runs the project’s normal build command after saving the .fl file and
|
||
// generated sources. This is the baseline action and should work with
|
||
// the user’s preferred toolchain, for example make, ninja, or a wrapper
|
||
// script in the project directory.
|
||
// 2: Run generated app
|
||
// Launches the compiled executable immediately after a successful build.
|
||
// This is the fastest edit-build-run loop and is probably the most-used
|
||
// action in practice.
|
||
// 3: Build and run
|
||
// 4: Open project in IDE
|
||
// 5: Reveal generated files in Explorer/Finder
|
||
// 6: Diff generated code against working tree
|
||
// TODO: make g_shell_config static, not a pointer, but don't load anything in batch mode
|
||
|
||
// FEATURE: add the files ./fluid.prefs and ./fluid.user.prefs as tool locations
|
||
// FEATURE: interpret compiler output, for example: clang, and highlight errors and warnings
|
||
// `.../shell_command.cxx:71:2: error: test`
|
||
// `71 | #error test`
|
||
// `clang++: error: no such file or directory: '.../shell_command.o'`
|
||
// would make the error message clickable in the shell window and could select the widget,
|
||
// open the matching editor in the widget panel, and highlight the line in SourceView.
|
||
|
||
/*
|
||
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
|
||
|
||
powershell -c "$wshell = New-Object -ComObject wscript.shell; $wshell.SendKeys('^{ESCAPE}')
|
||
*/
|
||
|
||
#include "app/shell_command.h"
|
||
|
||
#include "Fluid.h"
|
||
#include "Project.h"
|
||
#include "message.h"
|
||
#include "io/file_chooser.h"
|
||
#include "io/Project_Reader.h"
|
||
#include "io/Project_Writer.h"
|
||
#include "nodes/Widget_Node.h"
|
||
#include "panels/settings_panel.h"
|
||
#include "widgets/App_Menu_Bar.h"
|
||
|
||
#include <FL/Fl_Double_Window.H>
|
||
#include <FL/fl_message.H>
|
||
#include <FL/fl_string_functions.h>
|
||
#include <FL/fl_utf8.h>
|
||
|
||
#include <errno.h>
|
||
|
||
#if defined(_WIN32) && !defined(__CYGWIN__)
|
||
# include <lmcons.h> // for UNLEN
|
||
#else
|
||
# include <pwd.h>
|
||
#endif
|
||
|
||
using namespace fluid;
|
||
|
||
static std::string fltk_config_cmd;
|
||
static Fl_Process s_proc;
|
||
|
||
/**
|
||
See if shell command is running (public)
|
||
*/
|
||
bool shell_command_running() {
|
||
return s_proc.desc() ? true : false;
|
||
}
|
||
|
||
/** \class Fl_Process
|
||
Launch an external shell command.
|
||
*/
|
||
|
||
/**
|
||
Create a process manager
|
||
*/
|
||
Fl_Process::Fl_Process() = default;
|
||
|
||
/**
|
||
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 nullptr;
|
||
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") !=nullptr);
|
||
|
||
// 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(nullptr, (LPTSTR) cmd,nullptr,nullptr,TRUE,
|
||
DETACHED_PROCESS,nullptr,nullptr, &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 = nullptr;
|
||
return 0;
|
||
}
|
||
return -1;
|
||
#else
|
||
int ret = ::pclose(_fpt);
|
||
_fpt=nullptr;
|
||
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 nullptr 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) : nullptr;
|
||
}
|
||
|
||
// 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 = nullptr;
|
||
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 nullptr; // 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()) {
|
||
fluid_alert("Previous shell command still running!");
|
||
return false;
|
||
}
|
||
if (flags & Fd_Shell_Command::SAVE_PROJECT) {
|
||
Fluid.proj.save();
|
||
}
|
||
if (flags & Fd_Shell_Command::SAVE_SOURCECODE) {
|
||
Fluid.write_code_files(true);
|
||
}
|
||
if (flags & Fd_Shell_Command::SAVE_STRINGS) {
|
||
Fluid.proj.write_strings();
|
||
}
|
||
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)) != nullptr) {
|
||
// 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(std::string &cmd, const std::string ¯o, const std::string &content) {
|
||
for (int i=0;;) {
|
||
i = (int)cmd.find(macro, i);
|
||
if (i==(int)std::string::npos) break;
|
||
cmd.replace(i, macro.size(), content);
|
||
}
|
||
}
|
||
|
||
static void expand_macros(std::string &cmd) {
|
||
expand_macro(cmd, "@BASENAME@", Fluid.proj.basename());
|
||
expand_macro(cmd, "@PROJECTFILE_PATH@", Fluid.proj.projectfile_path());
|
||
expand_macro(cmd, "@PROJECTFILE_NAME@", Fluid.proj.projectfile_name());
|
||
expand_macro(cmd, "@CODEFILE_PATH@", Fluid.proj.codefile_path());
|
||
expand_macro(cmd, "@CODEFILE_NAME@", Fluid.proj.codefile_name());
|
||
expand_macro(cmd, "@HEADERFILE_PATH@", Fluid.proj.headerfile_path());
|
||
expand_macro(cmd, "@HEADERFILE_NAME@", Fluid.proj.headerfile_name());
|
||
expand_macro(cmd, "@TEXTFILE_PATH@", Fluid.proj.stringsfile_path());
|
||
expand_macro(cmd, "@TEXTFILE_NAME@", Fluid.proj.stringsfile_name());
|
||
if (cmd.find("@TMPDIR@") != std::string::npos)
|
||
expand_macro(cmd, "@TMPDIR@", Fluid.get_tmpdir());
|
||
}
|
||
|
||
/**
|
||
Show the terminal window where it was last positioned.
|
||
*/
|
||
void show_terminal_window() {
|
||
Fl_Preferences pos(Fluid.preferences, "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();
|
||
}
|
||
|
||
/**
|
||
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 std::string &cmd, int flags) {
|
||
if (cmd.empty()) {
|
||
fluid_alert("No shell command entered!");
|
||
return;
|
||
}
|
||
|
||
if (!prepare_shell_command(flags)) return;
|
||
|
||
std::string expanded_cmd = cmd;
|
||
expand_macros(expanded_cmd);
|
||
|
||
if ( ((flags & Fd_Shell_Command::DONT_SHOW_TERMINAL) == 0)
|
||
&& (!shell_run_window->visible()))
|
||
{
|
||
show_terminal_window();
|
||
}
|
||
|
||
// Show the output window and clear things...
|
||
if (flags & Fd_Shell_Command::CLEAR_TERMINAL)
|
||
shell_run_terminal->printf("\033[2J\033[H");
|
||
if (flags & Fd_Shell_Command::CLEAR_HISTORY)
|
||
shell_run_terminal->printf("\033[3J");
|
||
shell_run_terminal->scrollbar->value(0);
|
||
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()) == nullptr) {
|
||
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(fluid::Tool_Store::USER),
|
||
condition(0),
|
||
flags(0),
|
||
shell_menu_item_(nullptr)
|
||
{
|
||
}
|
||
|
||
/**
|
||
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_(nullptr)
|
||
{
|
||
}
|
||
|
||
/**
|
||
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 std::string &in_name)
|
||
: name(in_name),
|
||
label(in_name),
|
||
shortcut(0),
|
||
storage(fluid::Tool_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_(nullptr)
|
||
{
|
||
}
|
||
|
||
/**
|
||
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 std::string &in_name,
|
||
const std::string &in_label,
|
||
Fl_Shortcut in_shortcut,
|
||
fluid::Tool_Store in_storage,
|
||
int in_condition,
|
||
const std::string &in_condition_data,
|
||
const std::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_(nullptr)
|
||
{
|
||
}
|
||
|
||
/**
|
||
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 nullptr
|
||
const char *new_label = label.c_str(); // never nullptr
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
Get the name of the user that is currently running FLUID.
|
||
|
||
\return the current user name, or an empty string if it can't be determined
|
||
*/
|
||
static std::string get_current_user_name() {
|
||
#if defined(_WIN32) && !defined(__CYGWIN__)
|
||
// Use the wide-character API and convert to UTF-8 so that user names
|
||
// containing international characters are handled correctly.
|
||
wchar_t wbuf[UNLEN + 1];
|
||
DWORD wlen = UNLEN + 1;
|
||
if (!GetUserNameW(wbuf, &wlen))
|
||
return std::string();
|
||
char buf[(UNLEN + 1) * 4]; // worst case 4 bytes per UTF-16 code unit
|
||
unsigned n = fl_utf8fromwc(buf, sizeof(buf), wbuf, (unsigned)wlen - 1);
|
||
return std::string(buf, n);
|
||
#else
|
||
struct passwd pwd, *result = nullptr;
|
||
char buf[16384];
|
||
if (getpwuid_r(geteuid(), &pwd, buf, sizeof(buf), &result) == 0 && result)
|
||
return std::string(pwd.pw_name);
|
||
return std::string();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
Get the name of the host that is currently running FLUID.
|
||
|
||
\return the current host name, or an empty string if it can't be determined
|
||
*/
|
||
static std::string get_current_host_name() {
|
||
#if defined(_WIN32) && !defined(__CYGWIN__)
|
||
// Use the wide-character API and convert to UTF-8 so that host names
|
||
// containing international characters are handled correctly.
|
||
wchar_t wbuf[MAX_COMPUTERNAME_LENGTH + 1];
|
||
DWORD wlen = MAX_COMPUTERNAME_LENGTH + 1;
|
||
if (!GetComputerNameW(wbuf, &wlen))
|
||
return std::string();
|
||
char buf[(MAX_COMPUTERNAME_LENGTH + 1) * 4]; // worst case 4 bytes per UTF-16 code unit
|
||
unsigned n = fl_utf8fromwc(buf, sizeof(buf), wbuf, (unsigned)wlen);
|
||
return std::string(buf, n);
|
||
#else
|
||
char buf[256];
|
||
if (gethostname(buf, sizeof(buf)) == 0) {
|
||
buf[sizeof(buf) - 1] = '\0';
|
||
return std::string(buf);
|
||
}
|
||
return std::string();
|
||
#endif
|
||
}
|
||
|
||
/**
|
||
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: {
|
||
std::string user = get_current_user_name();
|
||
return !user.empty() && (user == condition_data);
|
||
}
|
||
case HOST_ONLY: {
|
||
std::string host = get_current_host_name();
|
||
return !host.empty() && (host == condition_data);
|
||
}
|
||
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 = (fluid::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 fluid::io::Project_Reader *in) {
|
||
const char *c = in->read_word(1);
|
||
if (strcmp(c, "{")!=0) return; // expecting start of group
|
||
storage = fluid::Tool_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 fluid::io::Project_Writer *out) {
|
||
out->write_string("\n command {");
|
||
out->write_string("\n name "); out->write_word(name);
|
||
out->write_string("\n label "); out->write_word(label);
|
||
if (shortcut) out->write_string("\n shortcut %u", 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);
|
||
}
|
||
if (!command.empty()) {
|
||
out->write_string("\n command "); out->write_word(command);
|
||
}
|
||
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() = default;
|
||
|
||
/**
|
||
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 = nullptr;
|
||
}
|
||
}
|
||
|
||
/**
|
||
remove all shell commands of the given storage location from the list.
|
||
*/
|
||
void Fd_Shell_Command_List::clear(fluid::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, fluid::Tool_Store storage) {
|
||
// import the old shell commands from previous user settings
|
||
if (&Fluid.preferences == &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 = fluid::Tool_Store::USER;
|
||
cmd->name = "Sample Shell Command";
|
||
cmd->label = "Sample Shell Command";
|
||
cmd->shortcut = FL_ALT+'g';
|
||
Fluid.preferences.get("shell_command", cmd->command, "echo \"Sample Shell Command\"");
|
||
Fluid.preferences.get("shell_savefl", save_fl, 1);
|
||
Fluid.preferences.get("shell_writecode", save_code, 1);
|
||
Fluid.preferences.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 = fluid::Tool_Store::USER;
|
||
cmd->read(cmd_prefs);
|
||
add(cmd);
|
||
}
|
||
}
|
||
|
||
/**
|
||
Write shell configuration to a preferences group.
|
||
*/
|
||
void Fd_Shell_Command_List::write(Fl_Preferences &prefs, fluid::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 == fluid::Tool_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(fluid::io::Project_Reader *in) {
|
||
const char *c = in->read_word(1);
|
||
if (strcmp(c, "{")!=0) return; // expecting start of group
|
||
clear(fluid::Tool_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(fluid::io::Project_Writer *out) {
|
||
int n_in_project_file = 0;
|
||
for (int i=0; i<list_size; i++) {
|
||
if (list[i]->storage == fluid::Tool_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 == fluid::Tool_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 = nullptr;
|
||
if (!shell_submenu)
|
||
shell_submenu = (Fl_Menu_Item*)Fluid.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 },
|
||
{ nullptr }
|
||
};
|
||
|
||
/**
|
||
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 nullptr. 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;
|
||
|
||
std::string filename = fluid::io::filechooser(
|
||
fluid::io::FileChooserType::SAVE_FILE,
|
||
fluid::io::FileChooserPath::ABSOLUTE_PATH,
|
||
"Export Shell Commands",
|
||
"Can't create shell commands file:\n%s.",
|
||
Fluid.proj.projectfile_path() + Fluid.proj.basename() + ".flcmd",
|
||
Fluid.proj.projectfile_path(),
|
||
"Fluid Shell Commands\t*.flcmd"
|
||
);
|
||
if (filename.empty()) return;
|
||
|
||
Fl_Preferences file(filename.c_str(), "flcmd.fluid.fltk.org", nullptr, (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 nullptr. 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;
|
||
|
||
std::string filename = fluid::io::filechooser(
|
||
fluid::io::FileChooserType::LOAD_FILE,
|
||
fluid::io::FileChooserPath::ABSOLUTE_PATH,
|
||
"Import Shell Commands",
|
||
"Can't open shell commands file:\n%s.",
|
||
Fluid.proj.projectfile_path() + Fluid.proj.basename() + ".flcmd",
|
||
Fluid.proj.projectfile_path(),
|
||
"Fluid Shell Commands\t*.flcmd"
|
||
);
|
||
if (filename.empty())
|
||
return;
|
||
|
||
Fl_Preferences file(filename.c_str(), "flcmd.fluid.fltk.org", nullptr, 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 = fluid::Tool_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 = nullptr;
|
||
|