FreeplaneJrubyInstaller Addon
Contents
This page is still a work in progress
Hi you found this page before its finished.
My idea is to finish it and **only then** make an announcement in the forum, and ask to add links to it in the addons wiki page.
This is to avoid misunderstandings and be able to explain the addon and how to use it with some good explanations.
So, if you got here congratulations, but keep the secret! The addon is already in github, so if you cant wait anylonger knock yourself out :)
PS: This is progressing slow but steadly, at the rythm of my very scarce free time... dont worry though, all the coding is already finished and the addon ready, the last missing piece is to make some extense docs here in this wiki to explain how it works...
Introduction
This addon adds support for .rb (ruby) scripts in Freeplane
Freeplane supports .groovy and .js (javascript) scripts out-of-the-box. This addon adds support for .rb (ruby) scripts, enabling you to run .rb scripts (ex: xxx/1.5.x/scripts/my_ruby_script.rb
alongside the out-of-the-box .groovy and .js scripts.
All this is possible because Freeplane was programmed with good design (applause to the devs :) ) that allows external scripting engines to be added, such as the Jruby interpreter that is capable of running ruby scripts with access to Java objects (Freeplane-java-objects!).
How to install the FreeplaneJrubyInstaller Addon in Freeplane
You dont need to manually download anything, all downloads are automatic.
To install the addon follow these instructions bellow (click on any image to enlarge it):
- Check that your Java version is >= 1.7 (Jruby requires it). If you want to update your Java, goto https://java.com/en/download/
- Copy to your clipboard (without any spaces!):
THE-URL-TO-THE-ADDON: https://raw.githubusercontent.com/zipizap/FreeplaneJrubyInstaller/master/addon_lab/FreeplaneJrubyInstaller-v1.6.2.addon.mm
- Follow the images:
- Open Freeplane for a second-time, and:
- Open Freeplane for a third-time, and:
- The following final window should appear, which indicates that Jruby is now installed in Freeplane. Hurray, the addon is now installed!
Youtube demo video: install FreeplaneJrubyInstaller Addon
The making of the FreeplaneJrubyInstaller addon - a very long story for curious minds
For me this addon is a personal give-back to the open freeplane community, in return for creating and maintaining freeplane, which I enjoy so much for so long :)
I first started making this addon as an experiment, but later realized that I could actually *make* the addon and it could also be usefull to others in the freeplane community. So I took it as a long-time-geek-hobby, which slowly evolved at the rythm of my scarce free time.
For them all, the addon should facilitate:
- easy install of jruby in freeplane
- easy programming of ruby scripts for freeplane, for newbie-programmers that want to try scripting in freeplane
- easy usage of ruby scripts in freeplane, for the casual users
While making the addon, there where a lot of decisions taken, and small details choosen by me. I want to explain them, in case they are usefull for other programmers to understand why things were made this way, and hope that someday someone improves the addon by implementing other better decisions. I did not knew where to start or how to explain all this, so I just started writting the "making of" the addon.
Reading this section is completely optional, the technical details are very resumed and a bit oriented for experienced programmers. All interesting information about how to *use* the addon is resumed in other previous sections.
If you are a programmer (or a curious mind :) ) who wants to follow the history of how is was made and works internally, keep reading. If not, just skip it, its ok :)
Great, you decided to keep reading - hope you enjoy. Tried to lay it out as clear as possible :) Grab a beer or a coffee/tea and bucle your seatbelt - its a long story and sometimes we'll move fast :)
I love freeplane. Since like 5 years now I use it everyday to organize my work and personal notes.
At first, the simple tree structure with some background colors was enough to be usefull, but with time I discovered that the multiple options of freeplane could fit my needs to better structure knowledge. No doubt a big part of freeplane-usefullness is how people use it.
So, someday I gave some attention to this scripts thing that freeplane had - basically you could write some code that would use the Freeplane API to programatically make freeplane do things. The code was in the Groovy language which is a kind of java-with-some-sugar-that-looks-a-bit-like-ruby, and I did not know groovy nor java, but ruby was (is) my prefered language and I feel very comfortable programming in ruby. So although with no knowledge of Groovy, I could slightly understand the groovy scripts, and managed to make a couple of simple scripts, basically from copy/pasting/modifying from examples of the scripts collection page and got my homemade very-simple-groovy-scripts going on. No big groovy knowledge there, I just did the basic modify-try-and-error cycle which many times is enough for simple things.
Time passed, and sometimes, very rarely, I would notice that my personal usage of freeplane, could be boostered with some little scripty-automation. This lead me to get bolder in making Groovy scripts, and soon enough I got to the point that I had to learn/understand a little more about Groovy to make my scripts more complex - the copy/paste/modify magic was falling short to what I wanted to program.
As it turns out, for another project, I passed the last 6 months of scarse freetime learning C++ from a "renowned" book, and out of boredoom and curiosity I casually took a Java book to my one-week-vacations (instead of C++), and in one week of late-nigh-readings I ended up feeling more capable with Java than I felt with 6 months of C++ reading. And that was the tipping point to drop the study of C++ in favour of Java and use Jruby (Jruby = Java + Ruby) for that other project. The book was Head First Java, and it made a very funny reading :) Take it to your vacations :)
So, luck took me to learning a bit of Java, and I knew that Groovy was like java-with-some-sugar-that-looks-a-bit-like-ruby, so I started to improve my groovy scripts with the new java understanding. And it worked :) To a certain point. I managed to make the more complex scripts that I wanted with Java and Groovy. Still it costed me an enourmous efforth, as I could only do it in scarce free time, which meant sometimes severall weeks between code-session of a few hours, and that was my only practice of java and groovy. It was working, but carried along a steep beginner-overhead for me, far from efficient/productive, and only worth for high-priority-not-very-simple-but-not-too-complex scripts. Yes, a step ahead, but not the last step.
Jruby: Freeplane can also run ruby scripts with Jruby!
On the other hand, I did read in the wiki that it is possible to use other programming languages for scripting (scala, jython, etc) through the JSR 223. With some try-and-error I got jruby to work with freeplane (very simple after all!), and got all excited - using jruby I could write scripts for freeplane in the ruby language (instead of groovy/java). So, I started to think about making an addon to easily enable ruby scripts with freeplane. And that motivation slowly lead to the creation of the FreeplaneJrubyInstaller addon.
My first intention was for the addon to simply install Jruby in Freeplane to enable ruby scripts, and just that.
I had to learn how to make a freeplane addon (which after you read it well, its easy!). Then I noticed that to make freeplane aware of jruby it was just to put jruby-xxx.jar in 'freeplane-user-dir/lib/jruby-xxx.jar', but the jruby-xxx.jar file is +20MBytes in size, which is too much to be packaged inside an addon.mm file (that would give memory problems, both when building the addon myself, and later when others tried to install the addon - a no go), and the alternative was to make a java-downloader-window, that would be executed after the addon was installed in freeplane, and that would simply download the jruby-xxx.jar file from internet. That was not easy but got it done (the right way!) :) Also, I left the default download url pointing to jruby-9.0.4 (the jruby version I use and know it works!) but that can be easily changed in the future to download more recent versions of jruby (if/when they are needed! A prior jruby-9.0.something.jar did not work because of a bug!). And so with the addon installed (and jruby post-downloaded), one could write a freeplane-ruby-script (ex: 'freeplane-user-dir/scripts/my_ruby_script.rb') and then restart freeplane and click in the new menu entry Tools/Scripts/my_ruby_script to run it. In Preferences/Plugins some options had to be enabled (Permit file read operations + Script Classpath = "lib")
So, at this point the addon could install jruby in freeplane. With jruby installed, freeplane could already run ruby scripts.
Also, I've seen that it was then possible for other future addons to use ruby instead of groovy addon-scripts inside the addon (via the addon.mm#scripts node, just like its now possible with .groovy scripts)
A good accomplishment, great!
Adding to ruby scripts the variables: c, node, ui, logger, textUtils, ...
However the ruby scripts did not had these handy variables defined - c, node, ui, logger, textUtils, ... - they had to be manually extracted from java classes... that's possible but it's annoying to do it in every single script. In groovy scripts these variables just work and that's great, so in ruby scripts the same should happen. That was possible to do using som ruby metaprogramming.
The code is in the file freeplane.rb (with comments!), you can find it in freeplane_user_dir/addons/freeplaneJrubyInstaller/FreeplaneJrubyCommonEnvironment/lib/freeplane.rb. This file is automatically required by freeplane_jruby_common_environment.rb, so in your freeplane-ruby-script you only need to require "freeplane_jruby_common_environment.rb" to have these variables availabe for use in *any* scope (like global variables).
Resuming
You need to have these freeplane preferences enabled, in Tools/Preferences/Plugins |
After, I realized that ruby scripts may at somepoint need to use rubygems. Rubygems by default use a default system-wide directory to store all the gems, for all ruby programs (freeplane and non-freeplane). I did not wanted to use that systemwide-default-rubygems-folder, I wanted to use an isolated and self-contained rubygems-folder, where only the gems necessary for the freeplane-ruby-scripts could be stored and read from.
I've investigated about how to use only a local-rubygems-folder... First I started with bundler but it turned out to be an unexpected dead-end for this, and finally concluded that the best solution was to (avoid bundler and) define the environment variables GEM_HOME and GEM_PATH (no dependencies, clean-small code, easily understood) to indicate to rubygems that it should only load gems from a specific local gem-home folder. The result of using GEM_HOME and GEM_PATH is that the freeplane-ruby-scripts only see and only use the gems present in this local gem-home folder. Great, gems containment and isolation achieved :)
I've decided to place the local gem-home folder in freeplane_user_dir/addons/freeplaneJrubyInstaller/FreeplaneJrubyCommonEnvironment/gem_home. The code that makes this local gem-home ready to use in scripts, is in the file 'only_gem_home.rb' file, you can find it in 'freeplane_user_dir/addons/freeplaneJrubyInstaller/FreeplaneJrubyCommonEnvironment/lib/only_gem_home.rb'. This file is automatically required by freeplane_jruby_common_environment.rb, so in your freeplane-ruby-script you only need to require "freeplane_jruby_common_environment.rb" to only use gems from this local gem-home.
Resuming
|
NOTE: If you want to manage (list/install/uninstall/...) gems to/from the local gem_home then have a look and edit the files freeplane_user_dir/addons/freeplaneJrubyInstaller/FreeplaneJrubyCommonEnvironment/gem.bat or gem.sh
NOTE2: If you are making an addon with ruby-scripts that needs more gems, then instead of adding gems into the common .../FreeplaneJrubyCommonEnvironment/gem_home of all freeplane-ruby-script, its better for you to package your own independent addon_gem_home folder, with a modified only_gem_home.rb pointing to the addon_gem_home folder. This is easier and garantees that your addon be deployed and executed with exactly the same gems that you want (instead of relying on the common .../FreeplaneJrubyCommonEnvironment/gem_home which is intended to serve as a common base for the non-addon freeplane-ruby-scripts, and may even be tinkered by curious users out there :) )
So, at this point the addon was capable of installing Jruby, and then freeplane-ruby-scripts could be run from menu Tools/Scripts/my_ruby_script The freeplane-ruby-script should have a require "freeplane_jruby_common_environment.rb" that defines the variables c, node, ui, ... and also enables the gems (installed in gem_home) that thereafter can be required normally by the script.
Great :)
RubyLiveDebugger
Well, with all that ready, the next step would be to write some freeplane ruby scripts! And, as it happens to everyone who programs, at some point you'll find an error and debug your code for that error... When an error is found during execution of your ruby script, a short error-message-window will show up in freeplane (which tipically will not show the line number of the error). On the other hand the error will also be logged into the freeplane log file (freeplane_user_dir/logs/log.0), where you may find the line number of the error among other usefull info such as the ruby exception raised. So keep in mind that the log file will have more info about the error than what is shown in the small error-message-window that pops up.
And there you go reading the log file for the line number of your error, then opening the ruby script and rechecking that line and trying to mentally recompute and fing the error. And thats ok, but there is a better way availabe in ruby - there is this gem called Pry that is used normally to debug a program live while it is still executing (its a REPL launched inside a running program). Pry is very usefull, you just put a line of code in the middle of your program where you want, and then run your program and when the program execution gets to that line it waits for you to interact with it: read variables, change variables, call methods, etc, it's live coding inside your program in that point. Have a look a some youtube videos to understand it better, or wait 5 min to read this to the end and then it try yourself, its pretty easy to use, and we will use it with freeplane!
However Pry expects the program to be launched from a terminal and reuses the terminal's stdin/stdout to interact with the user, but we are running a ruby program with jruby which is called from java and the program stdin/stdout is who-knows-where... :) the stdout at least seems to be logged into the log files, but stdin is out-of-reach. For these cases (and other similar that happen in the ruby universe, like daemon processes), there is another gem called Pry-remote which uses tcp/ip to communicate between a pry-remote-server listening on 127.0.0.1:9876, to which its possible to connect with a pry-remote-client. The pry-remote-server is attached to the ruby program, and waits a connection from the pry-remote-client. The pry-remote-client is executed from another terminal, and will connect to the pry-remote-server via Tcp/Ip and use the client-terminal stdin/stdout to allow the user to interact with the ruby program (attached to pry-remote-server). So, using pry + pry-remote, we can debug a freeplane-ruby-script in the middle of its execution, from an external terminal that is running the pry-remote-client, and inspect the running freeplane-ruby-script to see its variables value and try new commands.
Using pry + pry-remote in this way implies severall things:
- 1) have the freeplane-ruby-script execute a pry-remote command (in the line where the debug will occur). This will start the pry-remote-server which will stay listening and waiting for a tcp connection in 127.0.0.1:9876
- 2) in a separate terminal window, launch the pry-remote-client (using java + jruby + the pry-remote gem)
- 3) finally once pry-remote-client is openned and connected, the user can debug the freeplane-ruby-script using that terminal (typing in keyboard, reading in screen). Tab is very usefull (autocompletion!)
As can be seen, manually launching up a pry-remote debug session is a bit daunting... and repeating it every time we want to debug something is not pleasant and a distraction...
So I automated it programatically so that a script-developer would not need to think about all this, but just call a special command inside the freeplane-ruby-script where one wants to open a debug window: RubyLiveDebugger.open_debug_here(binding)
You can put the RubyLiveDebugger.open_debug_here(binding) in multiple lines - when the program is being executed, each time one of those lines is reached a debug window will open for you to debug the ruby script in that point of execution.
The RubyLiveDebugger class is defined in file ruby_live_debugger.rb located in freeplane_user_dir/addons/freeplaneJrubyInstaller/FreeplaneJrubyCommonEnvironment/lib/ruby_live_debugger.rb This file is automatically required by freeplane_jruby_common_environment.rb, so in your freeplane-ruby-script you only need to require "freeplane_jruby_common_environment.rb" to have available the RubyLiveDebugger.open_debug_here(binding) command
The RubyLiveDebugger is only usefull for freeplane-ruby-scripts developers - it's a kind-of development tool (a debug mode!), and so its not usefull/necessary to all other freeplane-users that just want to run/use a ruby-script.
Inside the pry-remote debug-session, you can execute normal ruby code and also use the pry-commands
The following pry-commands are more than enough for basic debugging - you really only need the "!!@":
* up/down keys : to navigate in past commands (command history) * !!@ : to exit from debug-session * _ (underscore) : the result of last evaluated expression (usually the result from previous line) * ls MyObject : list the methods of MyObject (see also "ls -h") * help : to list all the other pry-commands
To exit from a debug session, its mandatory to send the command !!@ - only exit with !!@ and never with 'exit' or 'quit' or closing the debug window directly. This is necesary for the pry-remote-server process to cleanup and properly release/continue the execution of the freeplane-ruby-script past the debug line. (the worst it can happen is to freeze freeplane indefinitely untill you restart it manually)
Resuming
Certain Preferences need to be enabled for a script to be able to use the RubyLiveDebugger: |
If you are really curious about other pry-commands, knock yourself out:
- this linux-journal article gives an overview of pry
- this demo video shows off pry in 8min (from its creator)
- the pry-wiki is where there is lots of more info... (lots)
And now, at this point the addon was capable of installing Jruby, and then freeplane-ruby-scripts could be run from menu Tools/Scripts/my_ruby_script The freeplane-ruby-script should have a require "freeplane_jruby_common_environment.rb" that defines the variables c, node, ui, ... and also enables the gems (installed in gem_home) that thereafter can be required normally by the script. Also that require (together with certain adjusted Preferences) also enables the debug command RubyLiveDebugger.open_debug_here(binding) that can be put in any place of a script to open a debug window when that script is executed.
Terrific :)
Dir structure after addon installed
Well, having all those previous parts tested and working (jruby, script-variables c,node,ui,... , self-contained ruby-gems, RubyLiveDebugger ) I then focused on packaging the addon, so that after installing the addon it would be easy to use by ruby scripts and curious programmers.
When the addon is installed, it takes this directory structure in the freeplane-user-dir (other files and directories not relevant to the addon where omited from the diagram)
There are a couple of things represented in this diagram, and we'll go one by one, so just follow me
- The
require "freeplane_jruby_common_environment.rb"
will call a chain of requires: [a] -> [b] -> [c] -> [d1]+[d2]+[d3] that together will enable to the ruby_sample_script.rb to use the "gem_home", omnipresent-global-methods, and RubyLiveDebugger.open_debug_here(binding). So a freeplane-ruby-script only needs torequire "freeplane_jruby_common_environment.rb"
to get all the benefits from that chain of requirements.
- All freeplane-ruby-scripts share one common rubygems local folder: "gem_home" [e0]. If you want/need to install more gems, have a look and edit [e1] or [e2] as needed. However, take care to dont uninstall existing gems because they may be missed by other freeplane-ruby-scripts that need them. If you screw up, just uninstall/reinstall the FreeplaneJrubyInstaller and "gem_home" will be back to normal
- When packaging an addon, you use addon-scripts in groovy. However, I've tested and checked that *if you already have Jruby installed in Freeplane*, then you can also execute addon-script in ruby. So in the future you could install this FreeplaneJrubyInstaller (to install jruby in freeplane) and after install another addon which uses addon-scripts in ruby. This can be usefull if you prefer to program addon-scripts in ruby instead of groovy (like I do :) ).
Now let me tell you about a funny thing I did in the FreeplaneJrubyInstaller to make it first execute an addon-script in groovy (before jruby was installed) and later (after jruby was installed) make it execute an addon-script in ruby: I made the addon-scripts in groovy [f1],[f2] which will be executed first and that contain code to check if jruby is installed, and, if jruby is not installed it executes another groovy script; if jruby is installed it executes another ruby script. The code for this [f1],[f2] was difficult, made in a hurry, but works. The interesting parts is the code that detects the presence of jruby, and the code that calls the execution of another groovy/javacript/ruby scripts.
Finally the addon is made
Hurray, so lets stop talking about it and start using it !!!!!
Resuming A freeplane-ruby-script should:
require "freeplane_jruby_common_environment.rb" # The 'freeplane_jruby_environment.rb' file enables the following: # # [1] From now on, you can require the gems installed in "gem_home" (and only those) # # [2] Enables these omnipresent-global-methods: node, c, ui, logger, htmlUtils, textUtils, menuUtils, config, invokeLater # # [3] Enables RubyLiveDebugger.open_debug_here(binding) And this sums up what you need to know to go ahead with freeplane-ruby-scripts. |
____
...
TODO: continue here...