Andy Maleh
Posted on March 26, 2022
Brandon Weaver has recently contacted me on the Glimmer Gitter to ask questions about Glimmer DSL for LibUI. He also mentioned the node pattern tool written by Marc-André Lafortune (a fellow Rubyist I know in Montreal), which is hosted on Heroku. Brandon said he was excited about the possibility of implementing something similar in pure Ruby using Glimmer DSL for LibUI by leveraging the rouge syntax highlighting gem. He has even blogged about the Ruby Tooling subject in the past with the title "Future of Ruby - AST Tooling", which Matz (creator of Ruby) has alluded to before.
Now comes the good news! The latest versions of Glimmer DSL for LibUI added support for the following features to facilitate pursuing the Ruby Tooling vision:
- Class-Based Custom Controls: enable building custom controls (aka widgets) as reusable components that are maintained cleanly in separate classes.
- Class-Based Custom Windows/Applications: facilitate building applications and reusable custom windows with less boilerplate code.
-
code_area
Custom Control: renders syntax highlighted code in anarea
control.
Given the great productivity benefits of Glimmer DSL for LibUI, I was able to piece together the code_area
custom control using the rouge gem in less than an hour.
Here is how the code_area
control looks like:
code_area
opens the doors to so many Ruby Tooling exciting possibilities in Glimmer DSL for LibUI. This is the Ruby community members' chance to be first movers and build Ruby tooling libraries that are scriptable in Ruby (unlike current popular editors).
Here is the examples/basic_code_area.rb code:
# From: https://github.com/AndyObtiva/glimmer-dsl-libui#basic-code-area
require 'glimmer-dsl-libui'
class BasicCodeArea
include Glimmer::LibUI::Application
before_body do
@code = <<~CODE
# Greets target with greeting
def greet(greeting: 'Hello', target: 'World')
puts "\#{greeting}, \#{target}!"
end
greet
greet(target: 'Robert')
greet(greeting: 'Aloha')
greet(greeting: 'Aloha', target: 'Nancy')
greet(greeting: 'Howdy', target: 'Doodle')
CODE
end
body {
window('Basic Code Area', 400, 300) {
margined true
code_area(language: 'ruby', code: @code)
}
}
end
BasicCodeArea.launch
Here is the code_area
implementation code (not very long, eh!):
# From: https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/lib/glimmer/libui/custom_control/code_area.rb
require 'glimmer/libui/custom_control'
module Glimmer
module LibUI
module CustomControl
class CodeArea
class << self
def languages
require 'rouge'
Rouge::Lexer.all.map {|lexer| lexer.tag}.sort
end
def lexers
require 'rouge'
Rouge::Lexer.all.sort_by(&:title)
end
end
include Glimmer::LibUI::CustomControl
REGEX_COLOR_HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/
option :language, default: 'ruby'
option :theme, default: 'glimmer'
option :code
body {
area {
rectangle(0, 0, 8000, 8000) {
fill :white
}
text {
default_font family: OS.mac? ? 'Consolas' : 'Courier', size: 13, weight: :medium, italic: :normal, stretch: :normal
syntax_highlighting(code).each do |token|
style_data = Rouge::Theme.find(theme).new.style_for(token[:token_type])
string(token[:token_text]) {
color style_data[:fg] || :black
background style_data[:bg] || :white
}
end
}
}
}
def lexer
require 'rouge'
require 'glimmer-dsl-libui/ext/rouge/theme/glimmer'
# TODO Try to use Rouge::Lexer.find_fancy('guess', code) in the future to guess the language or otherwise detect it from file extension
@lexer ||= Rouge::Lexer.find_fancy(language)
@lexer ||= Rouge::Lexer.find_fancy('ruby') # default to Ruby if no lexer is found
end
def syntax_highlighting(text)
return [] if text.to_s.strip.empty?
@syntax_highlighting ||= {}
unless @syntax_highlighting.keys.include?(text)
lex = lexer.lex(text).to_a
text_size = 0
@syntax_highlighting[text] = lex.map do |pair|
{token_type: pair.first, token_text: pair.last}
end.each do |hash|
hash[:token_index] = text_size
text_size += hash[:token_text].size
end
end
@syntax_highlighting[text]
end
end
end
end
end
Here is how to build class-based custom controls (address_form
and address_view
):
# From: https://github.com/AndyObtiva/glimmer-dsl-libui#class-based-custom-controls
require 'glimmer-dsl-libui'
require 'facets'
Address = Struct.new(:street, :p_o_box, :city, :state, :zip_code)
class FormField
include Glimmer::LibUI::CustomControl
options :model, :attribute
body {
entry { |e|
label attribute.to_s.underscore.split('_').map(&:capitalize).join(' ')
text <=> [model, attribute]
}
}
end
class AddressForm
include Glimmer::LibUI::CustomControl
options :address
body {
form {
form_field(model: address, attribute: :street)
form_field(model: address, attribute: :p_o_box)
form_field(model: address, attribute: :city)
form_field(model: address, attribute: :state)
form_field(model: address, attribute: :zip_code)
}
}
end
class LabelPair
include Glimmer::LibUI::CustomControl
options :model, :attribute, :value
body {
horizontal_box {
label(attribute.to_s.underscore.split('_').map(&:capitalize).join(' '))
label(value.to_s) {
text <= [model, attribute]
}
}
}
end
class AddressView
include Glimmer::LibUI::CustomControl
options :address
body {
vertical_box {
address.each_pair do |attribute, value|
label_pair(model: address, attribute: attribute, value: value)
end
}
}
end
class ClassBasedCustomControls
include Glimmer::LibUI::Application # alias: Glimmer::LibUI::CustomWindow
before_body do
@address1 = Address.new('123 Main St', '23923', 'Denver', 'Colorado', '80014')
@address2 = Address.new('2038 Park Ave', '83272', 'Boston', 'Massachusetts', '02101')
end
body {
window('Class-Based Custom Keyword') {
margined true
horizontal_box {
vertical_box {
label('Address 1') {
stretchy false
}
address_form(address: @address1)
horizontal_separator {
stretchy false
}
label('Address 1 (Saved)') {
stretchy false
}
address_view(address: @address1)
}
vertical_separator {
stretchy false
}
vertical_box {
label('Address 2') {
stretchy false
}
address_form(address: @address2)
horizontal_separator {
stretchy false
}
label('Address 2 (Saved)') {
stretchy false
}
address_view(address: @address2)
}
}
}
}
end
ClassBasedCustomControls.launch
And, here is how reusable custom controls look like (two address forms and two address views):
Happy Glimmering!
Posted on March 26, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.