My objective is to create a simple frontend to pandoc. I have learned that execl is a good way of calling executables in the system.
Note in the following code the function btn_pressed, that calls pandoc using the mentioned method.
[indent=4]
uses
Posix
Gtk
class TestWindow:Window
_file_chooser:FileChooserButton
_entry:Gtk.Entry
_button:Gtk.Button
_file:File
construct()
title = "Pandoc GUI"
window_position = WindowPosition.CENTER
destroy.connect( Gtk.main_quit )
var folder_chooser = new FileChooserButton("Choose a Folder",FileChooserAction.SELECT_FOLDER)
folder_chooser.set_current_folder( Environment.get_home_dir() )
//I used selection_changed directly as per the question in stack_exchange
//http://stackoverflow.com/questions/34689763/the-signal-connect-syntax
folder_chooser.selection_changed.connect( folder_changed )
_file_chooser = new FileChooserButton("Choose a File",FileChooserAction.OPEN)
_file_chooser.set_current_folder( Environment.get_home_dir() )
_file_chooser.file_set.connect( file_changed )
_entry = new Gtk.Entry()
_entry.set_text("Here the file name")
_button = new Button.with_label("Convert to pdf")
_button.set_sensitive(false)
_button.clicked.connect(btn_pressed)
var box = new Box( Orientation.VERTICAL, 0 )
box.pack_start( folder_chooser, true, true, 0 )
box.pack_start( _file_chooser, true, true, 0 )
box.pack_start( _entry, true, true, 0 )
box.pack_start( _button, true, true, 0 )
add( box )
def folder_changed( folder_chooser_widget:FileChooser )
folder:string = folder_chooser_widget.get_uri()
_file_chooser.set_current_folder_uri( folder )
def file_changed ( file_chooser_widget: FileChooser )
_file = File.new_for_uri(file_chooser_widget.get_uri())
try
info:FileInfo = _file.query_info (FileAttribute.ACCESS_CAN_WRITE, FileQueryInfoFlags.NONE, null)
writable: bool = info.get_attribute_boolean (FileAttribute.ACCESS_CAN_WRITE)
if !writable
_entry.set_sensitive (false)
else
_button.set_sensitive (true)
except e: Error
print e.message
_entry.set_text(_file.get_basename())
def btn_pressed ()
var md_name=_entry.get_text()+".md -s -o "+_entry.get_text()+".pdf"
execl("/usr/bin/pandoc", md_name)
_button.set_sensitive (false)
init
Gtk.init( ref args )
var test = new TestWindow()
test.show_all()
Gtk.main()
At execution I get no response at all from my code, without any pdf being rendering as well.
I would use GLib.Subprocess
to call external commands because it provides better control over the input to and output from the external command. Changing the example below to execl
should be easy enough though.
The first thing is to de-couple your external command from your window object. This makes it more testable. To do this a separate object is used - a wrapper around the Subprocess
call. Save this code as ToPDF.gs
:
namespace FileConverters
class ToPDF
const _command:string = "pandoc"
def async convert( source:string, output:string )
try
var flags = SubprocessFlags.STDOUT_PIPE \
| SubprocessFlags.STDERR_PIPE
var subprocess = new Subprocess( flags,
_command,
source,
output
)
output_buffer:Bytes
yield subprocess.communicate_async( null,
null,
out output_buffer,
null
)
if ( subprocess.get_exit_status() == 0 )
debug( "command successful: \n %s",
(string)output_buffer.get_data()
)
else
debug( "command failed" )
except err:Error
debug( err.message )
The ToPDF
class is now de-coupled from the rest of your application. This means it can be re-used. To illustrate this an integration test is shown below that uses the class.
The ToPDF
also uses asynchronous code. So I will explain that first. Making a method asynchronous means it will run concurrently with the main thread of the application. By have a call to an external program run concurrently it means the main thread doesn't lock up while it is waiting for the external program to finish. Using async
means the function is split in two. The first part is called with convert.begin( source, output )
and will run up to the yield
command. At that point the execution of the program splits in two. The main thread will return to the caller of convert.begin
, but what has started in the background is the Subprocess
. When the Subprocess
finished it returns to convert
and finishes the method call.
Save the integration test as ToPDFTest.gs
:
uses FileConverters
init
var a = new ToPDF()
a.convert.begin( "source_file.md", "output_file.pdf" )
var loop = new MainLoop()
var loop_quitter = new LoopQuitter( loop )
Timeout.add_seconds( 2, loop_quitter.quit )
loop.run()
class LoopQuitter
_loop:MainLoop
construct( loop:MainLoop )
_loop = loop
def quit():bool
_loop.quit()
return false
Compile with valac --pkg gio-2.0 ToPDF.gs ToPDFTest.gs
Then run the test with G_MESSAGES_DEBUG=all ./ToPDFTest
The test uses MainLoop
, which is the base class of Gtk.Main
. To simulate a long running program a two second timeout is set then MainLoop.quit()
is called to end the test. Unfortunately MainLoop.quit()
doesn't have the right function signature for a Timeout
callback so a wrapper class, LoopQuitter
is used.
Integration tests like this are often kept and run before a software release to ensure the application is working with other software modules.
To integrate ToPDF
with your window you need to change
execl("/usr/bin/pandoc", md_name)
to something like
var to_pdf = new Fileconverts.ToPDF()
to_pdf.convert.begin( md_name, pdf_name )
You may also want to wrap it in a command pattern similar to Avoiding global variables in Genie . You may also want to modify it to provide better feedback to the user.