Friday, January 27, 2023

Glimmer Ecosystem, Glimte 3rd Party Framework, PasswordStore

Traditionally, Glimmer GUI gems have been mostly a one-sided effort, with a few 3rd party contributions here and there (like Drag and Drop support for Glimmer DSL for Tk). Well, that changes with Glimte! Glimte is a 3rd party framework built on top of Glimmer (Tk flavor) by Phaengris to facilitate following a certain variation of the MVC pattern (Model-View-Controller) called MVVM (Model-View-ViewModel). It encourages a model of programming for desktop GUI views that is similar to Rails .erb and GTK .glade files, but using the Glimmer GUI DSL in .glimmer.rb files as the view format. As such, it provides a much lighter more programmer-friendly Ruby-native replacement for HTML, ERB, and Glade. Consequently, Glimte ushers in the next era of Glimmer; that is The Glimmer Ecosystem! 

The Glimmer Ecosystem enables the democratization of the style of development followed when building Glimmer desktop applications in Ruby, among many other obvious benefits. That in turn facilitates a vision similar to the Rails vision explained by Yahuda Katz in his keynote speech at RailsConf 2014 (at which I presented too), which was borrowed from Steve Jobs. What Yahuda alluded to was that by continuously building more floors for the lower levels of a building in the form of a framework and a community of open-source projects, we enable developers to start development at higher and higher levels than they would have been able to otherwise, thus helping them leapfrog earlier ways of development in ever increasing productivity!

So, what is Glimte?

"MVVM framework based on Glimmer for creating desktop apps in Ruby / Tk" 

- Source: https://github.com/Phaengris/Glimte

Are there any apps built with Glimte?

PasswordStore - "Tk-based desktop client for the Linux password store manager https://www.passwordstore.org/"
 

What is pass, the Linux password manager that PasswordStore is built for?

"Password management should be simple and follow Unix philosophy. With pass, each password lives inside of a gpg encrypted file whose filename is the title of the website or resource that requires the password. These encrypted files may be organized into meaningful folder hierarchies, copied from computer to computer, and, in general, manipulated using standard command line file management utilities.

pass makes managing these individual password files extremely easy. All passwords live in ~/.password-store, and pass provides some nice commands for adding, editing, generating, and retrieving passwords. It is a very short and simple shell script. It's capable of temporarily putting passwords on your clipboard and tracking password changes using git." 


Below is a full introduction to Glimte, taken straight from the GitHub project page.

Happy Glimmering!


Glimte

MVVM framework based on Glimmer for creating desktop apps in Ruby / Tk

NOTE! Glimte is in the very beginning of it's way, features may not be stable, documentation may be incomplete.

References

Sample app

Structure of the app

  • app/
    • initializers/
    • views/
    • models/
    • assets/
  • lib/
  • dev/
    • scenarios/
    • tasks/
    • assets/
  • app.rb

app.rb

A typical app.rb may look like that

#!/usr/bin/env ruby

require 'glimte'

Glimte.run

Initializers

When Glimte.run is executed, these files are loaded before the main window is built and opened.

The initializers are sorted alphabetically before run. So you may use 00-initializer.rb, 01-initializer.rb, ... to define execution order.

An initializer example

app/initializers/tk.rb

# Load a Tk theme
Tk.tk_call('source', Glimte.asset_path('tk/azure/azure.tcl'))
Tk.tk_call('set_theme', 'dark')

# Define some Tk style
Tk::Tile::Style.configure('Alert.TLabel', { "foreground" => "#FF3860" })

Views

Views are described in Glimmer's declarative DSL (see references above).

Each view should be put inside app/views/<view name>.glimmer.rb (the glimmer.rb extension is mandatory for a view).

app/views/main_window.glimmer.rb

This view must always exist, it describes the content of the Glimmer's root element

title 'My pretty simple app'

Views.shared_components.toolbar {
  grid row: 0
}

frame {
  grid row: 1, row_weight: 1
        
  label {
    text 'Hello world'
  }
  button {
    text 'Click me already'
  }
  
  Views.shared_components.statusbar {
    grid row: 2
  }
}

Calling views

You can use Glimmer's widget keywords as well as references to Glimte views. A Glimte view can be called as Views.<path_to_view>.<view_name> and basically shares same principles as Glimmer's keywords.

button {
  text 'Click me!'
}

Views.special_button {
  text 'Or me!'
}

Views.funny_components.really_special_button {
  text 'Or even me!'
}

Views.special_button refers to app/views/special_button.glimmer.rb

Views.funny_components.really_special_button refers to app/views/funny_components/really_special_button.glimmer.rb

Views.special_button != Views.special_button - every time when you refer to a component, a new instance is created.

(In practice you will probably want to use views for more complex things that just implement a specific button :) In fact to customize a button you'll probably use some Tk style. An input field + a button + an error message entry = more like use case for a view.)

A special case is Views.MainWindow - it is the main window instance, the Glimmer's root component. You always can refer to the main window as Views.MainWindow. You can't call View.main_window to create a new main window instance.

View types - main window, windows, frames

Views.MainWindow is an instance of Glimmer::Tk::RootProxy

Views.<component_name>_window is an instance of Glimmer::Tk::ToplevelProxy

(note - when you call a *_window component, it is created inside the root element, not inside the current element)

Views.<component_name> is an instance of Glimmer::Tk::FrameProxy

So you can treat views as Glimmer's widgets, define grid for them etc

In subdirectories

It is a Glimte convention that subdirectories refer to components united by same designation. So it's recommended to name your directories like <parent component or namespace>_components.

  • app/views/
    • main_window.glimmer.rb
    • main_window_components/
      • available_entities_list.glimmer.rb
      • entity_view.glimmer.rb
      • entity_view_components/
        • entity_delete_confirmation.glimmer.rb
    • shared_components/
      • toolbar.glimmer.rb
      • statusbar.glimmer.rb

Views.MainWindow
Views.main_window_components.available_entities_list
Views.main_window_components.entity_view
Views.main_window_components.entity_view_components.entity_delete_confirmation Views.shared_components.toolbar
Views.shared_components.statusbar

Includes

app/views/complex_widget.glimmer.rb

data_record_name
data_record_options
data_record_text_notes

app/views/complex_widget_components/_data_record_name.glimmer.rb

entry {
  # ...
}
label {
  text <= [complex_widget.errors, :name]
  visible <= [complex_widget.errors, :name, '<=': -> (v) { !!v }]
}

app/views/complex_widget_components/_data_record_options.glimmer.rb

# ...

app/views/complex_widget_components/_data_record_text_notes.glimmer.rb

# ...

Includes (or partials) are not views. They don't have an own container, an own view model. In fact they're just pieces of code included into the view's code. You can refer to the view's view model inside them directly (as on the example above).

View models

app/views/say_something.glimmer.rb

entry {
  variable <=> [say_something, :message]
}
button {
  text 'Say!'
  enabled <= [say_something, :message, '<=': -> (v) { !!v }]
  on('command') do
    say_something.said
  end
}
label {
  visible <= [say_something, :response]
  text <= [say_something, :response]
}

app/views/say_something.rb

class ViewModels::SaySomething
  attr_accessor :message,
                :response
  
  def said
    self.response = "Thanks for saying \"#{message}\"!"
  end
end

