Instantly share code, notes, and snippets.
Last active
November 11, 2020 04:32
-
Star
1
(1)
You must be signed in to star a gist -
Fork
1
(1)
You must be signed in to fork a gist
-
Save estum/8d91605966e393adb0d64ec4e426cb37 to your computer and use it in GitHub Desktop.
Menu utility for LES (Live Enhancement Suite)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# frozen_string_literal: true | |
# = Lesmenu: Menu utility for Live Enhancement Suite (https://enhancementsuite.me) | |
# | |
# The utility creates a YAML-formatted copy of LES's `menuconfig.ini`, | |
# which is (IMHO) a better format for a human-readable and -editable structure. | |
# | |
# First, you should generate '~/.hammerspoon/menuconfig.yml': | |
# | |
# $ lesmenu yaml -e # converts & opens editor | |
# | |
# Than, edit menu & save it back to INI: | |
# | |
# $ lesmenu ini # writes back to menuconfig.ini | |
# $ lesmenu ini - # outputs to stdout without writing the file | |
# | |
# You also can inspect generated structure in one of 3 forms: yaml, ruby hash or struct | |
# | |
# $ lesmenu inspect yaml --as struct | |
# | |
# Commands: | |
# lesmenu ini [TARGET] # Convert YML back to menuconfig.ini | |
# lesmenu yaml [TARGET] # Convert menuconfig.ini to yaml | |
# lesmenu inspect [SUBCOMMAND] | |
# lesmenu version # Print version | |
require "bundler/inline" | |
gemfile do | |
source 'https://rubygems.org' | |
gem 'dry-cli' | |
gem 'dry-struct' | |
gem 'dry-types' | |
gem 'awesome_print' | |
end | |
require "yaml" | |
module Lesmenu | |
Types = Module.new { include Dry.Types } | |
SEP = "--" | |
SUB_END = ".." | |
SUB_CHR = ?/ | |
SEP_ATTRS = { label: SEP }.freeze | |
INI_TAIL = [ | |
"; --", | |
";V DONT REMOVE THIS OR THE PROGRAM WILL NOT WORK V", | |
"; Readme", | |
"End" | |
] | |
class MenuWalker | |
attr_reader :items | |
def initialize | |
@items = [] | |
@_path = [] | |
end | |
def call(line) | |
case line.chr | |
when "/"; submenu(line) | |
when "."; exit_submenu | |
when "-"; current_items << SEP_ATTRS | |
when ""; close_item if @_item | |
when /[\w\"\']/; item(line) | |
end | |
end | |
def current_items | |
@_path.size > 0 ? @items.dig(*current_path) : @items | |
end | |
def current_path | |
@_path.size > 0 ? [*@_path, :items] : @_path | |
end | |
def current_depth | |
@_path.size > 0 ? @items.dig(*@_path, :depth) : 0 | |
end | |
def submenu(line) | |
depth = line[/^\/+/].size | |
sub = { depth: depth, label: line[depth..-1], items: []} | |
exit_submenu if depth == current_depth | |
current_items << sub | |
@_path = [*current_path, current_items.index(sub)] | |
end | |
def item(line) | |
return @_item = { label: line } unless @_item | |
if line[0] == ?" && line[-1] == ?" | |
line = line[1...-1] | |
end | |
@_item[:query] = line | |
close_item | |
end | |
def close_item | |
@_item[:query] ||= @_item[:label] | |
current_items << @_item | |
@_item = nil | |
end | |
def exit_submenu | |
@_path.pop(2) | |
end | |
end | |
YAMLWalker = -> (item, depth: 0) { | |
case item | |
when SEP | |
SEP_ATTRS | |
when String | |
{ label: item, query: item } | |
when Hash | |
item.flat_map do |label, sub| | |
case sub | |
when Array | |
depth = depth.next | |
{ label: label, items: sub.flat_map { |v| YAMLWalker[v, depth: depth] }, depth: depth } | |
when String | |
{ label: label, query: sub } | |
end | |
end[0] | |
else | |
item | |
end | |
} | |
class MenuConfig < Dry::Struct | |
MenuItem = Dry::Struct(label: Types::String) do | |
abstract | |
def encode_with(coder) | |
coder.represent_scalar(nil, label) | |
end | |
end | |
Separator = Class.new(MenuItem).attributes(label: Types::String.optional.default(SEP).constrained(eql: SEP)) | |
Item = Class.new(MenuItem).attributes(query: Types::String) | |
Submenu = Class.new(MenuItem).attributes(depth: Types::Integer.optional.default(0)) | |
ItemsList = Types.Array(Types.Constructor(Separator) | Types.Constructor(Item) | Types.Constructor(Submenu)) | |
class Separator | |
def to_ini(*) | |
SEP | |
end | |
end | |
class Item | |
def encode_with(coder) | |
label == query ? super : coder.represent_map(nil, label => query) | |
end | |
def to_ini(*) | |
[label, query].uniq.join("\n") | |
end | |
end | |
class Submenu | |
attribute :items, ItemsList.default {[]} | |
def to_ini(depth: 0) | |
depth = depth.next | |
lines = ["#{SUB_CHR * depth}#{label}", *items.flat_map { |v| v.to_ini(depth: depth) }] | |
lines << SUB_END unless lines[-1] == SUB_END | |
lines | |
end | |
def encode_with(coder) | |
coder.represent_map(nil, label => items) | |
end | |
end | |
Sep = Separator.new | |
attribute :items, ItemsList | |
def self.load_ini(source) | |
walker = MenuWalker.new | |
File.open(source, "r") { |io| io.each_line(chomp: true, &walker.method(:call).to_proc) } | |
new(items: walker.items) | |
end | |
def self.load_yaml(source) | |
unparsed = YAML.load(File.read(source)) | |
items = unparsed.map(&YAMLWalker) | |
new(items: items) | |
end | |
def encode_with(coder) | |
coder.represent_seq nil, [*items] | |
end | |
def to_ini | |
lines = items.map(&:to_ini).flatten | |
while lines[-1] == SUB_END | |
lines.pop | |
end | |
lines.concat(INI_TAIL) | |
lines.join("\n\n") | |
end | |
end | |
module CLI | |
module Commands | |
extend Dry::CLI::Registry | |
class Version < Dry::CLI::Command | |
desc "Print version" | |
def call(*) | |
puts "1.0.0" | |
end | |
end | |
class ConvertCommand < Dry::CLI::Command | |
def call(source:, target:, **options) | |
source, target = expand_paths(source, target) | |
menu = load_menu_from(source) | |
File.write(target, serialize_menu(menu), mode: "w") | |
spawn_editor(target) if options[:edit] && target != "/dev/stdout" | |
end | |
private | |
def load_menu_from(source) | |
raise NotImplementedError | |
end | |
def serialize_menu(menu) | |
raise NotImplementedError | |
end | |
def spawn_editor(target) | |
editor = Shellwords.split(ENV['EDITOR'] || "/usr/bin/open -e") | |
cmd = Shellwords.join([*editor, target]) | |
warn "$ #{cmd}" | |
pid = spawn(cmd) | |
Process.detach(pid) | |
end | |
def expand_paths(source, target) | |
[File.expand_path(source), | |
(target == ?- ? "/dev/stdout" : File.expand_path(target))] | |
end | |
end | |
class ToYAML < ConvertCommand | |
desc "Convert menuconfig.ini to yaml" | |
argument :target, default: "~/.hammerspoon/menuconfig.yml", desc: "Path to menuconfig.yml or - to stdout" | |
option :source, aliases: %w(-i), default: "~/.hammerspoon/menuconfig.ini", desc: "Path to menuconfig.ini" | |
option :edit, type: :boolean, | |
default: false, | |
aliases: %w(-e), | |
desc: %(Convert and open .yml file in editor (respecting EDITOR environment variable)) | |
private | |
def load_menu_from(source) | |
MenuConfig.load_ini(source) | |
end | |
def serialize_menu(menu) | |
menu.to_yaml | |
end | |
end | |
class ToINI < Dry::CLI::Command | |
desc "Convert YML back to menuconfig.ini" | |
argument :target, default: "~/.hammerspoon/menuconfig.ini", desc: "Path to menuconfig.ini or - to stdout" | |
option :source, aliases: %w(-i), default: "~/.hammerspoon/menuconfig.yml", desc: "Path to menuconfig.yml" | |
option :edit, type: :boolean, | |
default: false, | |
aliases: %w(-e), | |
desc: %(Convert and open .yml file in editor (respecting EDITOR environment variable)) | |
def load_menu_from(source) | |
MenuConfig.load_yaml(source) | |
end | |
def serialize_menu(menu) | |
menu.to_ini | |
end | |
end | |
register "version", Version, aliases: %w(v -v --version) | |
register "yaml", ToYAML, aliases: %w(yml y -y) | |
register "ini", ToINI, aliases: %w(i -i) | |
module Inspect | |
class INI < Dry::CLI::Command | |
desc "Inspect unparsed menuconfig.ini" | |
option :from, desc: "Path to menuconfig.ini", default: "~/.hammerspoon/menuconfig.ini" | |
option :struct, type: :boolean, default: false, desc: "Will generate struct?" | |
def call(from:, struct:, **) | |
menu = MenuConfig.load_ini(File.expand_path(from)) | |
if struct | |
ap menu, raw: true | |
else | |
ap ::YAML.load(menu.to_yaml), raw: true | |
end | |
end | |
end | |
class YAML < Dry::CLI::Command | |
DigArgs = Types::Params::Integer | Types::Params::Symbol | |
desc "Inspect unparsed menuconfig.yml" | |
option :from, default: "~/.hammerspoon/menuconfig.yml", desc: "Path to menuconfig.yml" | |
option :format, default: "yml", values: %w(yml yaml struct hash attrs), aliases: %w(-f --as), desc: "Output format" | |
def call(from:, format:, args: [], **) | |
yaml = ::YAML.load_file(File.expand_path(from)) | |
formatted = | |
case format | |
when "yaml", "yml" | |
yaml | |
when "hash", "attrs", "struct" | |
hashed = yaml.map(&YAMLWalker) | |
format == "struct" ? MenuConfig.new(items: hashed) : hashed | |
end | |
formatted = formatted.dig(*args.map(&DigArgs)) if args.size > 0 | |
ap formatted, raw: true | |
end | |
end | |
end | |
register "inspect", aliases: %w(p -p) do |prefix| | |
prefix.register "ini", Inspect::INI, aliases: %w(i -i) | |
prefix.register "yaml", Inspect::YAML, aliases: %w(yml y -y) | |
end | |
end | |
end | |
end | |
Dry::CLI.new(Lesmenu::CLI::Commands).call |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment