pax_global_header00006660000000000000000000000064143237215220014513gustar00rootroot0000000000000052 comment=1673cd32b84e19f4ca81cc12cfee1c61c488b1f4 nom-0.1.5/000077500000000000000000000000001432372152200123075ustar00rootroot00000000000000nom-0.1.5/.gitignore000066400000000000000000000000061432372152200142730ustar00rootroot00000000000000*.gem nom-0.1.5/README.md000066400000000000000000000110001432372152200135560ustar00rootroot00000000000000# nom **nom** is a command line tool that helps you lose weight by tracking your energy intake and creating a negative feedback loop. It's inspired by John Walker's [The Hacker's Diet](https://www.fourmilab.ch/hackdiet/) and tries to automate things as much as possible. ## Installation You'll need Ruby, Rubygems and gnuplot. On Windows, make sure that gnuplot's binary directory is added to your `PATH` during installation. Then run this command: $ gem install nom When you run `nom` for the first time, it will ask for your current and your desired weight. ## Usage Call `nom` without arguments to get a summary of your current status: $ nom 5.3 kg down (34%), 10.3 kg to go! Today: (1774) (200) Griespudding (110) Graubrot (125) Käse (87) Orangensaft --------------------- (1252) remaining You ate/drank something? Look up at FDDB how much energy it contained. (The search is German only for now, sorry.) $ nom Mate Club Mate (Brauerei Loscher) (40) 1 Glas (200 ml) (66) 1 kleine Flasche (330 ml) (100) 1 Flasche (500 ml) Mate Tee, Figurfit (Bad Heilbrunner) (0) 1 Beutel (2 ml) (1) 100 g (100 ml) Mate Tee, Orange (Bad Heilbrunner) (2) 1 Glas (200 ml) (0) 15 Filterbeutel (1 ml) Mate Tee, Guarana (Bad Heilbrunner) (0) 1 Glas (200 ml) Club-Mate Cola (Brauerei Loscher) (99) 1 Flasche (330 ml) (60) 1 Glas (200 ml) Report your energy intake: $ nom Club-Mate 100 Enter your weight regularly: $ nom 78.2 And get nice graphs. The upper graph shows weight over time, with a weighted (no pun intended) moving average, a weight prediction, and a green finish line. The lower graph shows daily energy intake targets and actual intake: $ nom plot ![Graphs of weight and input over time](http://files.blinry.org/nom-0.1.0.svg) Enter `nom help` if you're lost: Available subcommands: status Display a short food log w, weight Report a weight measurement s, search Search for a food item in the web n, nom Report that you ate something y, yesterday Like nom, but for yesterday p, plot Plot a weight/intake graph l, log Display the full food log g, grep Search in the food log e, edit Edit the input file ew, editw Edit the weight file c, config Edit the config file (see below for options) help Print this help There are some useful defaults: (no arguments) status weight search nom Configuration options (put these in /home/seb/.nom/config): rate How much weight you want to lose per week (default: '0.5') goal Your target weight image_viewer Your preferred svg viewer, for example 'eog -f', 'firefox', 'chromium' (default: 'xdg-open') unit Your desired base unit in kcal (default: '1') start_date The first day that should be considered by nom [yyyy-mm-dd] balance_start The day from which on nom should keep track of a energy balance [yyyy-mm-dd] ## Conventions *nom* looks for its configuration directory in `${XDG_DATA_HOME}/nom`, `~/.local/share/nom`, or `~/.nom/` (in that order), and operates on three files in that configuration directory: * `config` contains configuration settings * `input` contains stuff you ate * `weight` contains weight measurements. The files are plain text, you can edit them by hand. By default, energy quantities will have the unit "kcal". You can change this by adding a line like `unit: 0.239` to your `config`, which means you want to use the unit "0.239 kcal" (= "1 kJ"). Energy quantities are displayed in parentheses: `(42)` Weight quantities are displayed as "kg", but you can use arbitrary units, like pounds. ## License: GPLv2+ *nom* is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. nom-0.1.5/bin/000077500000000000000000000000001432372152200130575ustar00rootroot00000000000000nom-0.1.5/bin/nom000077500000000000000000000044261432372152200136040ustar00rootroot00000000000000#!/usr/bin/env ruby require "date" require "nom/nom" commands = [ # format: [ long_form, short_form, arguments, description ] [ "status", nil, nil, "Display a short food log" ], [ "weight", "w", "", "Report a weight measurement" ], [ "search", "s", "", "Search for a food item in the web" ], [ "nom", "n", " ", "Report that you ate something" ], [ "yesterday", "y", " ", "Like nom, but for yesterday" ], [ "plot", "p", nil, "Plot a weight/intake graph" ], [ "log", "l", nil, "Display the full food log" ], [ "grep", "g", "", "Search in the food log" ], [ "edit", "e", nil, "Edit the input file" ], [ "editw", "ew", nil, "Edit the weight file" ], [ "config", "c", nil, "Edit the config file (see below for options)" ], [ "help", nil, nil, "Print this help" ], ] aliases = { "--help": "help", "-h": "help", } nom = Nom::Nom.new cmd_name = (ARGV.shift or "status") if aliases.include? cmd_name.to_sym cmd_name = aliases[cmd_name.to_sym] end command = commands.find{|c| c[0] == cmd_name or c[1] == cmd_name} if command.nil? ARGV.unshift(cmd_name) if ARGV.last.to_f != 0 if ARGV.size > 1 # some words followed by a number cmd_name = "nom" else # a single number cmd_name = "weight" end else # some words cmd_name = "search" end command = commands.find{|c| c[0] == cmd_name or c[1] == cmd_name} end if command[0] == "help" puts "Available subcommands:" commands.each do |c| puts " "+"#{c[1].to_s.rjust(2)}#{c[1] ? "," : " "} #{c[0]} #{c[2]}".ljust(32)+c[3] end puts "There are some useful defaults:" puts " "+"(no arguments)".ljust(28)+"status" puts " "+"".ljust(28)+"weight " puts " "+"".ljust(28)+"search " puts " "+" ".ljust(28)+"nom " nom.config_usage else begin if ARGV.empty? nom.send(command[0]) else nom.send(command[0], ARGV) end rescue Exception => e puts e.backtrace puts e.message puts "Something went wrong. Usage of this command is: nom #{command[0]} #{command[2]}" end end nom-0.1.5/lib/000077500000000000000000000000001432372152200130555ustar00rootroot00000000000000nom-0.1.5/lib/nom/000077500000000000000000000000001432372152200136465ustar00rootroot00000000000000nom-0.1.5/lib/nom/config.rb000066400000000000000000000046321432372152200154450ustar00rootroot00000000000000require "nom/helpers" module Nom class Config def initialize file @file = file @config = {} if File.exists? file @config = YAML.load_file(file, permitted_classes: [Date]) end @defaults = { # format: [ key, description, default_value, type ] "rate" => [ "how much weight you want to lose per week", 0.5, Float ], "goal" => [ "your target weight", nil, Float], "image_viewer" => [ "your preferred SVG viewer, for example 'eog -f', 'firefox', 'chromium'", Helpers::default_program, String ], "unit" => [ "your desired base unit in kcal", 1, Float ], "start_date" => [ "the first day that should be considered by nom [yyyy-mm-dd]", nil, Date ], "balance_start" => [ "the day from which on nom should keep track of a energy balance [yyyy-mm-dd]", nil, Date ], "balance_factor" => [ "how many money units you'll have to pay per energy unit", 0.01, Float ], } end def has key @config.has_key?(key) or (@defaults.has_key?(key) and not @defaults[key][1].nil?) end def get key v = nil if @config.has_key?(key) v = @config[key] elsif @defaults.has_key?(key) if @defaults[key][1].nil? print "Please enter #{@defaults[key][0]}: " @config[key] = STDIN.gets.chomp open(@file, "w") do |f| f << @config.to_yaml end v = @config[key] else v = @defaults[key][1] end else raise "Unknown configuration option '#{key}'" end if @defaults[key][2] == Float v.to_f elsif @defaults[key][2] == Date if v.class == Date v else Date.parse(v) end else v end end def print_usage puts "Configuration options (put these in #{@file}):" @defaults.each do |key, value| puts " #{key}".ljust(34)+value[0].capitalize+(value[1].nil? ? "" : " (default: '#{value[1]}')") end end end end nom-0.1.5/lib/nom/food_entry.rb000066400000000000000000000010701432372152200163410ustar00rootroot00000000000000module Nom class FoodEntry attr_reader :date, :kcal, :description def self.from_line line date, kcal, description = line.split(" ", 3) date = Date.parse(date) kcal = kcal.to_i description.chomp! FoodEntry.new(date, kcal, description) end def initialize date, kcal, description @date = date @kcal = kcal @description = description end def to_s "#{@date} #{@kcal} #{@description}\n" end end end nom-0.1.5/lib/nom/helpers.rb000066400000000000000000000023551432372152200156420ustar00rootroot00000000000000module Nom class Helpers def Helpers::open_file filename program = if filename =~ /\.svg$/ default_program else # let's assume it's a text file default_editor end if program.nil? raise "Couldn't find a program to open '#{filename}'. Please file a bug." end system("#{program} #{filename}") end def Helpers::default_program if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ "start" elsif RbConfig::CONFIG['host_os'] =~ /darwin/ "open" elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/ "xdg-open" else nil end end def Helpers::default_editor ENV["EDITOR"] || if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ "notepad" elsif RbConfig::CONFIG['host_os'] =~ /darwin/ "open -a TextEdit" elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/ "vi" else nil end end end end nom-0.1.5/lib/nom/nom.plt.erb000066400000000000000000000031021432372152200157230ustar00rootroot00000000000000set terminal svg size 1920,1080 font "Linux Biolinum,20" set output "<%= svg.path %>" set multiplot layout 2,1 set border 11 set xdata time set timefmt "%Y-%m-%d" set format x "%Y-%m" set grid set lmargin 6 set xrange [ "<%= @weights.first %>" : "<%= plot_end %>" ] set yrange [ <%= [goal-1, @weights.min].min.floor %> : <%= [goal+1, @weights.max].max.ceil %> ] set ytics 1 nomirror set mxtics 1 set xtics 2592000 nomirror set obj 1 rectangle behind from screen 0,0 to screen 1,1 set obj 1 fillstyle solid 1.0 fillcolor rgb "white" linewidth 0 set obj 2 rectangle behind from "<%= @weights.first %>",<%= goal-1 %> to "<%= plot_end %>",<%= goal+1 %> set obj 2 fillstyle solid 0.2 fillcolor rgb "green" linewidth 0 plot <%= goal %> t "Goal" lc rgb "forest-green" lw 2, \ "<%= weight_dat.path %>" using 1:2 w points t "Weight" pt 13 ps 0.3 lc rgb "navy", \ "<%= weight_dat.path %>" using 1:3 w l t "" lt 1 lw 2 lc rgb "navy", \ "<%= weight_dat.path %>" using 1:4 w points t "" lc rgb "navy" pt 7 ps 0.2 unset obj 1 set yrange [ 0 : <%= quantize((@weights.first..@weights.last).map{|d| [consumed_at(d), allowed_kcal(d, 0), allowed_kcal(d, rate)]}.flatten.max) %> ] set ytics <%= quantize(200) %> nomirror set key right bottom plot "<%= input_dat.path %>" using 1:2:(0):($4-$2) w vectors nohead lc rgb "navy" notitle, \ "<%= input_dat.path %>" using 1:2 w points pt 13 ps 0.3 lc rgb "navy" t "Consumed energy", \ "<%= input_dat.path %>" using 1:3 w lines lc rgb "black" t "0 kg/week", \ "<%= input_dat.path %>" using 1:4 w lines lc rgb "forest-green" lw 2 t "<%= rate %> kg/week" unset multiplot nom-0.1.5/lib/nom/nom.rb000066400000000000000000000326061432372152200147730ustar00rootroot00000000000000require "open-uri" require "fileutils" require "nokogiri" require "uri" require "tempfile" require "erb" require "nom/food_entry" require "nom/config" require "nom/weight_database" require "nom/helpers" module Nom class Nom def initialize xdg_data = (ENV["XDG_DATA_HOME"] or File.join(Dir.home, ".local", "share")) preferred_config_location = File.join(xdg_data, "nom") [ preferred_config_location, File.join(Dir.home,".nom") ].each do |dir| if Dir.exists? dir @nom_dir = dir break end end if not @nom_dir or not Dir.exists? @nom_dir @nom_dir = preferred_config_location puts "Creating #{@nom_dir}" Dir.mkdir(@nom_dir) end @config = Config.new(File.join(@nom_dir, "config")) @weights = WeightDatabase.new(File.join(@nom_dir, "weight")) @inputs = read_file("input", FoodEntry) date = truncate_date @inputs.delete_if{|i| i.date < date} @weights.truncate(date) if @weights.empty? print "Welcome to nom! Please enter your current weight: " weight [STDIN.gets.chomp] end @weights.interpolate_gaps! @weights.precompute_moving_average!(0.05, 0.05, goal, rate) @weights.predict_weights!(rate, goal, 30) @weights.precompute_moving_average!(0.05, 0.05, goal, rate) precompute_inputs_at precompute_base_rate_at end def status kg_lost = @weights.moving_average_at(@weights.first) - @weights.moving_average_at(@weights.last_real) print "#{kg_lost.round(1)} kg down" if kg_lost+kg_to_go > 0 print " (#{(100*kg_lost/(kg_lost+kg_to_go)).round}%)" end print ", #{kg_to_go.round(1)} kg to go!" print " You'll reach your goal in approximately #{format_duration(days_to_go)}." puts log_since([@weights.first,Date.today-1].max) end def log log_since(@weights.first) end def grep args term = args.join(" ") inputs = @inputs.select{|i| i.description =~ Regexp.new(term, Regexp::IGNORECASE)} if inputs.empty? puts "(no matching entries found)" end inputs.each do |i| entry(quantize(i.kcal), i.date.to_s+" "+i.description) end separator entry(quantize(inputs.inject(0){|sum, i| sum+i.kcal}), "total") end def weight args if @weights.real?(Date.today) raise "You already entered a weight for today. Use `nom editw` to modify it." end date = Date.today weight = args.pop.to_f open(File.join(@nom_dir,"weight"), "a") do |f| f << "#{date} #{weight}\n" end initialize plot end def nom args nom_entry args, (Time.now-5*60*60).to_date end def yesterday args nom_entry args, Date.today-1 end def search args puts "Previous log entries:" grep(args) term = args.join(" ") puts term = term.encode("ISO-8859-1") url = "https://fddb.info/db/de/suche/?udd=0&cat=site-de&search=#{CGI.escape(term)}" page = Nokogiri::HTML(URI.open(url)) results = page.css(".standardcontent a").map{|a| a["href"]}.select{|href| href.include? "lebensmittel"} results[0..4].each do |result| page = Nokogiri::HTML(URI.open("https://fddb.info"+result)) title = page.css(".breadcrumb a").last.text brand = page.css(".standardcontent p a").select{|a| a["href"].include? "hersteller"}.first.text puts "#{title} (#{brand})" page.css(".serva").each do |serving| size = serving.css("a.servb").text kcal = serving.css("div")[5].css("div")[1].text.to_i #kj = serving.css("div")[2].css("div")[1].text.to_i puts " (#{quantize(kcal,1)}) #{size}" end end end def plot raise "To use this subcommand, please install 'gnuplot'." unless which("gnuplot") weight_dat = Tempfile.new("weight") (@weights.first).upto(plot_end) do |date| weight_dat << "#{date}\t" if @weights.real?(date) weight_dat << "#{@weights.at(date)}" else weight_dat << "-" end if date <= @weights.last_real weight_dat << "\t#{@weights.moving_average_at(date)}\t" else weight_dat << "\t-" end if date >= @weights.last_real weight_dat << "\t#{@weights.moving_average_at(date)}\n" else weight_dat << "\t-\n" end end weight_dat.close input_dat = Tempfile.new("input") input_dat << "#{@weights.first-1}\t0\t0\n" (@weights.first).upto(Date.today) do |date| input_dat << "#{date}\t" if consumed_at(date) == 0 input_dat << "-" else input_dat << quantize(consumed_at(date)) end input_dat << "\t#{quantize(allowed_kcal(date, 0))}" input_dat << "\t#{quantize(allowed_kcal(date))}" input_dat << "\n" end input_dat.close svg = Tempfile.new(["plot", ".svg"]) svg.close ObjectSpace.undefine_finalizer(svg) # prevent the svg file from being deleted plt_erb = IO.read(File.join(File.dirname(File.expand_path(__FILE__)), "nom.plt.erb")) plt = Tempfile.new("plt") plt << ERB.new(plt_erb).result(binding) plt.close system("gnuplot "+plt.path) image_viewer = @config.get("image_viewer") system(image_viewer+" "+svg.path) end def edit Helpers::open_file File.join(@nom_dir, "input") end def editw Helpers::open_file File.join(@nom_dir, "weight") end def config Helpers::open_file File.join(@nom_dir, "config") end def config_usage @config.print_usage end private def nom_entry args, date summands = args.pop.split("+") number = summands.inject(0) do |sum, summand| factors = summand.split("x") sum + factors.map{ |f| f.to_f }.inject(1){ |p,f| p*f } end kcal = dequantize(number) if kcal == 0 raise "energy term cannot be zero" end description = args.join(" ") entry = FoodEntry.new(date, kcal, description) open(File.join(@nom_dir,"input"), "a") do |f| if not @inputs.empty? and entry.date != @inputs.last.date f << "\n" end f << entry.to_s end @inputs << entry if @inputs_at[date].nil? @inputs_at[date] = [] end @inputs_at[date] << entry status end def allowed_kcal date, r=nil if r.nil? r = @weights.rate_at(date, goal, rate) end if date > @weights.last_real date = @weights.last_real end @base_rate_at[date] + r*1000 end def consumed_at date inputs_at(date).inject(0){ |sum, i| sum+i.kcal } end def kg_to_go @weights.moving_average_at(Date.today) - goal end def kcal_to_burn kcal_per_kg_body_fat = 7000 kg_to_go * kcal_per_kg_body_fat end def days_to_go kcal_to_burn.abs/(rate*1000) end def plot_end @weights.last end def balance_start if @config.has("balance_start") @config.get("balance_start") else @weights.first end end def balance_end Date.today-1 end def kcal_balance sum = 0 balance_start.upto(balance_end) do |d| if consumed_at(d) != 0 sum += consumed_at(d) - allowed_kcal(d) end end sum end def truncate_date if @weights.empty? or Date.today - @weights.last_real > 30 return Date.today end first_start = @weights.first if @config.has("start_date") user_start = @config.get("start_date") [user_start, first_start].max else # find the last gap longer than 30 days gap = @weights.find_gap(30) if gap.nil? first_start else gap[1] end end end def quantize kcal, decimal_places=0 (1.0*kcal/@config.get("unit")).round(decimal_places) end def dequantize number (1.0*number*@config.get("unit")).round end def format_date date if date == Date.today return "Today" elsif date == Date.today-1 return "Yesterday" else return date.to_s end end def format_duration days if days <= 7 n = days.round(1) unit = "day" elsif days <= 7*4 n = (days/7.0).round(1) unit = "week" else n = (days/7.0/4.0).round(1) unit = "month" end "#{n} #{unit}#{n == 1 ? "" : "s"}" end def entry value, text="" puts "#{" "*(6-value.to_s.length)}(#{value}) #{text}" end def separator puts "---------------------" end def log_since start remaining = 0 start.upto(Date.today) do |date| puts puts "#{format_date(date)}: (#{quantize(allowed_kcal(date))})" puts remaining = quantize(allowed_kcal(date)) inputs_at(date).each do |i| entry(quantize(i.kcal), i.description) remaining -= quantize(i.kcal) end separator entry(remaining, "remaining (#{(100-100.0*remaining/quantize(allowed_kcal(date))).round}% used)") end if kcal_balance > 0 and @config.has("balance_start") cost = if @config.has("balance_factor") " (cost: %.2f)" % (@config.get("balance_factor")*quantize(kcal_balance)).round(2) else "" end entry(quantize(kcal_balance), "too much since #{balance_start}#{cost}") end end def read_file name, klass result = [] file = File.join(@nom_dir,name) FileUtils.touch(file) IO.readlines(file).each do |line| next if line == "\n" result << klass::from_line(line) end result end def goal @config.get("goal") end def rate @config.get("rate") end def inputs_at date @inputs_at[date] || [] end def precompute_inputs_at @inputs_at = {} @inputs.each do |i| @inputs_at[i.date] = [] if @inputs_at[i.date].nil? @inputs_at[i.date] << i end end def precompute_base_rate_at alpha = 0.05 @base_rate_at = {@weights.first => @weights.at(@weights.first)*25*1.2} (@weights.first+1).upto(@weights.last) do |d| intake = consumed_at(d-1) if intake == 0 @base_rate_at[d] = @base_rate_at[d-1] next end loss = @weights.moving_average_at(d-1) - @weights.moving_average_at(d) kcal_per_kg_body_fat = 7000 burned_kcal = loss*kcal_per_kg_body_fat new_base_rate_estimation = intake + burned_kcal @base_rate_at[d] = alpha*new_base_rate_estimation + (1-alpha)*@base_rate_at[d-1] end (@weights.last+1).upto(Date.today) do |d| @base_rate_at[d] = @base_rate_at[d-1] end end def which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exts.each { |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) } end return nil end end end nom-0.1.5/lib/nom/weight_database.rb000066400000000000000000000060131432372152200173060ustar00rootroot00000000000000require "yaml" require "fileutils" module Nom class WeightDatabase def initialize(file) @interpolated = {} @weights = {} @moving_averages = {} FileUtils.touch(file) IO.readlines(file).each do |line| date, weight = line.split(" ", 2) date = Date.parse(date) @weights[date] = weight.to_f @interpolated[date] = false end end def interpolate_gaps! @weights.keys.each_cons(2) do |a, b| (a+1).upto(b-1) do |d| @weights[d] = @weights[a] + (@weights[a]-@weights[b])/(a-b)*(d-a) @interpolated[d] = true end end end def precompute_moving_average!(alpha, beta, goal, rate) trend = 0 @moving_averages[first] = at(first) (first+1).upto(last).each do |d| @moving_averages[d] = alpha*at(d) + (1-alpha)*(@moving_averages[d-1]+trend) trend = beta*(@moving_averages[d]-@moving_averages[d-1]) + (1-beta)*trend end end def predict_weights!(rate, goal, tail) d = (last) loop do if (@weights[d] - goal).abs < 0.1 and d > Date.today tail -= 1 end if tail == 0 break end d += 1 prev_weight = @moving_averages[d-1] || @weights[d-1] @weights[d] = prev_weight+dampened_rate(prev_weight, goal, rate)/7.0 @interpolated[d] = true end end def dampened_rate weight, goal, rate r = (goal-weight).to_f if r.abs > 1 r/r.abs*rate else r*rate end end def real? date @weights[date] and not @interpolated[date] end def at date @weights[date] end def moving_average_at date @moving_averages[date] end def rate_at date, goal, rate if date > last_real dampened_rate(@weights[date], goal, rate) else dampened_rate(@moving_averages[date], goal, rate) end end def empty? @weights.empty? end def first @weights.keys.min end def last @weights.keys.max end def last_real @interpolated.select{|d, i| not i}.keys.max end def truncate date @weights.delete_if{|d, w| d < date} end def min @weights.values.min end def max @weights.values.max end def find_gap days gap = @weights.keys.reverse.each_cons(2).find{|a,b| a-b > days} if gap gap.reverse else nil end end end end nom-0.1.5/nom.gemspec000066400000000000000000000014031432372152200144430ustar00rootroot00000000000000Gem::Specification.new do |s| s.name = "nom" s.version = "0.1.5" s.add_runtime_dependency "nokogiri", "~> 1.6" s.executables << "nom" s.summary = "Lose weight and hair through stress and poor nutrition" s.description = "nom is a command line tool that helps you lose weight by tracking your energy intake and creating a negative feedback loop. It's inspired by John Walker's \"The Hacker's Diet\" and tries to automate things as much as possible." s.authors = ["blinry"] s.email = "mail@blinry.org" s.files = Dir.glob("{bin,lib}/**/*") + %w(README.md) s.requirements << 'gnuplot' s.homepage = "https://github.com/blinry/nom" s.license = "GPL-2.0+" end