Version 4, last updated by arst at August 06, 2008 06:37 UTC
The NsisMode MiniAppMode
A NSIS mode based on ScriptedMiniAppMode
To implement the NSIS mode, we base our work on the Squirrel class ScriptedMiniAppMode_. This class is a Squirrel script version of the native class MiniAppModeIInterfaces. It enables creating scripted extension modes that are handled just as any other FWB extension.
The class has these methods:
- GetTypeName() – Returns the name of the mode.
- AcceptApp( type, app ) – Accepts and rejects MiniApps that it can extend.
- OnCommandId( app, id ) – Handles commands when triggered by the user.
- GetCommandEntry( ix ) – Describes the commands it handles.
- GetIdListInfo( ix_list ) – Define toolbars and/or menus for the mode.
- GetAccelerator( ix ) – Describes any keyboard accelerators for the mode.
- …more members can be found from a SqSession prompt by typing:
@sqs@@@> list ScriptedMiniAppMode@ - …or in the C++ header file mam/SqBindingsMAM.h (which defines the native side of this class).
So, we need to implement most of these to drive the NSIS compiler and provide a useful interface.
To have clickable errors when running the compiler, we also have to implement an extension to the generic command prompt class. The role of this extension is to scan the output from the compiler and color the error messages and react to user clicking on them.
A Nsis Mode Script File
We create a file NsisMode.nut in the directory <user-fwb>/scripts/squirrel (the users FWB directory [.i.e. /home/user/.FileWorkbench on Linux and something like C:\Documents and Settings\user\Application Data\FileWorkbench on Windows]).
We begin by making sure some other scripts have been loaded:
@ modload(“ScriptedMiniAppMode.nut”) @ @ modload(“CmdPromptExt.nut”) @ @ modload(“filename.nut”) @ @ modload(“fwb.nut”) @Strictly, we don’t need to do this, since the NsisMode.nut script is likely to be loaded quite late and the scripts on modload above are usually loaded early. (remember Squirrel don’t need prototypes, since all lookups happen while running the script).
Then it’s time to declare our class:
@class NsisMode extends ScriptedMiniAppMode { @ @ function constructor(){ @ @ println( “NsisMode::constructor” ) @ @ ::ScriptedMiniAppMode.constructor() @ @ } @ @ function GetTypeName( ){ return “Nsis Mode” } @The base class constructor has to be invoked manually and its important this happens. This will register our mode with the FWB main window manager (MiniAppMgrI) and make sure we get included when modes are associated with the apps.
We use a println( msg ) function to help debugging. The string output is sent to the default (embedded) script engine output, which can be made visible. Currently there is no Squirrel debugger in FWB, so these println can be quite helpful.
Then comes a function that simply returns the name of our mode (to identify us in menus and dialogs).
After this we can declare static data structures that represent our commands, toolbars and keyboard accelerators.
Commands and Icons
Using GIMP I created/borrowed some icons to be put on a toolbar. (Credits to FamFamFam which has a very useful icon collection. I copied some symbols from there.) The result:
| nsi-build.png |
| The build NSIS script tool |
| nsi-run.png |
| The tool for running a built NSIS script |
| folder_go.png_| !http://www.assembla.com/spaces/FileWorkbench/documents/c08BGEutqr3Btbab7jnrAJ/download/foldergo.png! | A tool for browsing the folder where the script is located |
| help.png |
| A help tool (to open the NSIS help file) |
Once created (as PNG images) the files were put in the icons sub-directory where FWB as installed. Then they can be loaded directly by FWB when needed.
We continue with some table declarations for the UI in NsisMode.nut:
@ // The commands that this MiniAppMode accepts @ @ static ms_cmd_ids = [ @ @ { id=4999 name=“Nsis Mode Toolbar” flags=::MiniAppModeI.maCMD_TOOLBAR help=“Nsis tools” }, @ @ { id=5100 name=“Build NSIS script” name_bm_enab=“nsi-build.png” help=“Compile this NSIS script” }, @ @ { id=5101 name=“Run installer” name_bm_enab=“nsi-run.png” help=“Run the installer” }, @ @ { id=5102 name=“Browse directory” name_bm_enab=“folder_go.png” help=“Browse NSIS script source folder” }, @ @ { id=5103 name=“NSIS Help” name_bm_enab=“help.png” help=“Open NSIS manual” }, @ @ ] @ @ @ @ // IDs on our toolbar @ @ static ms_list = {id=4999 name=“Nsis Toolbar” id_list=[5100,5101,5102,5103] } @The tables have an intuitive syntax, prefixing the member name with static ensures that only one single global table is made (not one per instance). Some things worth noticing above:
- Each command entry above can be used from: A – in a menu, B – on a toolbar or C – bound to a keyboard accelerator..
- The same id values can be reused with different meaning by different app types, they need not be globally unique. So, one can select an arbitrary range for the a MiniAppMode (4999…5103 above).
- The name field is what is shown when the command is added to a menu.
- The bm_enab member is the name of a bitmap defining a toolbar button or menu entry.
- The help member provides popup help for the command.
There are also other members that specify more details in a command, but if we don’t specify them, they are given default values.
Then we have the ms_list static member which describes a small toolbar. It contains the ID of the toolbar itself – 4999, its name and a list of IDs that are on the toolbar.
These command structures are communicated to the main app with two member functions:
@ function GetCommandEntry( ix ){ @ @ if( ix>=0 && ix<ms_cmd_ids.len() ) @ @ return ms_cmd_ids[ix] @ @ else @ @ return null @ @ } @ @ @ @ function GetIdListInfo( ix_list ){ @ @ if( ix_list==0 ) @ @ return ms_list @ @ else @ @ return null @ @ } @GetCommandEntry(ix) iterates through the commands while the index is still valid. GetIdListInfo(ix_list) returns our toolbar when ix_list is 0 and _null _ otherwise. With this data, the FWB main window can render our small user interface.
Accepting a MiniApps that have NSI Contents
NsisMode is really only interested in being connected with text editors that have an open *.nsi file. Otherwise the mode should not be connected and no toolbar shown. To accomplish this, we override the function AcceptApp(apptype, app):
@ function AcceptApp( apptype, app ){ @ @ // We want to accept only text editors with a *.nsi file name @ @ if( apptype.GetTypeName()==“Text Editor” ){ @ @ if( app ){ @ @ local ed = app.GetObj(“maTextEditor”) @ @ local file = ed.GetFile() @First we make sue that the type of the app is “Text Editor”. Then, the app itself needs to be asked for a more specialized interface maTextEditor. When we have that, we can ask it about its current file and extract the file extension.
@ local ext = GetFileExt(file) @ @ if( ext ){ @ @ ext = ext.tolower() @ @ if( ext==“.nsi” ){ @ @ println( "Accepting text editor: " + file ) @ @ return 1 @ @ } @It was an *.nsi file so we accept it and return 1.
@ } @ @ } @ @ else @ @ // No app, but we accept text editors in general @ @ return 1 @Above is the case when the argument app is null. In this case the question is rather if the mode has interest in the general app type or not. For “Text Editor” we do have this interest, otherwise not.
@ } @ @ println( "Rejecting app: "+name ) @ @ return 0 @ @ } @We can also note that a single NsisMode object can be used on multiple text editors at the same time.
Handling NSIS Commands
When one of our commands is triggered, we can catch it by overriding OnCommandId(ma, id). But first, we have a couple of member variables to setup (in the class body):
@ m_nsis_compiler = null @ @ @ @ // Regular expression to extract file name + line number @ @ static ms_re_out_file = PCRE\\s*\\\“(.*)\\\”\\s*") @The first variable is to store a string for the location of the NSIS compiler and the second one is a PCRE regular expression used for parsing the NSIS file (we scan the file to find the name of the generated installer executable).
Then, the OnCommandId function body:
@ function OnCommandId( ma, id ) { @ @ // Get the ma-editor object @ @ local ed = ma.GetObj( “maTextEditor” ) @ @ if( !ed ) return @ @ @ @ // Get the script name @ @ local file = ed.GetFile( ) @ @ // Split into path, filename and extension @ @ local pparts = ::GetFilePathParts(file) @ @ @ @ switch( id ){ @First we cast the app to a maTextEditor. Then we get the current file name and split it into parts (path, name, extension). After that we look at the command ID jump to the right code for the current ID using a switch statement.
The Build Command
@ case 5100: /Build/ @ @ // We must know the location of the compiler @ @ if( isempty(m_nsis_compiler) ){ @ @ local prog_path = vtree.GetVar( “win_prog_files” ) @ @ if( !isempty(prog_path) ) @ @ m_nsis_compiler = vtree.LocateFile( prog_path, "", “makensis.exe”, 3 ) @We check if we have located the NSIS compiler. If not, we ask the vtree instance (the central file operation class) for the path to the Program Files directory of Windows (“win_prog_files” is like an internal environment variable). Once we have that, we use the function vtree.LocateFile(…) which basically looks in a directory tree for a file with a given name. The last argument (3) says how many sub-directories down it should look (a full sub tree search on a 100 GB disk can take minutes to iterate so we want to avoid that). LocateFile also allows for specifying a wildcard for an intermediate path to be matched.
@ if( isempty(m_nsis_compiler) ){ @ @ prog_path = vtree.GetVar( “win_prog_files_common” ) @ @ m_nsis_compiler = vtree.LocateFile( prog_path, "", “makensis.exe”, 3 ) @ @ } @If not found, we also look in a common program files directory.
@ if( isempty(m_nsis_compiler) ){ @ @ ::MessageBox( “Cannot not locate ‘makensis.exe’ anywhere under \\nthe ‘Program files’ directory’”, @ @ “Cannot Find NSIS Compiler”, 0, ma ) @ @ return @ @ } @ @ } @And we handle the case where no compiler was found. Next we need to find or create a command prompt to run the NSIS compiler:
@ local prompt = GetRelatedAppCreateIfNeeded( @ @ ma, “prompt”, “Cmd Prompt”, @ @ AcceptOsPrompt ) @The function GetRelatedAppCreateIfNeeded(…) does quite a bit of work. It checks if ma is already associated with a prompt and if so, returns that one. Otherwise, it looks on the neighbor panes for a command prompt. If not found there, it creates a new one. AcceptOsPrompt() is a function which returns true only if its argument is an OS command prompt (and not a script shell).
@ if( !prompt ){ @ @ ::MessageBox( “Failed finding/creating a command prompt to run the NSIS compiler”, @ @ “No Command Prompt”, 0, ma ) @ @ return @ @ } @If it failed, report and return.
@ @ @ // Make sure prompt is visible @ @ ::ma_manager.Activate( prompt, false ) @This will make sure that our command prompt is made visible (if hidden by another MiniApp) but it does not receive keyboard focus. We’re almost ready to start giving commands to the prompt:
@ if( !(“__nsis_prompt_ext” in ::getroottable()) ) @ @ // Instantiate an output parser @ @ NsisPromptExt() @What we do here is to instantiate a command prompt extension for parsing NSIS compiler output, if one does not already exist. The NsisPromptExt class is defined later.
@ @ @ // Now command – Set the drive @ @ prompt.DoString( pparts.path.slice(0,2), 3 ) @ @ @ @ // Set directory @ @ prompt.DoString( "cd "+QuotePath(pparts.path), 3 ) @ @ @ @ // Run the compiler @ @ prompt.DoString( QuotePath(m_nsis_compiler) + " " + @ @ QuotePath(pparts.name + pparts.ext), 3 ) @Here we send three commands to the activated command prompt:
1 – We change the current drive (like typing C: in a DOS shell under Windows)
2 – We change to the directory where the NSIS script is located.
3 – We put together a command line that launches the NSIS compiler on our script.
The approach of splitting the path of the NSIS script into components (pparts) was useful, it keeps the code clear. The function QuotePath(path) does just that, it puts quotes around a path.
@ break @Done! Now we know that the compiler has started, a parser to parse its output is alive and this is all visible to the user.
The Run Command
Here the task is to locate the ready to run installer and launch it.
@ case 5101: /Run/ @ @ // We need to find the name of the ouput file. @ @ // Use a regular expression for that @ @ local text_ed = ma.GetObj(“TextEditor”) @ @ local out_file @ @ for( local ix=0; ix<text_ed.GetLineCount(); ix++ ){ @ @ local line = text_ed.GetLine(ix) @ @ if( line ){ @ @ if( ms_re_out_file.Match(line)>2 ){ @ @ out_file = ms_re_out_file2 @ @ break @ @ } @ @ } @ @ } @We ask ma for a TextEditor interface (which allow us for reading and writing the contents of an editor). Then we iterate the lines of the editor until we find one matching our regular expression for a line specifying the output executable file.
@ if( !out_file ){ @ @ ::MessageBox( “Failed locating ‘OutFile’ tag in script”, @ @ “Cannot Run Installer”, 0, ma ) @ @ return @ @ } @If not found, show a message and return.
@ // Absolute or relative path? @ @ if( IsPathRelative(out_file) ) @ @ out_file = pparts.path + “\\” + out_file @ @ //println( "Final out_file: "+out_file ) 2We add the path of the current NSIS script if the given output file is not absolute.
@ vtree.ShellOpenFile(out_file,vtree.Open,false) @ @ break @Finally we use vtree.ShellOpenFile(…) to launch the executable.
The Browse Command
Here we want to find a file browser which we want to point at the current NSIS script:
@ case 5102: /Browse/ @ @ // We need a file browser, see if we have one associated with us already. @ @ local browser = GetRelatedAppCreateIfNeeded( @ @ ma, “browser”, “File Browser”, null ) @We use GetRelatedAppCreateIfNeeded(…) again, this time looking for and optionally creating a file browser.
@ local mafb = browser ? browser.GetObj(“maFileBrowser”) : null @And we need to cast it to the type maFileBrowser.
@ if( mafb ){ @ @ // Path was split into parts @ @ mafb.SetDirectory( pparts.path ) @ @ mafb.SetCurrentFile(pparts.name+pparts.ext) @ @ // Activate the other browser app @ @ ::ma_manager.Activate( mafb, true ) @We found a suitable file browser and can now set its directory and current file. The full filename which was split into { path, name, extension } comes in handy here. Then we activate the browser and also make sure it has keyboard focus.
@ } @ @ else @ @ ::MessageBox( “Could not resolve NSI path”, “NSIS Browse Problem”, 0, ma ) @ @ @ @ break @If no browser was found, an error message. Done.
The NSIS Help Command
Here we want to launch the NSIS help file (“NSIS.chm”) and open it externally. If NSIS is installed somewhere under the Program Files directory, we can locate it using vtree.LocateFile(…):
@ case 5103: /* NSIS Help */ @ @ // Locate the NSI help file and open it @ @ local prog_path = vtree.GetVar( “win_prog_files” ) @ @ local nsis_help_file @ @ if( !isempty(prog_path) ) @ @ nsis_help_file = vtree.LocateFile( prog_path, "", “NSIS.chm”, 3 ) @Again, we used vtree.GetVar(name) to find the location of the Program files directory.
@ if( isempty(nsis_help_file) ){ @ @ ::MessageBox( “Cannot not locate ‘NSIS.chm’ anywhere under \\nthe ‘Program files’ directory’”, @ @ “Cannot Find NSIS Help file”, 0, ma ) @ @ return @ @ } @If not found, show a dialog and return.
@ // Now open it @ @ vtree.ShellOpenFile( nsis_help_file, 0, false ) @ @ break @We used vtree.ShellOpenFile(…) to open the help file in the Windows shell.
@ }@
@ }@
That closes the OnCommandId(…) function.
Instantiating a NsisMode Object
The NsisMode class is intended as a singleton. So we instantiate one immediately after the class declaration:
@ __nsis_mode <- NsisMode() @The constructor will register with the MiniAppManager automatically and connect with any open NSIS text files. If the script is run again, the old object is garbage collected and a fresh one created.
Each time we edit and rebuild the script (hit Ctrl-B in an FWB text editor), the updated version is merged into FWB. Editing and testing becomes a small closed loop.
The Output Parser
The NsisMode class is complete in itself, but if we also want clickable error messages, we have to implement one more class:
Goto the The NSIS Output Parser Page