CLI
Sometimes you might want your app to have a Command Line Interface so that users can open files or even configure it. Despite how similar these two cases seem, they are implemented differently.
Before I get into them, let's create an example app: A file size viewer that accepts files from the command line and has 1-2 options.
require "gtk4"
label = "No open file"
app = Gtk::Application.new("dev.geopjr.filesizeviewer", Gio::ApplicationFlags::None)
app.activate_signal.connect do
window = Gtk::ApplicationWindow.new(app)
window.title = "File Size"
window.set_default_size(200, 200)
main_label = Gtk::Label.new(label)
main_label.wrap = true
window.child = main_label
window.present
end
exit(app.run)
Files
Now lets make it open a file from command line and calculate its size in kb.
We are going to use the Gio::Application#open_signal
. This signal emits when we provide a file to our app without emitting the #activate one.
INFO
While it's possible to get the file path from ARGV, it's recommended to use the open_signal as it returns a list of Gio::File
& supports xdg-portals, allowing you to access files outside of sandboxed environments like flatpak.
With that said, all we have to do is let GTK know that we want to handle the open signal, connect to the signal, grab the first file from the list, query its stats and set the label to that:
app = Gtk::Application.new("dev.geopjr.filesizeviewer", Gio::ApplicationFlags::HandlesOpen) # <--
app.open_signal.connect do |files, hint|
file = files[0]
fileinfo = file.query_info("standard::size", :none, nil)
filesize_h = fileinfo.size / 1000
label = "File: \"#{file.basename}\" is #{filesize_h} kb"
# We need to manually emit the activate signal
app.activate
nil
end
Running crystal run src/app.cr -- ./shard.yml
or ./app.cr ./shard.yml
will result in:
Arguments
We now want to add some arg options:
-m, --megabyte
Whether to use megabytes instead of kilobytes
-l label, --label=LABEL
Default label if no file is set
For that we are going to use Crystal's OptionParser
:
require "gtk4"
require "option_parser"
label = "No open file"
megabyte = false
OptionParser.parse do |parser|
parser.banner = "Usage: file-size-cr [arguments] [file]"
parser.on("-m", "--megabyte", "Whether to use megabytes instead of kilobytes") { megabyte = true }
parser.on("-l label", "--label=LABEL", "Default label if no file is set") { |t_label| label = t_label }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
parser.invalid_option do |flag|
STDERR.puts "ERROR: #{flag} is not a valid option."
STDERR.puts parser
exit(1)
end
end
then we are going to update the open_signal so it correctly handles the -m
flag:
app.open_signal.connect do |files, hint|
file = files[0]
fileinfo = file.query_info("standard::size", :none, nil)
filesize_h = fileinfo.size / 1000
filesize_h = filesize_h / 1000 if megabyte
label = "File: \"#{file.basename}\" is #{filesize_h} #{megabyte ? "m" : "k"}b"
app.activate
nil
end
and last but certainly not least, we are going to remove all the flags but the files from ARGV and then pass it to out app. GTK will refuse to proceed on unknown flag but we still want it to handle files for the open_signal:
clean_argv = [PROGRAM_NAME].concat(ARGV.reject { |x| x.starts_with?('-') })
exit(app.run(clean_argv))
Running crystal run src/app.cr -- -m ./shard.yml
or ./app.cr -m ./shard.yml
will result in:
Running crystal run src/app.cr -- -l "You forgot to mention a file"
or ./app.cr -l "You forgot to mention a file"
will result in:
Final result
require "gtk4"
require "option_parser"
label = "No open file"
megabyte = false
OptionParser.parse do |parser|
parser.banner = "Usage: file-size-cr [arguments] [file]"
parser.on("-m", "--megabyte", "Whether to use megabytes instead of kilobytes") { megabyte = true }
parser.on("-l label", "--label=LABEL", "Default label if no file is set") { |t_label| label = t_label }
parser.on("-h", "--help", "Show this help") do
puts parser
exit
end
parser.invalid_option do |flag|
STDERR.puts "ERROR: #{flag} is not a valid option."
STDERR.puts parser
exit(1)
end
end
app = Gtk::Application.new("dev.geopjr.filesizeviewer", Gio::ApplicationFlags::HandlesOpen)
app.activate_signal.connect do
window = Gtk::ApplicationWindow.new(app)
window.title = "File Size"
window.set_default_size(200, 200)
main_label = Gtk::Label.new(label)
main_label.wrap = true
window.child = main_label
window.present
end
app.open_signal.connect do |files, hint|
file = files[0]
fileinfo = file.query_info("standard::size", :none, nil);
filesize_h = fileinfo.size / 1000
filesize_h = filesize_h / 1000 if megabyte
label = "File: \"#{file.basename}\" is #{filesize_h} #{megabyte ? "m" : "k"}b"
app.activate
nil
end
clean_argv = [PROGRAM_NAME].concat(ARGV.reject { |x| x.starts_with?('-') })
exit(app.run(clean_argv))