Thanks to the Shine syntax (Glimmer's interface for dynamic data binding)

your view models are just usual Ruby objects.

You only need to define attributes (attr_accessor) for the view to observe and react to.

If you want to make the view model to respond to change of an attribute, you may override the corresponding <attribute>= method.

Calling the view model from the view

app/views/some_tricky_component.glimmer.rb

button {
  on('command') do
    some_tricky_component.do_your_job!
  end
}

# is equal to

button {
  on('command') do
    view_model.do_your_job!
  end
}

In subdirectories

app/views/pretty_components/bells_and_whistles.glimmer.rb app/views/pretty_components/bells_and_whistles.rb

class ViewModels::PrettyComponents::BellsAndWhistles
  # ...
end

Initializing view model

class ViewModels::SaySomething
  attr_accessor :message
end
Views.say_something {
  # let's propose to the user something to say by default
  message 'Ehm... hello?'
}

Glimmer's widget properties are the priority. If you define attr_accessor :grid in your view model, calling grid still will be handled by Glimmer's grid method, not by yours grid= setter.

Forms

Forms support is very basic at the moment. Still there are some tools available.

app/views/form.rb

class ViewModels::Form
  attr_accessor :a, :b,
                :errors, :changes

  def initialize
    self.errors = Glimte::ViewModelErrors.new(:a, :b)
    
    # ... read initial values of a and b from somewhere
    
    self.changes = Glimte::ViewModelChanges(self, :a, :b)
  end
  
  def do_something
    return unless changes.a? || changes.b?
    # ...
    if we_failed?
      errors[:a] = 'Because of the value of a is invalid'
      # and / or
      errors[:b] = 'Because of the value of B is invalid'
    end
  end
end

app/views/form.glimmer.rb

entry {
  variable <=> [form, :a]
}
label {
  visible <= [form.errors, :a]
  text <= [form.errors, :a]
}

# ... and probably some widgets for B ...

button {
  text 'Do something with A and B'
  on('command') do
    form.do_something
  end
}

If you use Dry::Validation https://github.com/dry-rb/dry-validation

def do_something
  values = { a: self.a, b: self.b }
  errors.call_contract(Contract, values)
  return if errors.any?
  # ...
end

class Contract < Dry::Validation::Contract
  # ...
end

Glimte's additions to Glimmer

Raising Tk events

Using the Glimmer's tk property of the current widget, you can easily raise a custom Tk event

tk.event_generate("<CustomEvent>", data: "SomeMeaningfulStringValue")

Glimte offers additional raise_event method to do it in a bit more convenient way

raise_event 'CustomEvent', 'SomeMeaningfulStringValue'

# or even

raise_event 'CustomEvent', payload: { a: 'b', c: 'd' }

In the latter case, the data argument is converted to a YAML string. Sorry, at the moment Glimte doesn't provide an automated YAML parsing in event handlers (it's a TODO for the future). So you may do it like that for now:

on('CustomEvent') do |event|
  data = YAML.load(event.data)
  # ...
end

Catching Tk events

Some additions to the Glimmer's on method

@another_component = Views.another_component 

on('CustomEvent', redirect_to: @another_component)

# or even

on 'Event1', 'Event2', 'Event3', redirect_to: @another_component
on('CustomEvent', stop_propagation: true) do
  # ...
end

# is equal to

on('CustomEvent') do
  # ...
  break false
end

Modal windows

Tk doesn't have native modal windows support. Glimte offers kind of workaround. When defining a toplevel component (a window), you can specify modal true property. This will do the following:

  • the parent window is hidden
  • the current window becomes centered inside the parent window box
  • when the current window is closed, the parent window is shown again

app/views/modal_window.glimmer.rb

title 'Modal window'
modal true

Local events

  • stay inside the view, won't bubble up to the parent widget
    (so you may not bother about name conflicts for common event names like "Action", "Cancel" etc)
  • are raised directly on the view level

Good for handling local "OK" or "Cancel" etc actions

frame {
  label {
    # describe what we do here
  }
  frame {
    # left panel with options
    # ...
    frame {
      button {
        on('command') do
          raise_event('Action', local: true)
        end
      }
      button {
        on('command') do
          raise_event('Cancel', local: true)
        end
      }
    }
  }
  frame {
    # right panel with result preview
  }
}

on('Action', local: true) do
  # perform action (or apply changes), close window
end
on('Cancel', local: true) do
  # do nothing (or discard changes), close window
end
raise_action
raise_cancel
on_action do
  # ...
end
on_cancel do
  # ...
end

# are equal to

raise_event 'Action', local: true
raise_event 'Cancel', local: true
on('Action', local: true) do
  # ...
end
on('Cancel', local: true) do
  # ...
end

Some helper methods

  • close_window closes the closest window (root or toplevel). Can be called from any widget.
  • closest_view returns the closest parent view (the current view if called on the view's level). Can be called from any widget.
  • closest_window returns the closest parent window (root or toplevel) (the current window if called on the window's level). Can be called from any widget.
  • visible / hidden pair of properties
  • enabled / disabled pair of properties

No comments: