Full Code of sqshq/sampler for AI

master 9bc7ba732d31 cached
58 files
211.3 KB
74.0k tokens
375 symbols
1 requests
Download .txt
Showing preview only (225K chars total). Download the full file or copy to clipboard to get everything.
Repository: sqshq/sampler
Branch: master
Commit: 9bc7ba732d31
Files: 58
Total size: 211.3 KB

Directory structure:
gitextract_kb9i7998/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE.md
├── README.md
├── asset/
│   ├── bindata.go
│   ├── file.go
│   └── player.go
├── component/
│   ├── alert.go
│   ├── asciibox/
│   │   └── asciibox.go
│   ├── barchart/
│   │   └── barchart.go
│   ├── block.go
│   ├── component.go
│   ├── gauge/
│   │   ├── gauge.go
│   │   └── gauge_test.go
│   ├── layout/
│   │   └── layout.go
│   ├── menu.go
│   ├── runchart/
│   │   ├── grid.go
│   │   ├── legend.go
│   │   └── runchart.go
│   ├── sparkline/
│   │   ├── sparkline.go
│   │   └── sparkline_test.go
│   ├── statusbar.go
│   ├── textbox/
│   │   └── textbox.go
│   └── util/
│       ├── format.go
│       ├── format_test.go
│       ├── geometry.go
│       ├── geometry_test.go
│       ├── math.go
│       ├── parse.go
│       └── parse_test.go
├── config/
│   ├── arrangement.go
│   ├── component.go
│   ├── config.go
│   ├── default.go
│   ├── options.go
│   └── validator.go
├── console/
│   ├── console.go
│   ├── palette.go
│   ├── palette_test.go
│   ├── signal.go
│   └── symbol.go
├── data/
│   ├── consumer.go
│   ├── consumer_test.go
│   ├── error.go
│   ├── int.go
│   ├── int_basic.go
│   ├── int_pty.go
│   ├── int_pty_windows.go
│   ├── item.go
│   ├── sampler.go
│   └── trigger.go
├── event/
│   └── handler.go
├── example.yml
├── go.mod
├── go.sum
└── main.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
custom: ['http://sampler.dev/sponsor']


================================================
FILE: .gitignore
================================================
*.dll
*.so
*.dylib
*.test
*.out
*.db
.idea/
*.iml
*.iws

================================================
FILE: .travis.yml
================================================
language: go
go:
  - 1.12.x
  - master
os:
  - linux
  - osx
addons:
  apt:
    packages:
      - libasound2-dev
dist: trusty
sudo: false
install: true
script:
  - env GO111MODULE=on go build
  - env GO111MODULE=on go test -v ./...


================================================
FILE: Dockerfile
================================================
FROM ubuntu:focal

ENV Sampler=/usr/local/bin/sampler

RUN apt-get update && apt-get upgrade --yes && apt-get install libasound-dev wget --yes \
  && wget https://github.com/sqshq/sampler/releases/download/v1.1.0/sampler-1.1.0-linux-amd64 -O $Sampler \
  && chmod +x $Sampler
  
ENTRYPOINT ["sampler"]


================================================
FILE: LICENSE.md
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program 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 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

================================================
FILE: README.md
================================================
# Sampler. Visualization for any shell command.
[![Build Status](https://travis-ci.com/sqshq/sampler.svg?token=LdyRhxxjDFnAz1bJg8fq&branch=master)](https://travis-ci.com/sqshq/sampler) [![Go Report Card](https://goreportcard.com/badge/github.com/sqshq/sampler)](https://goreportcard.com/report/github.com/sqshq/sampler)

Sampler is a tool for shell commands execution, visualization and alerting. Configured with a simple YAML file.

![sampler](https://user-images.githubusercontent.com/6069066/56404396-70b14d00-6234-11e9-93cd-54461bf40c96.gif)

## Why do I need it?
One can sample any dynamic process right from the terminal — observe changes in the database, monitor MQ in-flight messages,  trigger a deployment script and get notification when it's done. 

If there is a way to get a metric using a shell command, then it can be visualized with Sampler momentarily.

## Installation

### macOS

[HomeBrew](https://brew.sh):
```bash
brew install sampler
```

[MacPorts](https://www.macports.org):
```bash
sudo port install sampler
```

or
```bash
sudo curl -Lo /usr/local/bin/sampler https://github.com/sqshq/sampler/releases/download/v1.1.0/sampler-1.1.0-darwin-amd64
sudo chmod +x /usr/local/bin/sampler
```

### Linux
```bash
sudo wget https://github.com/sqshq/sampler/releases/download/v1.1.0/sampler-1.1.0-linux-amd64 -O /usr/local/bin/sampler
sudo chmod +x /usr/local/bin/sampler
```
Note: `libasound2-dev` system library is required to be installed for Sampler to play a [trigger](https://github.com/sqshq/sampler#triggers) sound tone. Usually the library is in place, but if not - you can install it with your favorite package manager, e.g `apt install libasound2-dev`
#### Packaging status
- [Fedora](https://apps.fedoraproject.org/packages/golang-github-sqshq-sampler) `sudo dnf install golang-github-sqshq-sampler` (F31+)
- [Arch](https://aur.archlinux.org/packages/sampler) `yay -S sampler`
### Windows (experimental)
Recommended to use with advanced console emulators, e.g. [Cmder](https://cmder.app/)

Via [Chocolatey](https://chocolatey.org/docs)
```powershell
choco install sampler
```
or

[Download .exe](https://github.com/sqshq/sampler/releases/download/v1.1.0/sampler-1.1.0-windows-amd64.exe)

### Docker

```
# Create a configuration file
vim config.yml

# Build the container image
docker build --tag sampler .

# Run a container
docker run --interactive --tty --volume $(pwd)/config.yml:/root/config.yml sampler --config /root/config.yml
```

## Usage
You specify shell commands, Sampler executes them with a required rate. The output is used for visualization.

Using Sampler is basically a 3-step process:
- Define your shell commands in a YAML configuration file
- Run `sampler -c config.yml`
- Adjust components size and location on UI

## But there are so many monitoring systems already
Sampler is by no means an alternative to full-scale monitoring systems, but rather an easy to setup development tool.

If spinning up and configuring [Prometheus with Grafana](https://prometheus.io) is complete overkill for you task, Sampler might be the right solution. No servers, no databases, no deploy - you specify shell commands, and it just works.

## Then it should be installed on every server I monitor?
No, you can run Sampler on local, but still gather telemetry from multiple remote machines. Any visualization might have `init` command, where you can ssh to a remote server. See the [SSH example](https://github.com/sqshq/sampler#ssh)

## Contents

- [Components](#components)
  - [Runchart](#runchart)
  - [Sparkline](#sparkline)
  - [Barchart](#barchart)
  - [Gauge](#gauge)
  - [Textbox](#textbox)
  - [Asciibox](#asciibox)
- [Bells and whistles](#bells-and-whistles)
  - [Triggers (conditional actions)](#triggers)
  - [Interactive shell (database interaction, remote server access, etc)](#interactive-shell-support)
  - [Variables](#variables)
  - [Color theme](#color-theme)
- [Real-world recipes (contributions welcome!)](#real-world-recipes)
  - [Databases (MySQL, PostgreSQL, MongoDB, Neo4j)](#databases)
  - [Kafka](#kafka)
  - [Docker](#docker)
  - [SSH](#ssh)
  - [JMX](#jmx)

## Components
The following is a list of configuration examples for each component type, with macOS compatible sampling scripts. 

### Runchart
![runchart](https://user-images.githubusercontent.com/6069066/59168666-aff96d00-8b04-11e9-99b6-34d8bae37bd2.png)
```yml
runcharts:
  - title: Search engine response time
    rate-ms: 500        # sampling rate, default = 1000
    scale: 2            # number of digits after sample decimal point, default = 1
    legend:
      enabled: true     # enables item labels, default = true
      details: false    # enables item statistics: cur/min/max/dlt values, default = true
    items:
      - label: GOOGLE
        sample: curl -o /dev/null -s -w '%{time_total}'  https://www.google.com
        color: 178      # 8-bit color number, default one is chosen from a pre-defined palette
      - label: YAHOO
        sample: curl -o /dev/null -s -w '%{time_total}'  https://search.yahoo.com
      - label: BING
        sample: curl -o /dev/null -s -w '%{time_total}'  https://www.bing.com
```
### Sparkline
![sparkline](https://user-images.githubusercontent.com/6069066/59167746-de754900-8b00-11e9-9305-c9a4176634d2.png)
```yml
sparklines:
  - title: CPU usage
    rate-ms: 200
    scale: 0
    sample: ps -A -o %cpu | awk '{s+=$1} END {print s}'
  - title: Free memory pages
    rate-ms: 200
    scale: 0
    sample: memory_pressure | grep 'Pages free' | awk '{print $3}'
```
### Barchart
![barchart](https://user-images.githubusercontent.com/6069066/59167751-de754900-8b00-11e9-8d01-efd04ae1eec6.png)
```yml
barcharts:
  - title: Local network activity
    rate-ms: 500        # sampling rate, default = 1000
    scale: 0            # number of digits after sample decimal point, default = 1
    items:
      - label: UDP bytes in
        sample: nettop -J bytes_in -l 1 -m udp | awk '{sum += $4} END {print sum}'
      - label: UDP bytes out
        sample: nettop -J bytes_out -l 1 -m udp | awk '{sum += $4} END {print sum}'
      - label: TCP bytes in
        sample: nettop -J bytes_in -l 1 -m tcp | awk '{sum += $4} END {print sum}'
      - label: TCP bytes out
        sample: nettop -J bytes_out -l 1 -m tcp | awk '{sum += $4} END {print sum}'
```
### Gauge
![gauge](https://user-images.githubusercontent.com/6069066/59318799-4c06ae00-8c96-11e9-868a-7fef803f3739.png)
```yml
gauges:
  - title: Minute progress
    rate-ms: 500        # sampling rate, default = 1000
    scale: 2            # number of digits after sample decimal point, default = 1
    percent-only: false # toggle display of the current value, default = false
    color: 178          # 8-bit color number, default one is chosen from a pre-defined palette
    cur:
      sample: date +%S  # sample script for current value
    max:
      sample: echo 60   # sample script for max value
    min:
      sample: echo 0    # sample script for min value
  - title: Year progress
    cur:
      sample: date +%j
    max:
      sample: echo 365
    min:
      sample: echo 0
```
### Textbox
![textbox](https://user-images.githubusercontent.com/6069066/59168949-192db000-8b06-11e9-900b-0e92ff494f62.png)
```yml
textboxes:
  - title: Local weather
    rate-ms: 10000      # sampling rate, default = 1000
    sample: curl wttr.in?0ATQF
    border: false       # border around the item, default = true
    color: 178          # 8-bit color number, default is white
  - title: Docker containers stats
    rate-ms: 500
    sample: docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"
```
### Asciibox
![asciibox](https://user-images.githubusercontent.com/6069066/59169283-aa515680-8b07-11e9-8beb-716a387aed1b.png)
```yml
asciiboxes:
  - title: UTC time
    rate-ms: 500        # sampling rate, default = 1000
    font: 3d            # font type, default = 2d
    border: false       # border around the item, default = true    
    color: 43           # 8-bit color number, default is white
    sample: env TZ=UTC date +%r
```

## Bells and whistles

### Triggers
Triggers allow to perform conditional actions, like visual/sound alerts or an arbitrary shell command.
The following examples illustrate the concept.

#### Clock gauge, which shows minute progress and announces current time at the beginning of each minute

```yml
gauges:
  - title: MINUTE PROGRESS
    position: [[0, 18], [80, 0]]  
    cur:
      sample: date +%S
    max:
      sample: echo 60
    min:
      sample: echo 0
    triggers:
      - title: CLOCK BELL EVERY MINUTE
        condition: '[ $label == "cur" ] && [ $cur -eq 0 ] && echo 1 || echo 0'  # expects "1" as TRUE indicator
        actions:
          terminal-bell: true  # standard terminal bell, default = false
          sound: true    # NASA quindar tone, default = false
          visual: false  # notification with current value on top of the component area, default = false
          script: say -v samantha `date +%I:%M%p`  # an arbitrary script, which can use $cur, $prev and $label variables
```

#### Search engine latency chart, which alerts user when latency exceeds a threshold

```yml
runcharts:
  - title: SEARCH ENGINE RESPONSE TIME (sec)
    rate-ms: 200
    items:
      - label: GOOGLE
        sample: curl -o /dev/null -s -w '%{time_total}'  https://www.google.com
      - label: YAHOO
        sample: curl -o /dev/null -s -w '%{time_total}'  https://search.yahoo.com     
    triggers:
      - title: Latency threshold exceeded
        condition: echo "$prev < 0.3 && $cur > 0.3" |bc -l  # expects "1" as TRUE indicator
        actions:
          terminal-bell: true  # standard terminal bell, default = false
          sound: true   # NASA quindar tone, default = false
          visual: true  # visual notification on top of the component area, default = false
          script: 'say alert: ${label} latency exceeded ${cur} second' # an arbitrary script, which can use $cur, $prev and $label variables
```

### Interactive shell support
In addition to the `sample` command, one can specify `init` command (executed only once before sampling) and `transform` command (to post-process `sample` command output). That covers interactive shell use case, e.g. to establish connection to a database only once, and then perform polling within interactive shell session.

#### Basic mode
```yml
textboxes:
  - title: MongoDB polling
    rate-ms: 500
    init: mongo --quiet --host=localhost test # executes only once to start the interactive session
    sample: Date.now();                       # executes with a required rate, in scope of the interactive session
    transform: echo result = $sample          # executes in scope of local session, $sample variable is available for transformation
```

#### PTY mode
In some cases interactive shell won't work, because its stdin is not a terminal. We can fool it, using PTY mode:
```yml
textboxes:
  - title: Neo4j polling
    pty: true  # enables pseudo-terminal mode, default = false
    init: cypher-shell -u neo4j -p pwd --format plain
    sample: RETURN rand();
    transform: echo "$sample" | tail -n 1
  - title: Top on a remote server
    pty: true  # enables pseudo-terminal mode, default = false
    init: ssh -i ~/user.pem ec2-user@1.2.3.4
    sample: top    
```

#### Multistep init
It is also possible to execute multiple init commands one after another, before you start sampling.
```yml
textboxes:
  - title: Java application uptime
    multistep-init:
      - java -jar jmxterm-1.0.0-uber.jar
      - open host:port # or local PID
      - bean java.lang:type=Runtime
    sample: get Uptime
```    

### Variables
If the configuration file contains repeated patterns, they can be extracted into the `variables` section.
Also variables can be specified using `-v`/`--variable` flag on startup, and any system environment variables will also be available in the scripts.

```yml
variables:
    mongoconnection: mongo --quiet --host=localhost test
barcharts:
  - title: MongoDB documents by status
    items:
      - label: IN_PROGRESS
        init: $mongoconnection
        sample: db.getCollection('events').find({status:'IN_PROGRESS'}).count()
      - label: SUCCESS
        init: $mongoconnection
        sample: db.getCollection('events').find({status:'SUCCESS'}).count()
      - label: FAIL
        init: $mongoconnection
        sample: db.getCollection('events').find({status:'FAIL'}).count()
```

### Color theme
![light-theme](https://user-images.githubusercontent.com/6069066/59959405-994c0200-9484-11e9-856b-c4d18716e1de.png)
```yml
theme: light # default = dark
sparklines:
  - title: CPU usage
    sample: ps -A -o %cpu | awk '{s+=$1} END {print s}'
```

## Real-world recipes
### Databases
The following are different database connection examples. Interactive shell (init script) usage is recommended to establish connection only once and then reuse it during sampling.

<details><summary>MySQL</summary>

```yml
# prerequisite: installed mysql shell

variables:
  mysql_connection: mysql -u root -s --database mysql --skip-column-names
sparklines:  
  - title: MySQL (random number example)
    pty: true
    init: $mysql_connection
    sample: select rand();
```

</details>

<details><summary>PostgreSQL</summary>

```yml
# prerequisite: installed psql shell

variables:
  PGPASSWORD: pwd
  postgres_connection: psql -h localhost -U postgres --no-align --tuples-only
sparklines:
  - title: PostgreSQL (random number example)
    init: $postgres_connection
    sample: select random();
```

</details>

<details><summary>MongoDB</summary>

```yml
# prerequisite: installed mongo shell

variables:
  mongo_connection: mongo --quiet --host=localhost test
sparklines:
  - title: MongoDB (random number example)
    init: $mongo_connection
    sample: Math.random();
```

</details>

<details><summary>Neo4j</summary>

```yml
# prerequisite: installed cypher shell

variables:
  neo4j_connection: cypher-shell -u neo4j -p pwd --format plain
sparklines:
  - title: Neo4j (random number example)
    pty: true
    init: $neo4j_connection
    sample: RETURN rand();
    transform: echo "$sample" | tail -n 1
```

</details>

### Kafka

<details><summary>Kafka lag per consumer group</summary>

```yml
variables:
  kafka_connection: $KAFKA_HOME/bin/kafka-consumer-groups --bootstrap-server localhost:9092
runcharts:
  - title: Kafka lag per consumer group
    rate-ms: 5000
    scale: 0
    items:
      - label: A->B
        sample: $kafka_connection --group group_a --describe | awk 'NR>1 {sum += $5} END {print sum}'
      - label: B->C
        sample: $kafka_connection --group group_b --describe | awk 'NR>1 {sum += $5} END {print sum}'
      - label: C->D
        sample: $kafka_connection --group group_c --describe | awk 'NR>1 {sum += $5} END {print sum}'
```

</details>

### Docker

<details><summary>Docker containers stats (CPU, MEM, O/I)</summary>

```yml
textboxes:
  - title: Docker containers stats
    sample: docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
```

</details>

### SSH

<details><summary>TOP command on a remote server</summary>

```yml
variables:
  sshconnection: ssh -i ~/my-key-pair.pem ec2-user@1.2.3.4
textboxes:
  - title: SSH
    pty: true
    init: $sshconnection
    sample: top
```

</details>

### JMX

<details><summary>Java application uptime example</summary>

```yml
# prerequisite: download [jmxterm jar file](https://docs.cyclopsgroup.org/jmxterm)

textboxes:
  - title: Java application uptime
    multistep-init:
      - java -jar jmxterm-1.0.0-uber.jar
      - open host:port # or local PID
      - bean java.lang:type=Runtime
    sample: get Uptime
    transform: echo $sample | tr -dc '0-9' | awk '{printf "%.1f min", $1/1000/60}'
```

</details>


================================================
FILE: asset/bindata.go
================================================
// Code generated by go-bindata. (@generated) DO NOT EDIT.
// sources:
// 3d.flf
// 2d.flf
// quindar-tone.mp3
package asset

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"
)

func bindataRead(data []byte, name string) ([]byte, error) {
	gz, err := gzip.NewReader(bytes.NewBuffer(data))
	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}

	var buf bytes.Buffer
	_, err = io.Copy(&buf, gz)
	clErr := gz.Close()

	if err != nil {
		return nil, fmt.Errorf("Read %q: %v", name, err)
	}
	if clErr != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

type asset struct {
	bytes []byte
	info  os.FileInfo
}

type bindataFileInfo struct {
	name    string
	size    int64
	mode    os.FileMode
	modTime time.Time
}

func (fi bindataFileInfo) Name() string {
	return fi.name
}
func (fi bindataFileInfo) Size() int64 {
	return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
	return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
	return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
	return false
}
func (fi bindataFileInfo) Sys() interface{} {
	return nil
}

var _data3dFlf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x5a\x5d\x6b\x5b\x49\x0f\xbe\xf7\xaf\xd0\x85\xe1\xb4\x70\x88\xde\xa6\x7d\x17\x0a\xc6\x04\x7a\x51\x58\x7c\xbd\x57\x03\x3a\x6e\xeb\x64\xcb\xba\x49\x49\x9a\xdd\x0d\xe4\xc7\x2f\x33\x23\x69\xa4\x39\x1f\x3e\xb6\x93\x90\xdd\xce\xd8\x39\xf3\x8c\x34\xd2\xa3\x8f\x39\xd7\xfb\xeb\xcb\xed\x12\x3e\xc2\x6f\xf0\xfe\x7f\xf0\x0e\xfe\xbf\xd8\x6f\xef\xef\x9f\xde\x7f\xbb\xb8\xde\x5f\xc3\x97\x27\xd8\xc4\x29\x7c\xde\xed\xbf\xec\xee\x6f\xe0\x4d\xfa\xf6\xe6\x6a\xfb\xf7\xc3\xc5\xd7\xbb\x1f\x6f\x17\x6f\x1e\x7e\xdd\xed\x77\xb7\xb0\xff\xfe\x65\x77\xbf\xdd\xef\x9f\xe0\xfa\xfe\xee\x07\xfc\xfe\xb8\xbd\x85\x4f\xdb\xfb\xe6\x01\x7e\x3e\x5e\x5f\x3f\xc5\xd5\xde\x2e\x7e\xfd\xb3\xdb\xfe\xb5\xfb\x16\x97\xfd\xbc\xdf\xdd\xde\xc2\xa7\x3f\xb7\x3f\x7f\xee\xf6\x7b\x58\xdd\xdc\x7c\xbd\x7a\xfc\xfe\xf8\xf5\x62\xf7\xed\x71\xbd\xf8\x63\x77\xff\xf0\xfd\xee\x16\xde\x5d\x5c\xc2\x25\x5e\x7e\xc0\x8f\x1f\x16\x0b\x58\x5e\x4d\xff\xc7\x3f\x57\x57\x0b\x20\xca\xc3\x05\x06\x08\x79\x14\x07\x21\x8d\x80\x87\xf1\x91\x00\x81\x42\x1a\x41\xc0\x40\x21\x2f\x13\x90\xd0\x2c\x58\x2f\xed\x56\xb7\x00\x14\xe2\x6a\x19\x03\x09\x91\x70\x99\x1f\x5f\xe6\xe7\x97\x79\x2d\x99\x95\x85\x1d\x4a\x6f\x12\x67\x05\x36\x7e\x49\x19\x38\x7d\x12\xb7\x11\x07\xf1\x37\x49\x15\x90\x36\xf9\xfb\x0d\xa5\x95\x30\xc4\xbf\x4b\xdf\x27\x99\x91\x30\x6f\x55\x84\xe4\xcd\x7a\x58\xe8\x4d\x8b\x62\x45\x78\x51\x44\xc0\x86\xa8\x0b\xe5\xf3\xc0\x5b\x89\x7b\x24\x82\x90\x55\x92\xb5\x9d\x37\x01\x5d\x20\x60\x35\x77\x81\x8a\xf2\x59\xfd\x05\x2c\xfe\x93\x84\x8c\xeb\x23\x20\xcb\x88\x3c\x91\x6d\xe6\x09\xcf\xd2\x84\xb2\xf8\x90\xff\x96\x01\xd2\xa4\x3a\xe1\xbe\xb0\xac\xf3\xa2\x72\x7c\x8e\x1a\x0c\x3c\x7b\xc6\x38\x7a\x96\x07\x03\x22\x44\xf9\x31\xc9\x19\xff\x34\x6c\xd6\x00\x2b\xba\x5a\xc0\x73\x56\x01\x61\xd6\x76\x40\x9e\xd4\x80\x3d\xf8\x61\x1b\x8e\x16\x95\x21\x93\x1d\x89\x71\x19\xcb\x5a\x0e\x5a\xae\xb3\xe1\x64\x0c\xf9\x53\x6c\x64\x71\x0c\xd0\x62\xe3\x7c\x45\x9c\xa5\x53\x6f\xe9\x02\xf1\x39\x75\xa1\xb2\x18\x3b\xac\xb6\xcf\x96\x11\xe2\xbf\x79\xa9\x2e\x8e\xb3\xe3\x75\x21\xee\x21\x1f\x54\x20\x3e\x96\x6a\xf1\x6a\xfb\x95\x27\x80\xba\x41\x07\x0d\x88\x0f\xac\x21\x9e\x80\x38\x40\x0b\xad\xb3\x7e\x6b\xfa\x4e\xf9\x6e\x5c\x81\xc9\x49\x24\xbd\x50\x36\x6e\xf6\xbf\xe8\x5d\x11\x20\x9e\x6f\xb2\x73\x06\x30\x04\x33\x07\x4c\x3e\xaa\xfe\xcf\x30\x51\x4d\xd1\x06\xf2\xf9\xf3\xb7\x23\x47\xe0\x86\x94\x7f\x64\xaf\xa4\x5b\x4d\xc6\x78\x78\x85\xc3\x5b\xe3\x05\xd1\x7d\x2b\x4b\x10\x15\x1f\x2d\xfe\x99\x8f\x3c\xfb\xb3\xf1\xe5\xec\xa0\x71\xd1\x3c\x98\xb2\x62\x73\x34\x42\x40\xac\x28\x14\xb6\x0f\x10\x0c\xe1\x07\x3e\x90\xa0\xec\x1f\x15\x20\x01\x20\x8d\x0f\x31\xb1\x38\x4f\xde\x75\x53\x2c\x22\x59\x99\xba\x0d\x92\x6e\x81\xed\x40\x62\x4e\x6d\x15\x7d\xb3\x18\x42\x25\x22\x2b\x6b\x61\x5b\x0a\x6c\x97\xf9\x00\x10\x01\x95\xf8\xe2\x98\x09\x57\x0e\xde\xb0\xd0\x2c\xd4\x11\x0d\x53\xd8\xa8\x86\x91\x90\x02\xad\x88\x55\x12\x03\xcf\x39\x1a\x9e\x8a\xb0\x32\x93\xa3\x4c\x31\x48\x81\xc4\xc7\x6b\x27\x9f\xa3\x61\xf6\x10\x41\x8d\x73\x63\x40\x69\xdb\x8c\x1a\x95\xdf\xb1\x78\x22\x6c\x46\x65\x61\x35\x89\x98\xa1\x61\xaa\xcf\xd5\xd8\x30\x89\x27\x24\x99\x13\xa8\x0a\x7e\xa6\x86\x45\x5a\x25\x04\x48\xcb\xe5\xc7\xb1\xc1\x46\xe2\x68\x13\x7f\xc5\xd4\x53\x78\x60\x53\x47\x5d\x3b\x46\xbf\x21\xa0\x43\xd6\x04\xd6\x5f\xad\x35\xad\x81\xe0\xc5\xac\x69\x26\x6a\x5a\xb9\x15\x96\x40\xa2\x16\x05\x15\xc4\x61\x75\xed\x5e\xf6\x30\xc4\x12\xe5\x53\x89\x86\x92\xc4\xb0\xcb\x48\x68\xd2\xb0\xd4\x7b\xee\xb8\x85\x24\xb1\x0a\x12\x42\xeb\x85\x48\x79\x01\x98\x7e\xd3\x20\x2e\xb7\x02\x58\xb1\x22\xba\xf4\x9b\x62\x74\x0c\xf7\xcf\x12\xed\x27\xe2\xb1\xd5\x83\x9a\x96\x0d\x36\x00\x26\xdc\xa8\xe0\x12\x88\x94\x8f\x26\xb5\x3a\x40\x13\x50\x27\x18\x6e\xf3\x6b\x80\x75\x25\x6e\x10\x7b\xc9\x36\x7b\x20\x3f\xca\x06\x93\xbd\x2e\xd1\x49\xe8\x38\xca\x45\x0f\x91\x8c\xe5\xb8\xf2\xa1\x12\x88\x5c\x2c\xa1\x2e\x90\xe0\xc6\x19\x04\xcd\xff\xc4\x09\x02\x27\x61\x99\xe9\xf2\x58\xb4\xd8\x09\xad\x4f\x90\x9b\xb0\x1b\x38\x52\xdd\x38\x56\x85\x94\xb1\x8b\xc7\x71\x2c\x55\x86\x33\xae\x90\x68\x76\x46\x26\xed\x12\xe9\x44\x65\x7d\x70\xc5\x86\x55\x53\xa0\xcf\xe1\xd6\x69\x54\xf1\x20\x16\x51\xcc\xf2\xd5\x51\x6d\x22\x02\x86\xd8\x4a\x69\xf4\xf2\xa8\x1b\x2b\x2b\x50\xd8\xc0\xcb\xca\x6a\x30\x0d\x62\xc1\xc3\x62\x4a\xd6\x8e\x8a\x11\x4d\xe4\x9d\x47\x48\x16\x36\x64\x25\xc3\xf6\x1c\xc9\xc0\x27\x21\x55\x23\x81\x2a\x87\x01\xe7\x30\xf0\x12\x0e\x63\xe0\x89\x6b\x7b\x61\xfc\xa4\x48\x79\xd0\x16\x6b\x71\x9a\xeb\x12\x89\xdf\x86\x1e\x4a\xda\x37\x0b\xbe\x80\x53\x09\xcd\x84\x25\x8d\xa5\x22\xa7\xaf\xec\x8d\xb6\x07\x94\xed\xc6\x33\x54\x9d\xb2\x11\x55\x75\x9b\xe2\x54\x51\x75\x90\xda\xf4\x74\x55\x97\xef\xb4\xbe\x03\x5b\x04\x1b\x7f\x95\x96\xc2\xd9\x9e\xe3\xbf\xc3\x26\x10\x9a\xbc\x24\xfd\xb8\xec\xd3\xd7\x2f\x68\x13\xa2\x50\x84\x4e\xae\x34\x57\xe8\x09\x9d\x77\x3e\xcb\x6e\xa1\x73\xe6\xdd\xbd\x88\x79\x1b\xa5\x03\x0d\xb1\xa4\xa7\x49\xcf\x93\x60\x15\x7f\x8a\x79\xc3\x08\x6d\x81\xd0\x56\x2b\xa5\xd2\xe9\xb4\x75\xba\x88\xa1\x09\x4e\x44\xab\xe1\xd4\xaa\x9b\x1f\x71\x3d\x6d\x0e\x07\xfc\x16\xb4\x91\x56\x6a\xac\xf1\x03\x86\x33\xe1\x5b\xa6\x6d\x4e\xb5\x83\x12\x0c\x54\xf9\xbe\xe6\x37\xa7\xf0\x17\x55\xe4\x69\xa9\xd3\x12\x27\x2f\x0d\x26\xdd\x77\xd9\x7e\x6d\xd0\xc7\xf2\x17\xbc\xb6\x31\x0b\xfc\xe1\xc6\x47\x95\x6f\xa0\xf2\x57\xd2\x32\x43\xa5\x2c\x72\x4e\x7c\x04\xf0\xc8\x71\xe6\xd1\x71\x78\x07\xbd\x5d\x98\xcc\x27\xed\xe4\xdf\xb2\x9b\x86\x2d\xbe\xdf\x9d\xae\x95\x61\x77\xe6\x7b\xa7\xf6\x40\xba\xd0\x99\x88\xd2\x05\x84\x75\x8e\x28\xea\xd6\x0d\x96\x98\x92\xf2\xfb\xca\xfe\x07\x08\xb6\xde\x49\xd1\x90\x36\xee\xa4\x40\xcb\x3b\x88\x28\x52\x61\x81\x14\xdb\xdd\x39\x36\xd8\xcb\x18\xb8\xa6\xd7\xc0\x2d\x75\xbd\x3e\x2c\x53\x91\x34\x95\x1c\x44\xe0\x33\x87\xda\x1e\x7b\x16\xd9\x3f\x06\x47\x00\xec\xfe\x8e\xfa\xc0\x85\x54\xeb\x0c\xe0\x48\xcf\xc6\xd3\x43\x1e\xa8\x58\xca\xb2\x9d\x96\x85\x9d\x8c\xa5\xf1\xdc\x49\xe7\xd9\xb7\xb3\x47\x3b\xa1\xb5\x40\x4c\x58\x85\x52\x60\x90\x52\x8a\x6f\x67\x6d\xce\x16\xc8\xb5\x2c\xc0\x74\x2c\x08\x85\x38\x03\x62\xac\x61\xf3\x15\x80\xbb\x5e\x1a\xba\x5d\x9a\x04\x3b\xf8\x47\xa3\x63\xc5\x4d\x9b\x28\x4d\x66\x57\xdc\x97\xda\x7e\xf4\x52\x23\xe8\xa5\x06\x9e\x73\xa9\x31\xb6\x5f\xdf\x61\x33\xd7\x54\x9b\x70\x91\xda\x87\xb9\xd7\x76\x11\x74\xbf\xbd\x2c\x66\xcc\xe4\x86\xb3\x46\xe9\xad\x25\x6b\xc8\x90\xc3\x59\x63\x7b\x52\xd6\xe8\x47\x12\xe9\xa4\x7d\x58\x7a\x87\x7a\xff\x56\x8e\x61\xbc\x2f\x61\x94\x54\xdc\xd2\xd4\x39\xbe\x1f\x51\xba\x11\xa4\xbd\x08\x8c\x23\x98\xf0\xa1\xe1\x03\xb4\xdb\xef\x82\x66\x4b\x47\x6e\x1f\x44\xe5\xc3\x5a\x88\xe9\x5c\x28\xf9\xf3\x55\x7d\xf5\x3b\x75\xdb\x34\xcf\xae\xc0\xd9\x15\x0c\x5d\x7f\x12\x91\x9e\x3e\xda\x22\x89\x72\x7c\x73\x76\xe5\x0d\xcb\x58\x16\x14\xf6\x04\x32\x57\x73\x2f\x51\x77\xea\xe7\xa6\xfc\x13\xbe\xf1\x71\x5c\x35\xa8\x0a\xb4\x8f\x0f\x9d\xb9\xe5\x4f\x65\xcf\xd0\xbf\x93\xd7\xb2\xb2\x14\x95\xa6\xc0\x12\x3f\x99\xa1\x2b\x6c\xec\x1d\xe6\x2b\x14\x8e\x26\xec\x94\xab\xe1\x48\xcc\x55\x58\xd3\xe8\x40\x7a\x5d\xc1\xe7\xaf\xda\x3b\x18\x11\x46\xb7\xa2\x1c\x50\x8a\x9a\x26\x16\x1b\x5c\x70\x94\xa4\x10\x8b\xcb\x5a\x89\x45\xdc\x79\x59\xcd\xc0\x57\x85\x83\x18\x1b\x3c\xae\xc5\x34\x78\x47\xd2\x44\x61\x39\x28\x34\xc1\x64\x3a\x60\x1e\x07\x7c\xd9\x0f\x6d\xb5\xa0\x74\x7d\x5a\x3d\x38\xc1\xdf\x8b\xe9\x0b\xcb\x89\xab\x08\x3a\x74\x15\x31\xe0\x7a\x0b\x20\x7d\x71\xa2\x6b\xf2\x7d\xaf\xee\xbc\xec\xdb\x33\xdf\x88\xe7\xfa\x63\x10\x9a\x6d\x85\x66\xe3\x66\x73\x63\x7c\xee\x31\xb8\x84\x4d\xde\x62\xc9\x4a\xb6\x3d\x59\x97\x19\x96\xc4\xd0\x96\xc2\x07\xad\xa8\x77\xd4\xe5\x7d\x12\x6b\x9e\x5a\x83\xd8\x5e\x4e\x3f\x26\x9f\x8b\x81\xf0\xac\x18\x28\x07\x50\xdd\x61\x8d\x60\xc0\xa0\xe7\x0b\x96\xc7\xab\x30\xa5\xd2\xcb\xb8\xa9\xb8\x6a\x14\xbb\x87\x3f\x04\x34\x62\x0e\xfc\xda\x11\x03\x37\xf9\x06\x71\x0d\xb0\x42\x4e\x02\x51\x7c\x1e\x8f\xb8\x13\x19\x10\x0f\x06\x8b\xdb\xd2\x15\xc3\xae\xc4\x59\xe8\xd2\xb1\xad\x41\x9c\x04\x4d\x71\xcb\xfa\x9e\xe6\x02\x50\x52\x6f\x3b\x6d\x1c\x00\xb8\x8b\x2b\x73\x6d\x35\x62\x84\x0a\x22\x8c\xdf\x48\x7f\xb4\x4d\xea\x5f\xf1\xab\x2a\x2b\x90\x82\xa4\x94\x23\x95\x6d\x0f\x7b\x8f\x8b\x7a\xb6\x5f\x39\x58\x5b\x41\x89\x76\xb3\xbb\x1e\x2e\x11\x60\x5a\xe7\x97\x78\xe4\xcd\xa0\x75\x3c\xe8\xf4\x16\x44\xa6\x82\x54\xcc\x66\x7d\x0d\xf1\x0a\xd3\xa0\xbb\x61\x83\xc0\x6f\x51\x48\x85\x43\x98\xdf\x9b\xcb\x66\x43\xc8\x95\xc0\x52\x35\xbb\x9c\x8c\xcd\x07\xe3\x36\xf8\x5c\xc4\xe7\x23\x72\x35\xe6\x2e\xda\x55\x8d\xa0\x56\x96\xd5\x85\x27\xf5\x9b\x27\xe1\xc9\xc1\xe3\x4b\xb6\x8b\xe0\x30\xfe\xab\xf6\x5e\x0f\xe2\x67\x15\x92\x3e\x59\x8a\x8e\x91\xd6\xa0\xbc\xf0\x91\xea\xf7\xf6\xe8\x76\x99\xa2\x17\x70\x87\x5d\x43\x1b\xe4\xa3\xdb\x65\x2e\xa5\xae\x51\xcd\xdb\x0e\x18\x46\x2e\x05\x4f\x40\xad\x3a\xdd\xd4\x6b\xce\xe9\x39\xaf\x68\x45\xbe\x90\x28\x6c\xe1\x5f\x66\x53\xd4\xac\xad\x0a\xf5\xbf\x00\x00\x00\xff\xff\xb2\x63\x04\xcd\x51\x2c\x00\x00")

func data3dFlfBytes() ([]byte, error) {
	return bindataRead(
		_data3dFlf,
		"3d.flf",
	)
}

func data3dFlf() (*asset, error) {
	bytes, err := data3dFlfBytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "3d.flf", size: 11345, mode: os.FileMode(420), modTime: time.Unix(1550435406, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}

var _data2dFlf = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x7d\x6d\x73\xda\x48\xb6\xff\x7b\x3e\xc5\x79\x31\xf5\x0f\xd4\x3f\xb8\x8d\x6c\x63\xbb\x2a\x9b\x32\x06\xd9\xd6\x04\x83\x57\x40\x66\x93\xd1\x54\x0f\x89\x49\x4c\x0d\x81\x94\x8d\x77\x6e\xea\xea\x7e\xf7\x5b\xea\x67\x75\x9f\x96\x5a\xce\xde\xd4\xae\xc1\x18\x9d\x5f\x9f\xee\x3e\xe7\x77\x1e\x5a\x9a\x2f\x9b\x2f\xd1\xf2\x17\xe8\xc3\x09\xf4\xfa\xd0\x3b\x81\x5e\x0f\x0e\x21\x3a\x3e\xee\x1f\xb5\x66\xfb\xe5\xf6\x7e\xf9\x78\x0f\x9f\x7e\xc0\xf5\x66\xb5\xdd\xc2\xf0\x61\xf9\xfd\xfb\x6a\xb3\x81\xff\x07\xc9\x92\xfd\xba\x86\x23\x72\x7e\x04\xdd\x2e\x7c\x5a\x3e\xad\xee\x61\xb7\x85\xab\xc7\xe5\xf6\xaf\x57\x4f\x70\xf0\xb4\xfe\xda\x4a\xb6\x9f\x37\xcf\xf7\xab\x27\x48\x66\x53\x18\x2f\xf7\xeb\x6d\xb7\xd7\xfa\xb2\xfe\xba\x59\xed\xe1\x71\xb5\x59\x2d\x9f\x56\x10\x1d\xf4\x0a\x01\xbd\x08\x06\xcf\x5f\xa1\x77\x7e\x7e\xdc\xba\xdd\xdd\xaf\xbf\xac\x57\xf7\xf0\x65\xf7\x08\xe2\xeb\xd1\x41\x54\x8c\xe4\xd7\xdd\xc3\x16\x86\xbb\xbf\x97\x5b\x78\xf3\xb9\x78\xb9\xf8\xfc\x79\xbd\x39\xd8\x3d\x7e\x7d\xdb\x02\xd8\xef\x60\x79\x7f\x2f\x90\xfe\x3b\x7a\x7d\xf4\xfa\xf8\xf5\xc9\xff\xc0\xd3\xf3\xf7\xef\xbb\xc7\x3d\xb4\x17\xdb\xf5\xe7\xdd\xfd\x0a\x16\xff\xff\xb0\x77\x78\xd8\x3d\xec\x9d\x5e\x75\x0e\x5a\x77\xab\xc7\x6f\xeb\xa7\xa7\xf5\x6e\x0b\xeb\x27\x78\x58\x3d\xae\x3e\xfd\x80\xaf\xeb\x7f\xaf\xb6\x85\xc0\x6f\xc5\x60\x7e\xc0\xfe\x61\xfd\x04\x5f\x76\xdb\xfd\x6b\x58\x3e\xc1\x66\xb7\xfd\x5a\xbc\xee\x1f\x56\xad\x6f\x7c\xb4\x8f\xaf\x9e\x60\xbb\xfc\xb6\x2a\x64\x7c\xdf\x2c\x3f\xf3\xe9\x58\xc2\xe7\xdd\xb7\x6f\xab\xed\x1e\x36\xeb\xed\xea\xa0\xa5\x75\xfb\xf4\x03\xee\x96\xcf\x1b\xb8\x7c\x7e\xdc\xef\xb6\xf0\xe6\x69\xb7\x79\xde\xaf\x77\xdb\x8b\xd5\xf2\x71\xff\xb0\x59\x6f\xff\x3a\xd8\xae\xf6\x6f\xa1\x17\x91\xf3\x7e\x31\x90\x35\x9f\x4c\xd8\xae\xfe\x86\xef\xcb\xc7\xe5\xb7\xd5\x7e\xf5\xd8\x12\xba\x71\x81\x57\xc9\x75\x31\x57\xcb\xed\x7d\xf1\xf6\xb7\xf5\xf6\x00\xe0\x76\xf9\x03\x96\x9b\xa7\x1d\x7c\x5a\xc1\xd3\x66\xfd\xf5\x61\xbf\xf9\x01\xdf\xcc\x19\xfe\xb4\xda\xef\x57\x8f\xf0\xfc\xb4\x6a\xed\xbe\x30\xf1\x5f\x9e\x37\x9b\xee\xdf\xeb\xfb\xfd\x03\xf9\x6b\xf5\xb8\x25\x4f\xdf\x9e\x9f\x1e\x60\xb9\xd9\xaf\x1e\xb7\xcb\xfd\xfa\xdf\xab\xa7\xd7\xf0\xe9\x79\x0f\xf7\xab\x2f\xcb\xe7\xcd\x1e\x76\xcf\xfb\xef\xcf\xfb\x42\xf3\xc9\x74\x0e\x9f\x1f\x96\xdb\xaf\xab\xfb\x83\x16\xfc\x72\x81\xfd\xff\xa2\x05\x40\xe1\xa2\x05\x39\xe4\xfa\x27\x2d\x7e\xb6\x69\xa7\xf8\x2b\x80\xf8\x12\xfb\x5a\x1b\x72\x60\x1f\xbf\x87\xf7\xc0\xfe\xfc\x0b\x94\x5f\xe5\x05\xc5\x25\xc5\xff\xd8\xa7\x34\x87\x3c\x87\x9c\x23\x51\x80\x83\x83\xe2\x33\xf1\x0b\xfb\x6e\xce\xae\xc9\x69\x9e\xd3\x5c\x4b\x32\xc4\x51\xfe\x69\x0e\x79\xf1\x4a\x80\xb2\xe1\x65\x94\x42\xc6\xc6\x05\x40\xd8\xdf\x69\x2e\x07\x0c\x94\x72\x35\x08\xff\x13\x10\x20\x4c\x06\x01\xc2\x46\x42\x28\x51\x4a\x6a\x1c\x2a\x07\xdd\x06\x0a\x1d\x10\x57\x50\xc8\x48\xc6\x66\xa8\x4d\xdf\x02\xbc\x29\x3e\xcd\x28\xa5\x24\x23\xc6\x60\xf5\x7c\xb6\xd9\x34\xe5\x1c\xf0\x17\xe3\xa7\xc6\x11\x43\xe1\xd3\x0e\xce\x0b\x64\x34\x2b\xbe\x4a\xb9\xe6\x19\x64\x72\x02\x9c\x17\x42\x89\x9c\x74\x3e\xf6\x62\x5c\x0c\x20\x03\x31\x31\xa4\x98\x8f\x8c\x7d\x27\x23\xc6\x04\xeb\xcb\x40\xad\x9a\x5a\x32\xb5\x5e\xc6\xfa\x98\x8b\x23\x2e\xb6\x7f\x5a\x13\x60\x23\xa8\xaf\x15\xff\x38\x42\xf1\x8f\x23\x14\xfb\x28\x14\xa1\xb4\x41\xd9\x94\x8a\xaf\xc8\x85\x66\x6f\xe4\x92\xf3\x4b\x8b\xa9\x42\x10\xa8\x98\x65\xbe\xd2\x72\x15\x94\x51\x50\x31\xdd\x6c\xc5\xdd\xe1\xc9\x39\x03\x02\x72\x7e\x72\xec\x1d\x32\x7b\x54\x22\x17\x93\x20\x96\xb8\xd8\xdb\x1c\xaf\xd8\xe6\xc4\x99\xa2\xd2\xc5\x6a\x06\xa5\xce\xb9\x14\x43\xa5\x18\x76\xb1\x39\x6c\x6d\x21\x20\x54\xd5\x5b\x4f\xbe\x2b\x86\x95\x8b\x6b\x98\x51\xf9\xc6\x5d\xbc\xcd\x2d\x0d\xfc\xd0\x72\xba\x01\xb4\x29\x72\x31\xaf\xd4\xc4\xb7\xa5\xfa\xe8\x74\x97\x95\x86\xdc\xbb\xe4\x94\x54\x2d\x34\xb7\x6e\x6b\xc9\xab\x91\xd1\x5d\x62\x5e\xf2\x5a\x8d\x06\xdb\x25\xf6\xc6\x45\x37\x31\xfe\xa5\xb2\x2d\x99\xae\x43\xe8\x2d\xbd\x43\x26\x6c\xdc\x31\x6c\xc4\xd8\x6a\xcc\xce\xf2\x3b\x19\xf3\xb3\x12\x55\xeb\x27\xd7\x44\x6e\x9a\x4c\xae\x82\xf2\xc6\xc5\x64\xd3\x8e\xc3\x0e\x6a\x32\xd9\x2e\x17\x28\x85\x3a\xf4\x4f\x69\x76\xd0\x56\x86\xc7\xa6\x57\x0c\x36\x13\x9b\x4a\x9a\xbd\x36\x68\xc2\x1c\x9e\x14\x5a\xfc\x22\xad\x48\x00\xf0\x4d\xa1\x67\xc9\xb3\xa1\xf9\xce\xc8\xc1\x70\x06\x75\x1b\x5a\x38\x1f\x86\x25\x87\x0f\xd2\xbc\xe4\x5f\x33\x9f\x15\x83\x83\xe7\x38\x1f\x1c\x59\xaf\x2b\xa8\x75\x65\xce\xbb\x8c\x5c\xe7\x3f\xc0\x18\x36\x05\x35\x1a\x21\x86\xe6\x1e\x9f\x89\xe9\x2c\xc3\x09\xd3\x67\xba\xc8\xa0\xbf\xe8\x6a\xca\xfe\x2e\x22\x11\x11\x8d\x58\xc3\x16\xc4\x44\x25\x09\x9a\xb1\x41\x6e\xe2\x29\x33\x90\xdc\x20\x99\x93\x42\xa0\x7b\x97\x81\x44\x5e\x18\x20\x67\xeb\x57\xdc\xe8\x72\x38\x10\xcb\x45\x73\x73\x4b\x29\x0d\xe5\x22\x60\xef\x6a\x16\x46\xb2\x59\xce\xf9\x5a\x0c\x34\x23\x6a\xcc\xa0\x83\x35\x28\xcf\x91\x33\xc3\x99\xd2\x14\x32\x75\x7d\xa6\x67\x38\x73\xf0\x7f\x8a\x0f\xf1\xfd\x2c\xed\x07\x34\xa5\xf9\x76\x55\x20\x72\xc9\x8a\xc3\x90\xe1\x0d\x28\x9d\xbd\x17\x13\xc5\x68\x59\x30\xa3\x19\xee\xd5\x88\x96\x42\x63\x00\xd4\x18\xc2\x66\x9b\xf2\x7d\x26\x82\xbd\x4c\xbb\xde\xe2\x17\x45\x8a\x19\xbc\x07\x15\xf5\x65\x16\x31\x96\x04\x95\x65\x59\xe2\x2c\x89\x96\x50\x57\xae\x21\x5a\x0a\x55\xe2\x54\x2c\xae\x1c\xb3\x6d\x42\x6c\x44\x6a\x30\xea\xc2\xf7\x2a\xd0\xa9\x0d\xac\xa8\x0c\x64\x7c\xf1\x7f\xc9\xf4\xa4\xe5\x71\x67\x9a\xfb\xe2\xf2\xbc\xb8\x46\x4d\x98\x9a\x2a\x39\x49\x72\x7a\x32\x35\x31\xb6\x63\x90\xfb\xc4\x8d\xe3\x95\x49\x73\x84\x62\xc2\x19\xef\x67\x7c\x90\x22\x81\x28\xbf\x20\x21\x7c\xd5\x3b\x27\x0e\xb0\x82\x0c\xe0\x58\x58\xc6\xa2\xa5\x51\x91\x0d\x9a\x64\xdd\x36\x76\xe9\x6b\xd4\xdf\x6b\xcf\xe7\x44\x7c\x9a\x5d\x0f\xd0\x00\x5b\x03\x6b\xc6\x91\xb0\x9a\x59\xad\xa5\x04\xed\xf5\xa9\x1c\x5c\xf0\x80\x51\x4c\xc8\x94\x07\xf3\x61\xea\x2f\x2b\x36\x95\x5c\x2a\x7d\x9e\x43\x4d\xa1\xb3\x2a\xf6\x88\x0e\x7e\xea\x66\xd5\xa0\x08\x84\x45\x75\x94\xe9\x26\xff\x66\x6e\xc3\x5e\x65\x9c\x2a\x86\xa3\x58\x94\xc8\x1d\x6b\x0e\x49\xa0\xdb\xb4\x09\xca\xfd\x22\x6c\x69\x0f\x03\x1d\x4c\x69\x27\xb3\x09\x33\xd5\xfe\xd3\x62\x8c\xb2\xfa\x16\x49\x22\x21\x32\x34\x9e\x43\x6b\x9b\xd8\xbc\x55\x93\x4e\xd4\x20\x2b\xde\x3a\xb0\x18\xf3\x65\xfb\x86\xfb\xc9\xf2\xe6\xa6\xca\xdf\xbd\x32\xa3\x56\x85\xe5\x1a\xd4\x85\x8e\xbf\xa4\x11\xca\x1a\x0c\xdb\x98\xa5\xcd\x4d\xf5\xee\x94\x21\xaa\x69\x15\xc5\x95\x6e\xa0\x26\x47\x56\x11\x20\x56\x98\x2c\x04\x10\x07\x23\x2a\xf4\x62\xe3\xfa\x92\x88\x92\x94\x92\x20\x4b\x96\xed\x3e\x10\xea\x7b\x2b\xcc\x00\x61\xbc\x46\xfa\x3b\xde\xc0\x58\x1e\x2a\x22\x11\x22\xe8\x4e\xb2\x9d\x3d\xdb\x3c\x93\x54\xd9\x1a\x27\xb8\x37\xf0\xa6\x54\x6b\x53\x95\x28\xaf\x95\x2a\x5b\x95\x93\x27\x59\xb1\x24\xe5\x2d\xbc\x35\x7e\x67\x99\x98\x60\x39\x92\x73\x9a\x23\x92\xd9\xf0\x57\x27\x5e\x6a\xd3\x0e\x95\x8e\x89\xd0\xcc\xb2\xbf\x02\x01\x09\xf3\x90\x8b\xcd\x50\xb3\x2e\x17\x50\x17\x6b\xdf\x19\x9e\x48\x60\xc8\xe1\x04\x5a\x31\xec\x9a\x92\x49\xf8\xb0\x11\xb3\x72\x09\x50\x79\xf4\x3c\x93\x9f\xe4\xcc\xf4\x85\xd7\xb8\x68\xf5\xfa\x87\x00\x93\x69\xf7\x32\x8d\x07\xef\x60\x76\x37\x18\xc6\xfe\x12\x74\xaf\xdf\x03\x48\x26\xef\xe3\x74\x1e\x8f\x20\xfe\xd7\x70\x3c\xb8\x1d\xcc\x93\xe9\x04\x6e\x07\xe9\xbb\x30\xaa\xea\xf5\x23\x80\x61\x3c\x99\xc3\x2c\xb9\x9e\x08\xe6\xd2\x71\xa2\x0c\x1a\x3a\xe5\xa0\x01\x00\x04\xad\xf1\xb2\x71\xaf\x7f\x04\x70\x37\x5d\x4c\x46\x86\x18\xa3\x4e\xf1\x9a\xb2\xf9\xa6\x3a\x4b\x36\x52\xb9\x36\x7d\x6d\xe5\x72\x5c\xe4\x31\xc0\x70\x91\xa6\xf1\x64\xf8\x41\x48\x25\xbc\x72\xcc\xeb\xb3\xc2\x4c\xf5\x1a\x8a\x72\x45\xf1\x57\x56\xac\x20\xa5\x05\xe9\xf5\x4f\x00\x3e\xc4\x13\x39\x40\x2a\x7d\xb8\xf6\x70\x39\xfb\x2c\x2f\xbd\xc3\x82\xe4\x5e\xbf\x0f\x70\x99\x4e\xdf\xc5\x13\xb8\x1c\xa4\x96\x81\xd3\xdc\x36\x79\x66\xe0\xbd\xfe\x29\xc0\x2c\x1e\xb2\xf5\x31\x26\x09\x44\x38\xc0\xa6\x98\xc8\x00\x38\x83\x4c\x14\x8e\xb2\xc2\x5b\xb2\x59\x12\x86\xdf\xeb\x9f\x01\x8c\x92\x41\x9c\xc6\xb3\x64\x86\xef\x52\x56\x96\x92\xb1\xa8\xfb\xce\xd0\xe4\x1c\x60\x38\xbd\xfb\x90\x26\xd7\x37\xf3\xd2\xda\x51\xa3\x38\xcb\xd3\x39\x90\x4e\xb1\x88\x0a\x15\xcd\xb7\xd9\x17\x75\xb1\x89\xe6\xda\xcb\xf3\x94\x8f\x01\x9d\x1e\x02\x5c\xc5\xb7\xc9\x24\x99\xc4\x30\x4d\x47\xc9\x64\x30\x86\x64\x32\x4a\x86\x83\xf9\x34\x6d\x69\x36\x56\x96\xad\x8c\x2a\x2f\x97\xdb\x4c\xcf\xdf\x3b\xed\x01\x8c\xe3\xab\x79\xf7\x6e\x9a\x4c\xe6\xc9\xe4\x1a\x46\xd3\xc5\xe5\x38\x86\xc1\xe4\x7a\x1c\xc3\x3f\x17\xd3\x79\xc9\x22\xa4\x93\x67\x7a\xc8\x3a\xa0\xaa\x04\xaa\x5a\x60\x99\x5f\x7a\xa7\x11\xb0\xae\x90\x9a\x1f\xc0\xb2\x02\x28\xc5\x0b\x72\xb8\xd6\x84\x9f\x1e\x01\xcc\xa6\x57\x73\xb8\xf9\x70\x77\x13\x4f\x4a\x11\xb3\x29\x15\xea\x14\x3f\x06\x48\xe3\xeb\x64\x36\x8f\xd3\x78\x14\xb6\x72\x39\xf3\x7b\x62\xe5\x58\xc0\x42\xd4\xca\xe5\x34\xa7\x19\xbe\x72\x27\x00\xb7\x83\x61\x3a\x9d\x04\xf4\x1e\xb0\x77\x6a\xc8\x7d\x80\x51\x7c\x9d\xc6\xb1\x1a\xae\x8e\xfd\x94\x43\xd6\xfe\x98\x80\x47\xf5\x53\x80\xbb\xf1\x62\xd6\xbd\x4d\x26\x8b\x59\xc9\x6f\xf1\xe9\x43\x3a\x30\xb4\xd0\xcf\x19\xb7\x92\x78\x06\x30\x5b\xdc\xc5\xe9\x6c\x98\x26\x77\x73\x98\xff\x36\x2d\x95\xcb\xa0\x83\xc6\x03\x65\x7e\xed\x9d\x9e\x5b\x52\x6e\xd2\x38\x36\xe3\x0a\x2a\x8b\xbb\x56\xd8\x67\xc9\x39\x3b\x04\x18\x0c\x17\xf3\x18\x06\xc3\xc2\x33\xb7\x44\xbc\x41\xf8\xd7\xcd\x14\xd3\x48\x34\x7b\x67\x3d\x80\xdb\x64\x98\x4e\x91\x6d\x5a\x5d\x35\x3c\x50\x86\x96\x2b\x61\x11\xc0\x5d\x32\x1e\xa6\xd3\xdf\x8c\xc5\xd2\x8c\x06\x20\x6b\x6b\x6d\x29\x91\xc5\xb3\x32\xcd\x61\xfb\xc9\xf6\xe8\x67\x47\xc5\x08\x47\xa3\x71\x0c\xa3\xe9\xbc\xa5\x47\xa7\xcb\xf6\x88\x5a\x05\x0d\xc4\xa3\x64\x3c\x1e\xb4\xb4\xa5\xd8\x3f\x99\x8c\x0e\xed\xb0\x2b\x4e\xca\xeb\x30\x9d\xc4\xf2\x1b\xc4\x65\xc1\x12\x56\xbf\xd8\xea\xb3\xe1\x62\xec\xf7\x52\xd2\x49\x71\xa2\x11\x6b\x98\xfb\xf6\xc4\xd9\x29\x00\xf3\xad\x81\x0e\x4a\x15\xd3\x32\xb3\x81\xa0\x5a\x08\xb2\x89\x60\x86\x2a\x0c\xe6\x0c\xe0\xfd\x62\x7c\x3d\x48\xe1\x2a\x1d\x70\x8e\x99\x4e\x0a\xf1\x83\x74\x1e\xa7\x72\x0b\xc8\x24\xb3\x70\x05\xcc\x91\xcb\x34\x83\xb0\x8f\x44\x99\x86\x00\x29\xf6\x3c\x57\x88\x17\xfe\xcb\x8d\x2c\x85\x7a\x8e\xa3\xde\x0c\xc6\x57\x26\xa4\x46\x94\x59\x53\xce\xfb\x57\x1d\x89\xa7\x4b\x4c\x94\x94\x03\x6e\x05\x76\x7e\xe8\x82\x31\x0b\x93\x4a\xce\x54\x2d\x52\xe9\xc9\x4c\xce\xd0\x94\x99\x5e\x49\x57\x5a\xd6\xd6\xaf\xaf\x18\x84\x19\x70\xfd\x73\x11\xcf\xca\xdc\x22\x3a\x64\xa2\x69\xa3\x5a\xee\xbc\x6a\x96\x59\x79\x44\xef\x3c\x02\x18\x0f\xe6\xc9\x04\x86\x83\xbb\x64\x3e\x18\xc3\x38\x9e\xcf\xe3\x14\x06\xf0\x5b\x32\xbf\x81\xeb\x74\xf0\x3e\x6e\x99\x5d\xbf\x82\x9a\xa4\x6b\x0f\x0a\xdb\x7b\xe7\x47\xd5\x18\xcc\xcf\xc8\x48\x44\x4a\x26\x0d\x31\x8e\xab\x31\x86\x49\x3a\x5c\xdc\x5e\x8d\xe3\x7f\x31\xa1\x44\xe6\x37\x84\x66\x79\x33\xa0\x93\x6a\xa0\x79\x32\x1e\x31\x65\x8a\xcc\x48\x60\x64\x4d\x95\xe9\x57\x63\xf8\x02\xaf\x86\xf9\x54\xef\xfc\xb4\x1a\x27\x2d\x5c\xc5\xe0\x72\xfa\x3e\x2e\x71\x1b\xb4\x77\x9d\x86\x0a\x9d\xf9\x80\xb8\x64\xee\xd6\xf5\x09\x00\xd0\xfc\xc8\xcc\x46\x46\xbf\xac\xd9\x28\x43\xf6\x02\xc9\x61\x52\x85\x78\xee\x41\x1c\x8a\xfd\xa0\xbd\xf9\x0b\xda\x7f\x9d\xc2\xba\x2e\x5a\xd1\xe1\xa1\x07\x25\xf6\x59\x4f\x11\xd6\x61\x2d\x3f\x6f\x5c\x10\x1d\xf6\xaa\x31\x5c\xeb\x29\x5c\x73\x43\x0c\x9f\x17\x88\xc3\xac\x27\x1c\xc8\xe7\x0a\xe2\xa0\x9d\x1d\x8e\xe3\x73\x07\x49\x79\x61\xe4\x94\x65\x54\xf4\x02\xcb\x5d\x49\xbb\x1b\x19\x1d\xfa\xac\x3f\xb1\x16\x43\x6e\x29\x59\xfc\xab\x93\xeb\xb3\xf8\x04\x59\x00\x22\x5b\x0b\x34\xcb\x83\x84\xfb\xcc\x3c\x09\x73\x27\x0c\x80\xdb\xa6\x6a\xdb\xe4\xb2\xcd\x66\x4e\xba\xcf\xca\xe3\xf9\x8d\xca\x0b\x54\x5e\x0f\xaa\x3b\x57\x6a\x93\xeb\x72\x3f\x72\xf4\x85\xc3\xf8\x4c\x7b\x82\x78\x60\xe5\x80\xd9\x9e\x51\x1d\xd4\x83\x3f\x65\xf8\x95\x95\xbb\x00\x51\xcf\x67\xd2\xd3\x7a\x42\x0c\x2c\x45\x45\x3d\x9f\x49\x4f\xeb\x09\x31\x18\xc3\x67\xd2\xd3\x40\x42\x0c\x06\xf2\x99\xf4\xb4\x9e\x10\x83\x31\x7c\xe6\x3c\x0d\xdb\xc1\xc1\x38\x45\x9a\xb9\x18\xcf\x93\xbb\x71\x11\x6d\x97\xaa\x24\xf2\xab\x05\xf3\xf0\xe3\x84\xe2\x28\x61\xa6\x8f\x11\x32\x19\x3e\x53\x16\x63\x9d\xcd\xd3\xe9\xbb\xd8\x26\x1e\x42\x64\x50\x2a\xd3\x03\x42\x64\xdb\x8b\x20\x03\xf5\x99\xf4\x22\x80\x78\xc2\x6a\x9e\x51\xcf\x67\xce\x8b\x00\xe2\x09\xc5\xf0\xd9\xf2\xa2\x66\x97\x42\x96\x37\x02\x8a\x7c\x66\xbd\xa8\xde\x41\x0d\x0b\xc5\x51\xe4\x33\xed\x0f\x9e\x49\xa3\x6c\xd2\x2a\x1a\xe5\x4e\xe5\x2f\x8a\x7c\xa6\x3d\xbf\x99\xa6\x13\xa7\xb5\xa8\x0f\x09\xe9\x16\xa8\x51\xf0\xb5\x85\x2b\x73\x9e\xdd\x0e\xc6\x4a\xf4\xec\x66\x90\xde\xc1\xac\xf5\xc2\x92\x72\x14\x1d\xa3\x62\x2b\xb3\x8c\x46\x6d\xea\x28\x3a\xa9\x42\xc0\x5d\x6a\x43\x84\x7e\x15\x42\x88\x43\x0d\x83\x39\xad\x82\xc1\xdd\x69\x43\x45\xce\xaa\x10\x02\x9c\x69\x18\xca\x79\x15\x8a\x9d\x5b\xc8\x55\x69\xb7\x3b\xfa\x64\xe7\xab\x00\x98\xa3\x43\x1c\x26\x2e\x25\xef\x62\x73\x99\x2e\xf7\x4f\x50\x1b\xb8\x00\x30\x0e\x06\x38\xad\x00\x81\xd4\x43\x91\xdc\x8c\x02\xa0\xc1\x91\x07\x99\x4b\x1c\x45\xa8\x74\x2c\x93\x50\x51\xc7\x45\xd8\xd1\x86\xe8\x08\x37\x6a\x2c\x83\x50\xd1\x46\xb0\x6c\xdc\xb2\xbd\x99\x83\x32\x8a\x60\x00\xdc\xb0\xc3\x32\x06\x93\xfa\xe5\xe1\x35\xd1\x21\xb0\xb6\x11\x6e\xdc\xe5\x7c\x81\x8a\x22\x4a\x56\xd1\xcc\x8a\x8e\x70\xfb\xb5\x32\x04\xa3\x9a\xea\x97\x84\xdb\x69\x40\x4e\x00\xd6\xb1\xca\x52\x31\x89\x89\xc6\x8d\x33\x30\x23\x80\xb0\xd3\x71\xd1\x31\x6e\x9b\x22\x1f\x28\xc2\x28\x7e\x81\x6c\xab\x03\x2d\x3e\x92\x7d\x34\xd5\x82\x71\x69\xf6\x18\x37\x45\x2c\x03\x28\x87\x9c\xa1\xc7\x43\xa2\x63\xdc\x1c\x9b\x65\x01\x55\x9d\xdd\xe8\x18\x37\xca\x66\x39\x40\x35\x02\x6e\x9a\x2f\xc8\x00\xaa\x61\x70\x03\x6d\x16\xff\x57\x23\xe0\xc6\xd9\x3c\xfa\xaf\x46\x39\x05\x18\x25\xef\x93\x59\x39\xee\x57\x21\x92\x0a\xa5\x0b\xd9\x76\xe9\xa1\x90\x6c\xcb\xc3\xcd\xd7\xc9\x03\xca\x84\xa1\x4f\x22\x49\x9b\x86\x36\x21\x72\xd8\x76\x32\xc0\x81\x70\x63\x7e\x79\x2e\xe0\x90\xec\x09\x6e\xc8\x2f\xcf\x04\x5c\x04\xdc\xa2\x7f\x32\x0f\x70\x61\x70\xb3\xfe\xb9\x2c\xc0\x45\xc1\x4d\xdb\x97\x03\x04\x4c\x97\x75\x60\x28\x3a\xc1\x4d\x1b\xcf\x00\x1a\x1d\x4f\x8b\x4e\x70\x73\xfe\xf0\x73\x53\x64\x29\x70\xf8\x5f\x87\x3d\x6f\xf5\x54\x84\x88\xaa\xe1\x6b\x84\x6d\xda\x6c\xc3\x4a\xcf\x0c\x07\xdf\x5a\x0e\x8a\x0a\xd7\x80\x50\xa0\xa4\x41\xac\xcb\x50\x6a\x3a\x29\x97\x69\xcc\x4b\x8e\xea\x5c\x66\x96\x89\xb4\xbf\x91\x32\xf8\xd6\xb2\x40\x8c\x63\x72\xa6\x47\x09\xd6\xa5\xa6\x9b\x32\xbd\x9e\x4e\xe2\x77\x1e\xf7\x58\xab\x4c\x9b\xb6\x25\x4e\x65\xce\x86\xa1\x84\x9f\x5e\x36\x50\x7c\x15\x99\xa1\x6d\x8f\x2a\x00\xa6\x46\x04\xac\xcf\x3d\xfa\x6f\x04\x62\x30\x78\xdc\xe7\x80\xe8\x4d\xa6\xf6\x58\xdd\x79\x68\x26\xde\x57\x8e\x19\x62\x2e\x52\x9e\x3f\x07\x46\x23\x59\x53\x55\x70\x42\xc1\x91\x24\x10\xd1\x09\x49\x90\x3e\x83\x6a\x7d\x46\xd3\xb9\x91\x1f\xfe\xfe\x87\xc9\x90\x4d\xd5\xb9\xac\x52\xa7\x04\x64\xe2\x34\xd2\x66\x58\xb3\x3a\x03\xe1\x64\xb2\x4c\xda\x3c\x35\xd2\xf6\x60\x4d\x46\x95\x0b\x63\x82\x48\x8c\x46\x5a\xc4\x1e\x2d\x46\x3e\x2d\x28\x55\x81\xb5\x5b\x63\xc5\x6e\xb7\x61\x30\x57\xa8\x16\x16\x48\x21\xf8\x85\xb7\x00\x14\x20\xde\x42\xfe\x08\xad\xc3\xaa\xde\xbc\xba\xdb\x90\xd5\xb5\xc4\x3d\xda\x39\xcd\x89\xc4\xcb\x29\xda\x97\x60\x98\x38\xd3\x38\x88\xd0\xed\x76\xf3\x97\x6b\x56\xd3\xad\xab\xe2\xcd\xd0\x0e\x1a\xc3\xa9\xac\x1a\x94\x79\x93\x3a\xbc\x19\x92\x75\x33\x14\x1f\xd7\xc4\xb5\xbc\xd9\x48\x99\xca\x2a\x82\x04\xc9\xb2\x22\xba\xbc\x10\x6b\xe4\x24\x29\xb5\xba\xf8\x98\x26\xae\xf2\x69\xbe\xdb\x40\xab\xf5\xc1\xd9\xc6\x07\x74\x61\x52\x4f\x40\xd5\x85\x41\x78\xfb\x79\x5e\x7e\x0e\x57\xa5\x4d\x15\x3f\xf7\x70\xb6\x41\x51\x6c\xdf\xec\xd7\x43\xf3\x7f\xcf\xc7\x34\x71\x95\x57\x6b\xbe\x22\x38\xcb\x60\x20\xfc\xda\x2c\xb8\xc8\xc6\xc4\xfb\x18\xe6\x1a\x63\x65\x4a\x32\xec\x50\x03\x05\xb7\x85\x81\xa9\x82\xd3\x0c\x8a\x64\x46\x1a\x54\x1e\x12\x0b\xba\x07\x8a\x21\xf9\x18\xe7\xda\x0a\x68\x51\xb6\x0c\x57\x08\x67\x1c\x0b\xc4\x28\x0a\x34\xbd\x9f\xab\x00\xf1\xf6\x98\xae\x11\xa3\xa4\xbf\xff\xf1\x13\xfa\x44\x38\xd1\x60\x40\x85\x97\xf9\x09\x9d\x7c\x5c\x73\x1d\x7a\x8e\xa6\x42\xa3\x0e\xe5\xb5\x6f\x06\x84\x93\x8d\x0b\x63\xba\x81\x40\x7d\x3a\x9d\x4e\x47\xc2\xf8\xd8\xe6\x06\xd9\xdb\x2c\x6b\xe1\xc3\x27\x90\x39\x47\x56\xab\x6e\x74\x67\x58\x38\xe5\xa0\x48\xea\x7e\x51\xc8\x55\xd9\x27\xac\x52\xc9\x90\x7c\xbc\x73\x63\x45\x1e\xe6\x21\xdc\x7f\x34\xbb\x75\x9f\xe1\xe0\xa4\x83\xa1\xf0\xa0\xf0\x1f\x4d\x6f\x6d\x64\x28\x3e\xde\x49\xca\x15\x45\xc2\x3d\x69\xe8\xa3\x04\x98\xe8\xca\xb2\xb7\x2a\x55\x02\x6f\xf2\x57\xd4\xe6\x99\x30\x1f\xaf\x24\xe5\x08\x89\xa8\x7e\x6b\x83\x81\xe2\x64\x62\x49\x96\x91\x97\x44\xa8\x2b\xfb\x33\xc9\x3e\x1e\x49\x1c\x67\xd8\x78\xd0\x38\x6d\x58\x82\xf9\xd1\xe2\xca\xce\x07\x13\xe6\x63\x86\xc4\x8a\x0b\x42\x9f\x28\x61\x44\x1c\x11\x4e\x07\x8e\x64\x90\xc5\x2d\xb1\x6f\x8d\x17\x9a\x53\x2e\x54\xca\x3c\xf2\x79\xff\xc4\x75\xca\x94\x47\x64\x4d\xe6\xd6\xd3\x7c\x1c\x4d\xe7\xe3\x78\x36\x83\xa4\xa5\x42\x30\xa8\x99\xd8\x23\xd7\xa5\x27\xd7\x83\xf9\x22\x8d\x21\xf9\x55\xcc\x27\xe8\x29\xd5\x77\xb7\x83\x7e\xab\x13\xbc\xdc\xad\x47\x33\x10\xdb\x9d\x97\x21\x6a\x0a\x87\x7a\xe4\xe2\x1e\x65\x50\x77\x29\x33\xe1\x3e\x27\xfe\x2b\x1e\xa0\xa8\x08\x85\x15\x29\xf2\x0b\xf5\x04\x11\x83\x9a\x54\x58\x5f\xbe\x25\x54\x22\xe2\xae\x1c\xc7\x23\x99\xac\x87\xf0\x91\x4b\x0d\xf8\xab\x75\xd7\x35\x93\xee\x73\xdf\xef\x6c\xf6\x33\x1f\xfb\x44\xe4\x83\x4c\x5e\x81\x4c\xc1\x0f\xc4\x7d\x36\xfc\x2e\x6d\xa1\x47\xc1\xb2\x12\x08\xf7\xdf\x18\x8c\x44\x09\xb9\x11\xbc\xa3\x01\xf0\x96\xc7\xbb\xb4\xd4\x17\xd7\x77\x2e\xe7\x44\x86\xbe\xbe\xdb\xcb\x99\x54\xdf\x29\xa1\xb1\xd5\x53\x05\x91\xbd\xe5\xec\xfc\x94\xd4\xc0\x39\x0b\xe3\x09\xe0\x8f\x06\xe8\xe0\x2d\x10\xe2\x38\x2e\xdc\xca\x2e\xab\xc7\x6c\x4d\xb6\x33\xd6\xea\x51\x9b\x6b\x3a\xac\x1a\x75\x19\x46\x8a\xc6\x5e\x55\xf7\xd6\x58\xcd\x51\x8d\x0e\x22\xa7\xa1\x32\xa9\x29\x84\x65\xcd\xe7\x3d\xae\xd4\x20\x10\x44\xbe\x43\x0e\x35\x31\x90\xab\x6a\x5d\x4a\xf7\x04\x79\x96\x44\x78\xed\x5a\x85\x3c\x6d\x6f\x2f\x92\x02\x52\x38\xa5\x4f\x9c\xbb\xf9\x19\x86\xef\xa0\xd9\xd8\x0e\xc6\x34\x04\x11\xcf\x78\xc9\x09\x11\x22\x09\x3b\x42\x5f\xa9\x0b\xde\x30\x73\x50\x0c\xd6\x61\xab\x44\x08\x66\x15\xc7\xbe\xc3\xa2\x13\xcb\x92\xe5\x0d\x2b\xcd\x1f\x80\xc4\x60\xf0\xee\xd8\xc4\xad\xf7\x4b\x87\x24\x0f\x6a\x37\x08\x53\x8f\x7d\x67\xc1\x27\x98\xe9\x35\xd2\xa6\x43\x3b\x26\x10\xde\xfd\x76\x60\x2c\x07\x5b\xaf\x8f\x05\xe3\x3b\x53\x3a\xb1\x0c\x31\x23\x2f\x7d\x38\x15\x83\xc1\x29\xc2\x02\x61\xa5\x18\xae\x4b\x86\x3c\x65\xb0\x76\x6d\xf0\x38\x7f\x02\x77\x69\x3c\x8c\x47\xf1\x08\x2e\x3f\xc0\xe0\x6e\x5a\xec\xdf\xbb\x1b\xf3\xa8\xd8\x85\x7c\x6a\x18\x7f\xdf\x86\x4e\x81\x2b\x0e\xd7\x18\x41\x10\xf6\x54\x12\x09\xee\xad\x37\x4d\xae\x1b\xee\x06\x90\xf2\xb5\x53\x3e\xf6\x94\x99\x84\xec\xba\x85\x57\x12\x8d\xbb\x24\xa9\x1a\xb9\x2f\x37\x98\x56\x54\x96\xc1\x3c\x2c\x06\x22\x0b\xf7\x1f\xb3\x60\x38\x78\xa2\xf0\x9f\x46\xf1\x65\x10\x53\x2b\xd3\x01\xf9\x40\x2b\xa0\x5d\xe4\x01\x66\x15\xe7\x7d\x19\x0c\x9e\x4c\xfc\x67\x41\x4e\x6a\x6e\x4b\x90\x37\x74\x1a\x5d\x46\x7d\x96\xcf\xa9\xc9\xd7\xa2\xe1\xc9\xc6\xff\x0d\x96\x3f\x1d\x99\xca\xf6\x8c\x55\xf2\x35\x9e\x45\xc3\x69\xb2\x14\xc8\xe7\xbc\x19\xc1\xe0\x72\xd5\x9a\xb0\x8d\xd4\x39\xa2\x51\x06\x2d\x3b\x04\xf1\x60\xe1\xd2\x20\x8c\xba\x2d\xdb\x88\xa5\xea\x2d\xb1\x6a\xb8\x65\x68\x5f\xfa\x92\xda\x24\x48\xb1\xde\x5a\xc8\x13\xf1\x18\x0c\x9e\xb3\xa4\x48\xd3\x5b\x71\xa0\xf0\x1c\xc6\x5d\x70\xde\x00\xe4\xc4\x97\xb4\xa4\x0e\xfd\x35\x7d\xb2\x5f\x99\x97\x4e\xf0\xa4\xc5\x81\x29\xb3\x5f\x9d\x26\x1d\x5d\x7a\x3c\xf1\xd5\x9b\x52\x9b\xf8\x7e\x6e\x3d\x70\x4e\x4a\x11\xe2\x53\xbc\xd7\x64\x3d\x7c\xac\x33\xf3\xec\xaa\xe6\x8f\x4a\x64\x30\x38\xfd\xcc\xdc\x53\x0e\x17\x02\x2c\xe0\x69\x53\x4c\xb0\x8f\x7b\x66\x68\x65\x36\x7b\xe9\xf3\x1e\x19\x16\xce\x3f\x18\x92\xdc\x54\x84\x66\x34\x54\x13\x1f\xef\xcc\xbc\x96\xd1\x40\x0d\x23\xf5\x3b\xc1\x99\xc7\x81\x01\xdd\x9d\x83\x4a\x1d\x74\x94\xd1\xf7\xf1\xcd\xcc\x63\x13\x2f\x5b\x88\x3e\x4e\x34\x33\xd7\x26\x84\x49\x54\x8f\xdf\x10\xec\xbd\x47\x06\x5b\x03\xfb\x49\x1d\x50\x73\xbe\xd9\x58\x83\x3e\xde\xb7\x70\x61\x74\x36\xcc\x5f\x7d\x8f\x53\x33\xd6\xc0\xc7\x13\x73\x6b\x0d\x5e\xfe\xfc\x52\x06\x83\xf3\xc4\xdc\x3d\x8d\x11\x34\x7e\x43\xb0\x8f\x21\xe6\x4e\x8a\x5a\xa5\x40\x37\xef\xe6\xdd\x4a\x05\x70\x82\x70\x50\x3c\x2b\xd0\xad\xd0\xa0\xe6\x16\x38\xdd\x30\x50\x26\x46\xb2\x66\xb7\xc0\x31\x98\xca\xf3\xbb\x2f\x00\xc1\x4e\x8d\xf4\x7d\xfc\xb0\xa8\x3a\x35\x02\xb2\x4e\x1f\xae\x0c\x4e\x10\x15\x28\xfa\x29\x5a\x0d\xb4\xf1\xb1\xc5\xc2\x6e\x36\xa8\x18\x2e\xab\xbb\x39\x11\x83\xc1\x89\xe2\xe5\x20\xa8\x2e\x3e\xbe\x58\xa0\xb7\x4a\x4d\x95\x37\xaf\x79\xca\x20\xba\x3a\x38\x65\xd4\x22\xd1\xc6\x5a\x9d\xd6\xdd\x71\x69\x65\x11\xdd\x2e\x74\xbb\x7c\xd7\x11\xd2\x78\xd7\x9d\x56\x1e\x1f\xc7\x32\x16\x15\x9f\x54\x62\xa1\x9a\xf9\xb8\x65\xe1\xf6\x8f\x1a\x3f\x1b\xda\xe8\x52\x9d\xe2\xd4\x62\xa3\x18\x7e\x21\xfc\xb9\x9b\xba\x6f\x75\xea\x63\x99\xdf\xdc\x68\x88\xd7\x12\x49\x66\x3c\x5e\x13\x08\xc9\xb2\x9f\x7f\xce\x34\x1b\x09\x4e\x44\xc8\x38\x40\x0e\x03\xc4\x43\x2b\x8b\x41\xbc\xf8\x89\x9f\x0c\xdb\xc7\x55\x1f\x7c\x67\x5e\x39\x74\x01\x0c\x0d\x6e\xde\x65\x58\x38\x5f\x55\x22\x71\x15\x2b\xd7\x16\x39\xbd\x71\xea\xe3\x2f\xe4\x30\xff\xef\x7f\x00\x2f\x3c\xf3\x45\x6e\x72\x47\x32\x83\xf2\xf5\x68\x3e\xda\xf7\xbd\x11\xf9\xb4\xaa\xf0\x47\x7b\x33\x00\xbc\x3b\xf3\xd1\x49\x3c\xc0\x48\x3c\xea\x9e\xa3\xca\x04\xfb\x3a\x35\x1f\x91\xc6\x2d\xfd\xfd\x8f\xc6\x0f\x26\x67\x20\x78\x97\x06\x81\x90\xa7\xf5\x02\x9f\x04\xcb\x84\xfb\xfa\x34\x1f\xdd\x60\xfd\x65\xa3\xc7\x3b\x34\x1f\xbd\x61\x7a\xf0\xc8\x71\x3a\x1a\x4f\x27\xd7\xfc\x66\x73\x30\xff\xc3\x40\x32\x5e\x93\x2d\xab\x6e\x4d\x68\x1b\x0d\x4f\x41\x8e\x4e\x0e\x2e\x53\x4f\xf4\xc3\x5f\xf8\x85\xa3\x33\x90\xec\xce\x4e\x9b\x72\x1b\x53\xbc\xf3\x8b\xf7\x55\x5c\x7e\x0e\xe6\x9a\x32\xb3\xe2\x5f\x72\x7e\x88\x0b\x2e\xc1\x74\xe8\x9e\xb1\xa9\x17\x5d\xbc\x88\x46\x23\x28\xd1\x9b\xf1\xbc\x3e\x90\x8f\x49\x52\xd9\x51\xc5\xb8\xff\x37\x00\x00\xff\xff\x51\x0d\xcb\x5c\xc3\x6e\x00\x00")

func data2dFlfBytes() ([]byte, error) {
	return bindataRead(
		_data2dFlf,
		"2d.flf",
	)
}

func data2dFlf() (*asset, error) {
	bytes, err := data2dFlfBytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "2d.flf", size: 28355, mode: os.FileMode(420), modTime: time.Unix(1550434995, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}

var _dataQuindarToneMp3 = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x9a\x79\x5c\x53\xd7\xb6\xc7\x57\x26\x08\x61\x30\x02\x02\x02\xd5\x18\x44\x11\x21\x03\x84\x90\x44\x22\x81\x24\x08\x42\x24\x0c\x75\xa8\xd6\x26\x24\x07\x48\x21\x83\x19\x18\xf4\xaa\xb1\xb6\x8a\x56\x5b\x40\xc5\x58\xd0\x82\x28\xe2\x40\x8b\x5a\x9f\xd3\x6d\x8d\x62\x1d\x0a\x58\x54\x54\x6c\xb5\xc5\xb6\x58\xb1\xad\xca\xd5\x56\xab\x96\xbc\x4f\x12\x50\x69\x7b\xfb\xec\xbb\xbe\x0f\xca\xe3\xf7\x4f\x3c\x39\x7b\xaf\xe5\x6f\x9f\x6f\xd6\x3a\xfb\x1c\x12\x04\xe1\x18\x00\x80\x09\x1e\xe2\xd4\x84\xa9\x00\xc3\x96\x01\x4c\x17\x89\x21\x2a\xba\x40\x23\x95\xe5\x20\x7a\x52\x06\x92\xa5\x50\x71\xc9\x37\x0f\x1e\x22\x93\x14\x72\x2e\x79\x5a\x84\x88\x26\xd2\xf0\x91\x6c\x45\xfc\x5c\x2d\x92\x36\x77\x4a\xba\x6c\x6e\x8e\x8c\x2d\x27\x47\x4f\x24\x44\x15\x70\x0a\x94\x1a\x25\xa2\x97\x92\x0a\x94\xb9\x2a\x1d\xa7\x80\x4b\x96\xca\xd5\x19\x08\x47\xa5\xe3\x58\xbf\xa6\x92\x49\xb6\x21\xfa\x1c\x2e\x39\xc6\x7a\x82\x34\x5d\x24\x26\xf1\xd5\x5a\x84\x14\x41\x09\x0f\x95\xd1\xe8\x74\x12\x93\x49\xa1\x33\x22\x98\x4c\x7a\x08\x29\x8c\x46\x0f\xa3\xd2\xc2\xa8\x34\x66\x28\x9d\xc1\x89\x60\x72\xc2\x22\x49\x3d\x22\x4f\x24\x90\xa2\xb4\xf2\x4c\x4e\xaa\x20\xae\x27\x99\x56\x9e\xc9\x25\x67\xeb\xf5\x1a\x0e\x95\x9a\x9f\x9f\x4f\xc9\x0f\xa7\xa8\xb5\x59\x54\x3a\x9b\xcd\xb6\x06\x09\x0b\x0b\xd5\xca\x33\x43\x75\x85\x2a\xbd\xb4\x20\x54\xa5\x0b\xb0\x86\xb0\xc7\x10\x20\x3a\x99\x56\xa1\xd1\x2b\xd4\x2a\x92\xf5\x58\x9a\xa1\x36\xe8\xb9\x64\x32\xc1\x9a\xa9\xc7\x89\x52\xf3\x28\xb8\x4a\x47\xb1\xb9\xa2\xc8\xd4\x4a\x6a\x81\x54\x43\xa5\x53\x68\xd4\xdf\x0d\x16\x89\xfe\x7a\xb8\x52\xd9\x67\x86\x4e\x2f\xcc\xd3\xff\xf5\x0c\x5d\x7a\xa1\x06\xa1\xa6\x22\x3a\xb5\x41\x2b\x43\x84\x79\x88\x4a\x1f\xd0\x37\x44\x2a\x92\xf9\x77\x42\xa4\x22\x99\x7d\x02\xc8\x65\x8f\x66\x6b\x0c\xda\x5c\xdb\xe2\xc9\x65\x54\x24\x17\x51\x22\x2a\xbd\x8e\x4a\xa7\xd0\x7f\xef\x52\xf0\xef\x5c\x2a\xed\x09\x05\x85\x2a\xa9\x52\x21\x13\x21\x72\x85\xd4\x3e\xb7\x40\xa9\xe1\x88\x10\xbd\x54\x2e\xd5\x4b\x05\x52\x3d\xc2\x25\x87\xd1\xe8\xe1\xa1\x34\x76\x68\x18\x2d\x9d\xce\xe6\x84\xb3\x38\x8c\x88\xf1\x34\x16\x87\x46\x7b\x3c\x5e\x2d\x57\x64\x16\x3e\xe5\x68\x91\x88\x93\xa0\xd2\xe9\xa5\x2a\x19\x92\x20\xe0\x92\x0b\x94\x1a\x8a\x42\x21\xe7\x08\x23\x19\x91\x7c\x66\x98\x50\xc8\x0e\xa3\x0b\xc3\xe9\xf4\x58\x76\x58\x04\x8b\xc9\xa7\x31\xe3\x62\xe2\x62\xf8\x8c\xb8\x27\xa6\x0b\xd4\x32\x83\xd5\x71\xef\x74\xf9\xdf\x9b\x9e\xac\x55\x64\x29\x54\xd2\xdc\x3f\x0b\x13\x16\x2b\x88\x89\x65\xb1\xf8\x7c\x96\x3d\x0c\x5b\x20\xe0\x0b\x05\x71\x34\x41\x78\x8c\x20\x2e\x86\x16\x6b\x0b\x23\x97\x71\x32\xd5\x5a\xa5\x54\xcf\x25\x4b\x0d\x72\x85\x9a\xaa\xd4\x20\x59\x36\x58\x49\x51\xf6\x14\xf1\x0a\x9d\x5e\xad\x2d\xb4\x7d\x65\x27\x38\x0d\x99\x63\x3f\xb2\x1f\xe6\x2a\xec\x07\x24\x1b\x57\x1c\xa9\xcc\x8a\x36\x97\xac\x93\xe6\x21\x72\x72\x9f\x53\x8a\x3f\x59\xab\xa7\xfa\x5f\x3e\x8a\x90\x9f\x8d\xa8\xfa\x5e\x97\x08\x0e\x83\xc1\x89\x60\x3d\x71\x5d\x1e\x0d\xd6\xa9\x33\xf5\xf9\x52\x2d\x12\x93\x85\xa8\xf4\xbd\x35\x20\xc6\x20\x57\xd8\x7e\x7b\xfc\x34\x26\x29\x68\x9a\x42\x25\x57\xe7\xeb\xc6\xf5\x9d\x28\xcb\x96\xaa\xb2\x10\x39\x97\x4c\x55\xf6\xd0\x43\xa6\x3e\x43\xc7\xfc\xe7\xd9\xf1\xb3\x74\x2a\x0c\x7f\x0a\x90\xff\xc2\xa9\xed\x37\x17\xce\x78\x81\xae\xad\x90\xf1\x3c\x3b\x7e\xa6\x4e\x23\x9e\x81\xd3\x3e\xf5\xb4\xdf\xae\xad\x4c\xad\xca\x43\xb4\xfa\xdf\xbb\xd6\x48\xb5\x52\x25\xa2\x47\xb4\x3a\x2e\x39\x53\xab\x56\x92\xec\x05\x52\x9d\x95\x35\x81\x24\x53\xcb\x11\x19\x77\xcc\x1c\x83\x5a\x3f\x21\x4f\xad\xcd\x50\xe8\xec\xff\x26\xe9\xd5\xa4\x27\xea\xe8\xb3\x5c\x6f\xe6\x73\xb1\xde\x03\x67\x3d\x9f\xa6\xc9\xf6\x1b\xbf\xbd\x4e\xa3\xa8\x4f\x36\xdc\x28\xea\x1f\x5b\x72\x4f\x97\x16\x20\x5a\x45\x1e\x22\x8f\xd3\xaa\x95\x04\x7b\xc0\x54\x24\xf3\x3f\x00\xc9\x3e\x5f\xfe\x67\x77\x27\x7f\x63\xbe\xfa\x3f\xb9\x3d\xa1\x3e\xf2\x27\x10\x71\xd2\xb5\x52\x59\x8e\xee\x89\x9b\x90\x58\x69\xd6\x9f\xc2\x60\x1f\xae\xb7\x0e\x9f\x22\x55\x22\x5c\x32\xdf\x80\x88\xd5\x0a\x95\x9e\x24\x92\x6a\x73\x10\xad\x8e\xfc\xc7\x81\xd6\x3b\x55\xdb\xc0\xbe\xe7\x32\xad\xbc\xa6\xda\xee\x04\x33\x19\x0c\x3a\x8d\xf6\x6f\xf8\xfb\x43\xca\x34\x43\x86\x2c\x57\xa1\xf9\x1f\x33\x26\xa8\x92\x0d\xfa\xa7\xcc\x69\x27\xa1\xd7\xb5\x8d\x84\x3e\xcb\x62\x3f\xff\xc4\xe6\xc2\xba\x67\xa1\xf6\x6c\x5a\x26\x12\xa2\xa8\x8f\xb6\x4b\x3d\x1e\xfe\x8f\x35\x98\x64\x30\xc9\x60\x92\xc1\x24\x83\x49\x06\x93\x0c\x26\x79\xae\x92\x10\x1e\x3f\x70\x45\x54\x72\x2e\x39\x9f\x1c\x3d\x11\x06\x35\xa8\x41\x0d\x6a\x50\x83\x1a\xd4\xff\x3f\x59\x1e\xd4\x4b\x00\x1c\xee\x66\x37\x77\x44\x93\xf0\x00\x4e\x9a\x4a\x40\xf9\x95\xbc\x9a\xd2\xd6\x6d\x24\x02\x04\x36\x00\x40\x61\xa9\x53\xc2\xb9\xe0\xfa\x3b\xde\x18\x62\x49\xd9\xbf\xf2\xaa\x93\xab\x3a\x0e\xcf\xa8\x61\x70\xe3\x4e\xbd\x3f\x16\x69\x9f\xb2\x2c\x67\x66\x86\x96\x25\xbd\x42\x4f\x66\x6f\x64\x62\xc5\x67\xfd\x8b\x0f\x7f\x8a\x65\xbc\xc7\x2c\x4e\xf4\xc0\x57\x07\x49\x68\x78\x56\x89\xf3\xd0\x8a\x88\xb7\xe2\x17\x5f\x57\x3b\xcf\x1f\xde\xb0\x3f\xa3\x7a\x7a\x92\xe1\x42\x40\x51\x74\x69\x73\x87\x0b\xe5\x97\x43\x93\x82\x56\x4c\xff\x70\x01\xe9\xd4\xe5\x07\x77\x4a\x4f\x4d\x3b\x14\xa4\xbd\xfc\xe0\xe7\x71\xec\xd9\x97\xaa\x1a\x97\xde\x6c\xdd\xb9\x72\x9e\x76\x69\xe8\xd7\x87\x52\x57\xf9\xce\xd3\xb9\xcd\x8a\x5e\xb0\x79\xe5\xd9\x2f\x6b\x2d\x0d\x77\xaf\x4f\xbf\xcc\x3d\xf0\xee\xc3\xe8\x4b\x34\xdb\xe7\xa1\x7d\xd2\x03\xf3\x0c\x01\xda\xee\x2b\x4b\x3a\x7f\x6a\x5d\xd9\xf8\x60\x69\x64\xb2\x3a\xe5\xc0\x82\x2b\xcd\x67\xbf\x6c\xfd\xe0\xfd\x7d\x3a\xfd\x2f\x3f\x27\xab\xf7\xe9\x38\xb6\x8f\x05\x86\xdb\xe7\xb8\x9f\xfc\x70\xfb\x73\x6e\xda\x55\x6c\xbb\x18\x03\x21\xb1\xa3\xc9\xd5\x2e\x29\x42\x74\x91\xc0\xf1\xb3\x55\xed\x3e\xdf\x7c\x14\x1f\xf7\xa3\xf6\xaa\x07\xb8\xfe\x97\xc7\x30\xb2\xef\x2a\xa9\x59\x36\x63\xee\x86\x31\xa5\x19\xfb\x36\x44\xcd\x2f\x67\xc7\x87\xd3\xc1\x41\x7a\xb6\x73\x6f\x9b\xb9\xcd\xf4\x0d\x86\x44\x3b\x4d\xcf\x0d\xa0\xb5\x8c\x66\x14\xb1\xf4\x0e\x8b\x83\xf0\x38\x56\xfd\x46\x89\x5f\x8b\x0f\x11\x53\xb6\x28\x6f\x8c\xdf\x4f\xd1\x31\xc1\x7b\x2d\x96\xce\xa2\x76\x7a\x44\xdb\x7d\xb6\x7e\xe8\xf8\xfc\xc0\xa9\x16\x8b\xe5\x8b\xa3\x31\x5f\xe6\x05\x14\x5f\xa2\x6b\xf7\x1a\x66\x1e\xb0\x58\x2c\x77\x0f\x37\x68\x46\x6c\x11\xb5\x18\x2b\x59\x18\x4a\xf7\xab\xbf\xe5\x5b\x2c\x16\x4b\x37\xfa\x8d\x6f\x88\x9e\xde\xfe\xcb\x12\x32\x47\xd5\xde\x38\x78\xdf\x62\xf9\xda\x62\x53\x0b\x3f\xe5\x62\x6d\x6b\x73\x76\x6b\xcd\x47\xaf\xa7\xbd\x12\x31\xf9\xdc\x17\x07\x17\x9a\x0f\xd7\xcb\x5d\xfc\xdc\x50\x2b\xde\x63\xca\x4f\xef\x26\x7d\x84\xdd\x09\x90\xb0\x11\x00\x0b\x70\xea\xc7\xbb\xfe\x00\x96\x5a\xf6\x68\x09\x17\x9b\xfd\x72\x39\x67\x7c\xa9\xdf\xa9\x63\x12\x70\x5d\xbf\x58\x5c\xda\x8e\xe3\xc5\x37\x08\x68\x2b\x66\xce\xa0\xb5\xec\x9b\xdc\x34\x71\xd1\x25\x16\x0c\x23\x61\xd1\x43\xd9\x57\x97\xee\xb2\x3c\x4b\x3d\xa8\x97\x08\x80\x50\x3f\x2c\x35\x7f\xa4\xd9\x46\x5c\x3b\xa0\x1c\x18\x69\x7c\xb3\xdf\x71\x00\x08\x34\x03\x60\x9f\x69\xbe\x7e\x17\x80\xcb\xc1\xe6\x42\x80\x87\x3f\xbd\x7e\x3e\x3f\x6a\xb4\xf1\x6a\xd7\x2f\x3b\x66\x17\xcd\xff\x4c\xdf\x28\x96\x10\x40\x0a\x73\x41\xcc\xe7\x56\x8e\x70\x81\x7c\xf4\x28\x57\x37\x63\xe9\x37\x6d\xf5\x07\x1b\x9b\x2e\xcf\x07\xd8\x50\xdd\x70\x60\xb6\x78\x8f\x68\x43\xf4\x0a\xdc\xdb\xcd\xfb\x29\x95\x44\x42\xf2\x76\x08\x02\x33\x78\x49\xdc\x87\x2c\xc1\x98\x97\x64\x50\xca\x1d\xca\x63\x40\xb2\x09\x64\x66\xca\x45\xff\x9d\x3c\xcf\x45\x7b\xfb\xdb\xef\x73\xa7\x07\xf5\x92\x2d\x30\xe4\x81\xe7\x69\xcd\xf8\x2e\xf3\x08\x70\xd2\x00\xa0\x1c\x5c\xd2\x26\x99\x9d\x07\x28\x71\x96\x76\x00\xd4\xba\xba\x4a\x18\x79\x66\x9b\xeb\xa2\x99\x6b\x5c\x00\x5f\xe9\x24\x3a\x59\x56\x48\x04\x13\xda\x48\x9e\x6e\x72\x2f\xbd\xb2\x9d\x87\x59\x82\x7a\xc5\x87\x51\xb4\x9f\x84\x4f\xd6\x5b\x7b\xc1\x29\x13\x0f\x20\xfa\xe1\x6f\x26\x59\x36\xb1\xc8\x5c\xe7\x62\x02\x9c\xb1\x96\x88\x89\x23\xc8\xf4\xb9\x95\x38\x94\x39\xee\xc3\x91\x09\xbb\x78\x2e\x7b\xfa\xdb\xdc\x0b\xa0\x5e\xe2\x52\x0b\x50\x5d\xe6\x40\x3b\x71\xb8\xa0\x94\xc4\x76\xc7\x81\x4a\x5c\x8f\xda\x01\xc0\xef\x67\x3c\x80\x63\xf7\x91\x06\x11\xab\x73\x8b\xbc\x89\x73\x97\x7a\x74\x61\xde\x0a\xd7\x98\xf6\xf3\x05\xe8\x13\xc6\xad\xcb\xf9\x6b\x1a\x61\x68\xb1\x95\x38\xb7\x0a\x80\x4b\x25\x53\x5a\x7e\x1a\x9a\x3f\x8d\x58\x0f\x3e\x78\x23\xde\x88\x4e\x42\x1f\xb9\xf3\x5e\x6e\xbc\x51\x59\x23\xa0\x94\x81\xf3\x90\xfe\xb6\xf4\x62\xc8\x4e\xdc\x43\xcf\x54\x0d\x74\x99\x03\x8c\x76\xe2\x66\xa5\x4c\x32\x0f\x70\xe2\x6c\x85\x6b\x05\x80\xe5\x48\x12\x73\x76\x60\xd9\xfa\xd5\x75\x86\xc5\xe1\x7c\x10\x3b\xbb\xc3\x59\xdc\xce\x0b\xb8\x69\xe6\x7a\xef\x89\xa9\x89\x9b\x8d\x0e\xf5\xd6\xa1\x9f\x17\x59\x6b\x5c\x37\x21\x7e\xd4\xcb\xb1\xed\x4e\xe3\xf2\x67\xa4\x6a\xde\xe2\xd1\xa6\x83\xcb\xb0\xc6\xa6\x36\x13\xee\x84\x51\xf9\x9d\xa9\x2a\xd8\x28\x5f\xdc\xdf\xbe\x9e\x7f\xd9\x89\xbb\x6f\xaf\x71\x63\x7a\x6a\x1c\x31\x45\x68\xc6\x0c\x6c\xe2\x2c\x16\x33\x00\xfa\xa6\x0c\x20\x72\x4e\xf1\x96\xdd\xbb\x6e\xd4\x86\x95\x99\x70\x46\xa2\x11\x9d\xe4\x9c\xec\x7f\xa1\x08\x73\xd8\xbc\xd2\x65\x72\x70\xa9\x31\xdd\x68\x83\x73\x25\x80\xe5\xbb\x35\x85\x7e\x4e\xce\xc6\xd1\x9c\xf3\x53\x87\x93\x62\xdb\x83\x5c\x60\xb7\x43\xda\xc9\x11\xe5\xa9\x46\xef\xaa\xd5\x49\xa5\x46\xd9\xc5\xfe\xf6\xf4\x22\xa8\xb7\xab\xa6\xdc\x42\x75\x99\x03\x7a\x88\xcb\x4e\xe1\x0f\xf0\x1a\x67\xed\xa9\xde\x75\x00\x23\x2d\x21\x1b\x13\x05\xc1\x30\x74\x6d\x60\x5d\x70\x1a\x88\xdd\x3c\xc1\x03\xb5\xfa\x35\x86\x1e\x45\x13\xe3\x66\xd4\x4d\x8e\x45\x25\x4b\xac\xc4\x7d\xbc\x0e\xc0\xd2\x72\x82\xb9\xd1\x59\x1e\x0c\x8d\x7b\x1c\x13\x37\xf9\xa0\x2a\x2b\x47\x28\xc6\x38\x28\x36\x66\x8f\x46\xbd\x7e\xa8\xf4\x7a\x2a\x91\x28\xab\xe8\x6f\x5f\x2f\x80\x7a\xbb\x6a\x8b\x95\xb8\xc0\x9e\xae\xea\x93\x16\x33\xc0\x89\xeb\xe9\xab\x3b\x0a\x01\x2c\xf7\x2f\x63\xe6\xc4\x84\xdf\x3f\x35\x75\xb8\x07\x8f\xe4\x59\x5f\x83\xcb\xb9\x91\xcd\xc0\xd4\x12\x8a\xbe\x58\xfe\x8e\xf1\x8d\x8f\x34\x00\xb0\xed\x9f\x2f\x03\x3c\x90\xbf\x9b\xee\xd4\xaa\xc8\xce\x2c\x3a\x97\x29\xdb\xbc\x8a\x00\x25\xe0\x66\x8a\xd8\x71\x72\x2d\xcf\x7f\xeb\xfc\xb9\x85\x92\x6b\x8e\x01\x8e\xd1\xfd\xed\xea\xf9\x57\x6f\x57\xfd\xfc\x16\xae\xcb\x3c\xd4\x5e\xe3\x1c\x3f\x4a\xe1\x9a\xa9\x03\x88\x38\x33\x40\xf0\x8d\x66\x80\xe8\x33\x13\xb0\x8b\x35\xb7\x26\xfb\xf1\xdb\xc2\xbe\xfc\x04\x0d\xce\x34\x34\x6a\x79\x5d\xab\x19\x8b\x6e\x04\x93\x2b\x48\x8d\xdb\x69\xdb\x24\x69\x74\x92\x2b\x0d\x8f\xae\x92\xe0\xc5\xc7\x2b\x34\xa8\xe2\xd1\x00\xb0\x6e\x7b\x04\xc0\x2f\x77\x42\xb0\x89\xd7\xcf\x78\xec\x30\x93\x50\x1d\x5c\x71\xeb\x22\x1a\x0e\x2a\xc1\xd1\xe4\xb0\x5b\xb1\xb6\x38\xc8\x68\x34\xb4\x97\x25\x0f\x83\xa0\x4a\xf4\x20\x76\x7f\xa5\x5e\xe2\x92\x35\xe8\xc7\x3b\x07\x49\xda\x84\x01\x5e\xe3\x1e\x02\x00\x3a\xf7\x28\x60\x2c\xbb\xd3\x5b\x7f\xf4\xdc\x9d\xbe\xcb\xb4\x0a\x45\x34\x4a\xae\xb9\x99\x88\x1f\x6c\x7d\x57\x1c\x5a\xbb\x69\x08\x77\xd8\x50\x23\x9d\x87\xd7\xd8\x8a\x21\x02\x60\xb9\x97\x73\xe2\x52\xe9\xd7\xfc\xba\x40\x9d\x3b\xb0\x46\xe3\xb0\xe0\xeb\x3d\xfb\xf6\x90\xf6\x91\x93\xc6\xaa\x1e\xae\x2b\x19\x66\x1c\xac\x70\x4f\xa3\xc7\x35\xce\xba\x73\xe8\x21\x0e\x9b\x16\x3d\x70\x9f\xc7\xf5\xca\x06\xdd\x4e\x4f\xc0\x58\xb6\x0a\x2e\x36\x37\x51\xcf\x99\x32\x5d\x64\xa3\x2b\x3d\x97\x06\x40\xe0\xdd\x40\xec\x9a\xdc\x85\x1d\x1a\x93\xff\x0e\x60\x59\x89\x2b\x17\x03\x58\x9a\x34\xa9\x92\xaf\x46\xf9\x23\x17\xd3\xaa\xc4\x4e\xa3\x80\x87\x47\x99\x30\x1f\x8e\x5b\xd6\x36\xaa\x6a\xf7\xde\x21\x27\x57\xef\xd8\x01\xec\xfe\x36\xf5\x02\xa8\x77\xe7\x70\xca\x4a\x1c\xb9\x87\xb8\x82\x34\xee\xc0\xae\x71\xdd\x00\x80\x62\x7e\x06\x28\xcb\xcf\x8d\x37\x8f\x6c\x5c\xfa\xda\xcb\x5e\xc3\x81\x26\xc1\x7a\x41\xab\xb9\x64\x4b\x93\x89\xf7\x69\xb0\xdf\x3f\x67\xbb\x0d\x07\xad\xd1\xc9\xba\xc9\xc0\x97\xf3\x00\x2c\x27\xda\x28\x27\x2f\xc4\x3a\x7f\x55\x9e\x89\xbf\xf1\x32\x54\x01\xd6\xe4\x16\x9a\xb0\x66\xdf\x4b\x1f\x74\xee\xff\x87\x58\x8e\x3e\xbd\xf4\xfd\xfe\xf6\xf5\x22\xa8\x0f\x71\xbd\x4f\x47\x3c\x53\xa2\x07\x36\x71\x96\x9e\xed\xaa\xc7\x72\x23\x8c\xb4\xcc\x10\x5c\xc8\x60\x5f\x7c\x67\x2f\xf6\x5d\x2c\x48\x66\x61\x9a\xd0\x6b\x18\xcd\xfe\x6e\x17\x6f\xdf\xf4\xdb\xf8\x12\xac\x51\x00\x80\xf3\xc7\x33\x00\x2c\xdb\xaa\x57\x8a\x1c\xea\xb1\x23\x3f\x9e\xea\x32\x33\xc3\xe8\x13\x8c\xa9\x21\x10\x7c\xb3\x4a\xd1\xc8\x0d\xd5\xad\x4b\xf5\x6f\x62\x2e\x0c\x3e\x03\x7e\x0a\xd9\x89\xfb\xd5\x53\x7c\x0b\xfd\xf8\x9d\x43\x54\x8a\x70\xc0\x13\x67\x04\x80\xd9\xc7\x8c\xb0\xf0\xd7\x90\x8e\x5c\x54\xdd\x57\x07\xf6\xa3\x2b\xd1\x2e\x50\x19\x44\x30\xb9\x8e\xfb\x75\xcb\x74\xaf\xca\xea\xb2\x9b\x67\x88\xc6\x9c\x14\x6b\x57\x6d\x9e\x0f\x60\x69\x39\xcd\x54\xee\x3e\xfa\xfd\x6e\xe5\xbd\x44\x1d\xf0\xd0\x34\x14\x4a\x87\xc1\x1e\x8a\x78\x15\xe5\x19\x35\xfd\xe6\xe5\x51\x78\xfc\xfe\x43\xfd\xed\xea\x05\xd0\xa3\xe7\x71\x7d\x76\x0e\x03\xfe\x9d\x83\xad\xad\xae\x7f\x05\x50\x96\x13\xb0\x69\xa1\x17\xf9\xc2\x02\xaf\x72\x1f\xb4\x31\x21\x37\x86\x14\x26\x11\xcc\x69\xdf\x86\xf1\x82\xb7\x22\xe7\x77\x3a\xf1\x26\xba\x03\xc0\xe5\x1b\x26\x1e\x40\xd4\xdd\x0a\x56\x83\x03\xce\x4c\x39\xf0\xb6\xcf\x10\x54\x8a\x11\xe0\xe4\xc8\x9d\x95\x28\x5a\x0a\xee\x36\x54\x92\x69\x61\x8e\xb2\x21\xd5\x7e\x4c\x47\x54\xbd\x18\x1b\x7a\xe7\xee\x7b\x21\x30\x6b\x69\x7f\xfb\x7c\x0e\xf5\xf8\x9d\x83\x63\x97\x79\x58\xcf\xd3\x11\x5a\x4a\x82\x39\x7a\x80\x11\x67\x06\x88\xfc\x64\x15\x0f\xa2\x2d\x94\xce\x55\x95\x1d\x71\xcb\xee\x25\xe0\x55\xaa\x7b\x24\x14\x16\xbc\x87\xb0\x12\x7c\x25\xbe\x2e\x12\x31\x96\x16\x89\xe2\x3b\x48\xaf\x30\xd6\xe1\x5a\x03\xb7\xef\xf9\xd7\x59\x42\xa5\x5f\x41\x51\x51\xd1\xb2\xb7\xdc\x67\xb7\xc2\x48\x72\x6c\xdc\xd1\x2d\x02\x4a\x52\xc3\xa6\xd3\x39\xcd\xdb\xb3\xde\x58\xff\x66\x47\xc8\xfd\xf1\x1f\xee\x19\x41\x8f\x61\x85\x4e\x88\xe5\x11\xce\xfb\x66\xde\x32\x0f\x1f\xdb\xed\xd3\x16\x6f\xca\x16\xbe\xbf\xdc\xbd\x0c\xd5\x8e\x1f\xf1\x21\xcf\x1c\xff\xa6\x77\x2e\x25\x2a\x72\x44\x7e\xc8\xe4\xcd\xf5\xdd\x2f\x1d\xd9\x50\x33\x41\xef\xe7\xb7\x70\x45\xab\xf0\xf8\x8d\xc8\x6f\xfd\x8f\x6c\x18\x35\x2d\x74\xda\x7b\x15\x8d\xfe\x9b\x8b\xfd\xce\xdf\x47\xfa\x7b\xb1\x9e\x4a\xdd\xe3\x2e\xa7\xac\x2b\x16\x1f\xca\x51\x8f\x75\x58\xd4\x1e\xdf\x58\x38\x61\x66\x34\xb1\xa1\x6e\xc3\xd1\x4a\x91\x23\xf6\xad\x45\x35\x1c\x37\x4a\x11\xe3\xd0\x75\x87\x9d\xed\x79\xe6\xee\x10\xc3\xd2\x05\x67\xa1\xe1\xe8\x62\x3b\x71\xf8\x63\x41\xe9\x85\xf7\xae\xcf\x02\x3b\x71\x63\x36\x57\xcf\xf8\xfe\xb6\x6b\x6d\x0f\x71\x91\xfe\x6e\x57\xfd\xc7\xe0\x7d\x82\x82\x24\xf5\x93\x3f\x3f\xe6\x92\xe3\x56\xdd\xb5\x92\x6d\x04\x07\x34\xca\xab\x6b\xc7\x22\xde\x55\x27\x74\xd5\x2c\x9f\xad\xc4\x7f\x7c\xc6\x70\x3c\x20\x6c\x05\xc0\xa2\x7f\xe4\x55\xf1\x86\xe3\x09\x01\xa6\xcd\x12\xb1\xe9\xd8\xab\x2c\xfc\xc1\x8e\x6f\x93\x0c\x8e\xf3\x8a\xcd\x66\x74\x4b\x98\xb0\x7d\xc6\x91\x96\xf1\x45\xd7\x5f\x5b\x3b\xdf\xb1\x2d\xca\x7d\xed\x35\x47\x21\xf3\xb7\x6c\xaf\xc3\xb8\x97\x12\x02\x82\xd2\x97\x97\x14\x4d\xd9\x5e\xa8\x3d\xfd\xdb\x4f\x88\xe2\x1d\xd3\x0f\x1f\xcc\x32\xbb\x0f\x8d\x19\x1e\x55\x98\x44\xc4\xd7\x94\x94\x4d\xdd\x7a\x9e\x73\xad\xec\x03\xfd\xf1\xe9\x37\x2f\xcd\xd1\x86\x08\x2f\x78\xeb\xdd\xbb\x62\x48\xf1\xe5\x33\x33\x72\x77\xf9\xef\xba\x52\xeb\x5b\x7e\xa2\x02\xd7\xb6\xf3\x52\x61\x4d\x7d\xfd\xdb\xee\x58\xfd\x85\xb9\xd7\x16\xd0\xe7\x5f\xfc\x72\xc2\x82\x1f\xa2\x97\xaf\x97\x1d\xbc\xd3\x54\xe5\xe1\x81\xc6\x49\x3b\x89\x3b\x47\x2e\xf7\x1a\xb1\xb5\xad\x62\xb6\xb8\xe1\xc2\x3c\x6a\xc9\x27\x2b\xe8\x79\x11\x27\x8f\x9f\x5e\x12\x84\xbe\x66\x4c\xfd\xcd\xf3\xe2\xfc\x2d\xae\x69\xef\x1b\xdf\x25\xd6\xae\x91\xf1\xc3\xdc\xd3\x94\x7b\x53\x62\x93\x62\x8a\x52\x59\xc5\x5b\xc4\xe7\x12\x73\x08\x7b\xf8\x82\xdb\x0f\x2d\x89\x35\xeb\x6b\xf4\x37\xb3\x5c\xf6\xac\xf9\xe2\xc6\xd9\x3a\xcf\xd1\x65\xfa\x10\xe9\xf7\x3b\x37\xb2\x4f\xdc\x73\x32\x0d\x97\x24\x4e\x5e\x4f\x6d\xc6\xbc\xf2\x6a\x53\xe7\xaf\xa5\xb4\x8e\xa4\xc6\x9a\x2f\x1a\x87\xe6\xec\xea\x38\xa9\x1c\x3b\x36\xf1\xed\x39\x2a\xd2\xf1\x24\xfe\xbd\xbd\xc4\x4d\xe3\x76\xbc\xb1\x6c\xeb\xb9\x3c\x8f\xd5\x15\x6b\xbd\xd1\x0f\xb9\xc2\x4f\x91\x24\x85\x77\xc0\x36\xc3\x38\x25\x7b\xf5\xb4\x29\x65\x67\x85\x9f\x26\x4d\x59\x39\xcc\xa5\x69\x12\x9d\x13\x56\xbd\xf6\xcc\x25\x43\x38\xb3\x62\xd3\x77\x9d\x73\x32\xd8\x22\x8f\xcd\x57\x9a\xa6\xbc\x53\xa3\x48\x3b\x56\x72\x8e\xb3\xba\x83\x91\x4e\xf7\x99\x92\x26\xfc\xb4\xe8\x2a\x67\x75\x4a\xd5\xd8\xe2\x0e\xcf\xce\xb3\x82\x4e\x64\x0d\x27\xbb\x36\x33\x83\xfd\xce\xe6\xf4\x65\xdf\xb1\x85\xbe\x04\x83\xab\xf8\x98\x94\x2d\xf2\xa8\x91\xdd\x5b\xec\x3d\xd6\x2b\xc0\x74\x26\x65\x52\x6a\x45\xcd\xdb\x0c\xc6\x75\xdf\x25\xbb\x4e\x6c\x60\x14\x9f\x9f\xfa\xad\x69\xfd\x28\x51\xd6\x38\x36\x7f\x4b\xb0\x6f\xc8\xdc\xb8\x67\x76\xc3\xd0\xf3\xce\x21\xfb\x5c\xe1\xaf\xf3\xb2\xa7\xd8\xbb\x2a\x00\x8a\x02\x00\xa4\x01\x55\xe3\xfe\xf7\xea\xb7\x3f\x97\x1d\x90\x4a\x8f\x99\xd4\xaf\xf9\xff\x3b\x00\x00\xff\xff\xd6\x2f\xcb\x16\x21\x4a\x00\x00")

func dataQuindarToneMp3Bytes() ([]byte, error) {
	return bindataRead(
		_dataQuindarToneMp3,
		"quindar-tone.mp3",
	)
}

func dataQuindarToneMp3() (*asset, error) {
	bytes, err := dataQuindarToneMp3Bytes()
	if err != nil {
		return nil, err
	}

	info := bindataFileInfo{name: "quindar-tone.mp3", size: 18977, mode: os.FileMode(420), modTime: time.Unix(1379677344, 0)}
	a := &asset{bytes: bytes, info: info}
	return a, nil
}

// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	if f, ok := _bindata[cannonicalName]; ok {
		a, err := f()
		if err != nil {
			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
		}
		return a.bytes, nil
	}
	return nil, fmt.Errorf("Asset %s not found", name)
}

// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
	a, err := Asset(name)
	if err != nil {
		panic("asset: Asset(" + name + "): " + err.Error())
	}

	return a
}

// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	if f, ok := _bindata[cannonicalName]; ok {
		a, err := f()
		if err != nil {
			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
		}
		return a.info, nil
	}
	return nil, fmt.Errorf("AssetInfo %s not found", name)
}

// AssetNames returns the names of the assets.
func AssetNames() []string {
	names := make([]string, 0, len(_bindata))
	for name := range _bindata {
		names = append(names, name)
	}
	return names
}

// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
	"3d.flf":           data3dFlf,
	"2d.flf":           data2dFlf,
	"quindar-tone.mp3": dataQuindarToneMp3,
}

// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
//     data/
//       foo.txt
//       img/
//         a.png
//         b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
	node := _bintree
	if len(name) != 0 {
		cannonicalName := strings.Replace(name, "\\", "/", -1)
		pathList := strings.Split(cannonicalName, "/")
		for _, p := range pathList {
			node = node.Children[p]
			if node == nil {
				return nil, fmt.Errorf("Asset %s not found", name)
			}
		}
	}
	if node.Func != nil {
		return nil, fmt.Errorf("Asset %s not found", name)
	}
	rv := make([]string, 0, len(node.Children))
	for childName := range node.Children {
		rv = append(rv, childName)
	}
	return rv, nil
}

type bintree struct {
	Func     func() (*asset, error)
	Children map[string]*bintree
}

var _bintree = &bintree{nil, map[string]*bintree{
	"data": &bintree{nil, map[string]*bintree{
		"3d.flf":           &bintree{data3dFlf, map[string]*bintree{}},
		"2d.flf":           &bintree{data2dFlf, map[string]*bintree{}},
		"quindar-tone.mp3": &bintree{dataQuindarToneMp3, map[string]*bintree{}},
	}},
}}

// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
	data, err := Asset(name)
	if err != nil {
		return err
	}
	info, err := AssetInfo(name)
	if err != nil {
		return err
	}
	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
	if err != nil {
		return err
	}
	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
	if err != nil {
		return err
	}
	return nil
}

// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
	children, err := AssetDir(name)
	// File
	if err != nil {
		return RestoreAsset(dir, name)
	}
	// Dir
	for _, child := range children {
		err = RestoreAssets(dir, filepath.Join(name, child))
		if err != nil {
			return err
		}
	}
	return nil
}

func _filePath(dir, name string) string {
	cannonicalName := strings.Replace(name, "\\", "/", -1)
	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}


================================================
FILE: asset/file.go
================================================
package asset

import (
	"bytes"
	"io"
)

type AssetFile struct {
	reader io.Reader
}

func NewAssetFile(data []byte) AssetFile {
	return AssetFile{bytes.NewReader(data)}
}

func (a AssetFile) Read(p []byte) (n int, err error) {
	return a.reader.Read(p)
}

func (a AssetFile) Close() error {
	return nil
}


================================================
FILE: asset/player.go
================================================
package asset

import (
	"github.com/hajimehoshi/go-mp3"
	"github.com/hajimehoshi/oto"
	"io"
	"log"
)

type AudioPlayer struct {
	player *oto.Player
	beep   []byte
}

func NewAudioPlayer() *AudioPlayer {

	bytes, err := Asset("quindar-tone.mp3")
	if err != nil {
		log.Fatal("Failed to find audio file")
	}

	player, err := oto.NewPlayer(44100, 2, 2, 8192)
	if err != nil {
		// it is expected to fail when some of the system
		// libraries are not available (e.g. libasound2)
		// it is not the main functionality of the application,
		// so we allow startup in no-sound mode
		return nil
	}

	return &AudioPlayer{
		player: player,
		beep:   bytes,
	}
}

func (a *AudioPlayer) Beep() {

	decoder, err := mp3.NewDecoder(NewAssetFile(a.beep))
	if err != nil {
		panic(err)
	}

	if _, err := io.Copy(a.player, decoder); err != nil {
		panic(err)
	}
}

func (a *AudioPlayer) Close() {
	_ = a.player.Close()
}


================================================
FILE: component/alert.go
================================================
package component

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"strings"
)

func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buffer) {

	if alert == nil {
		return
	}

	color := console.ColorWhite

	if alert.Color != nil {
		color = *alert.Color
	}

	width := util.Max([]int{len(alert.Title), len(alert.Text)}) + 10

	if width > area.Dx() {
		width = area.Dx()
	}

	cells := ui.WrapCells(ui.ParseStyles(fmt.Sprintf("%s\n%s\n",
		strings.ToUpper(alert.Title), alert.Text), ui.NewStyle(console.ColorWhite)), uint(width))

	var lines []string
	line := ""

	for i := 0; i < len(cells); i++ {
		if cells[i].Rune == '\n' {
			lines = append(lines, line)
			line = ""
		} else {
			line += string(cells[i].Rune)
		}
	}

	block := *ui.NewBlock()
	block.SetRect(util.GetRectCoordinates(area, width, len(lines)))
	block.BorderStyle = ui.Style{Fg: color, Bg: ui.ColorClear}
	block.Draw(buffer)

	buffer.Fill(ui.NewCell(' ', ui.NewStyle(console.ColorBlack)), block.Inner)

	for i := 0; i < len(lines); i++ {
		buffer.SetString(lines[i],
			ui.NewStyle(color), util.GetMiddlePoint(block.Inner, lines[i], i-1))
	}
}


================================================
FILE: component/asciibox/asciibox.go
================================================
package asciibox

import (
	ui "github.com/gizak/termui/v3"
	fl "github.com/mbndr/figlet4go"
	"github.com/sqshq/sampler/asset"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"strings"
)

// AsciiBox represents a component with ascii-style text
type AsciiBox struct {
	*ui.Block
	*data.Consumer
	alert   *data.Alert
	ascii   string
	style   ui.Style
	render  *fl.AsciiRender
	options *fl.RenderOptions
	palette console.Palette
}

const asciiFontExtension = ".flf"

func NewAsciiBox(c config.AsciiBoxConfig, palette console.Palette) *AsciiBox {

	options := fl.NewRenderOptions()
	options.FontName = string(*c.Font)

	fontStr, err := asset.Asset(options.FontName + asciiFontExtension)
	if err != nil {
		panic("Failed to load the font: " + err.Error())
	}

	render := fl.NewAsciiRender()
	_ = render.LoadBindataFont(fontStr, options.FontName)

	color := c.Color
	if color == nil {
		color = &palette.BaseColor
	}

	box := AsciiBox{
		Block:    component.NewBlock(c.Title, *c.Border, palette),
		Consumer: data.NewConsumer(),
		style:    ui.NewStyle(*color),
		render:   render,
		options:  options,
		palette:  palette,
	}

	go func() {
		for {
			select {
			case sample := <-box.SampleChannel:
				box.renderText(sample)
			case alert := <-box.AlertChannel:
				box.alert = alert
			}
		}
	}()

	return &box
}

func (a *AsciiBox) renderText(sample *data.Sample) {
	text := strings.TrimSpace(sample.Value)
	lines := strings.Split(text, "\n")
	a.ascii = ""
	for _, line := range lines {
		ascii, _ := a.render.RenderOpts(line, a.options)
		a.ascii += ascii + "\n"
	}
}

func (a *AsciiBox) Draw(buffer *ui.Buffer) {

	buffer.Fill(ui.NewCell(' ', ui.NewStyle(ui.ColorBlack)), a.GetRect())
	a.Block.Draw(buffer)

	point := a.Inner.Min
	cells := ui.ParseStyles(a.ascii, a.style)

	for i := 0; i < len(cells) && point.Y < a.Inner.Max.Y; i++ {
		if cells[i].Rune == '\n' {
			point = image.Pt(a.Inner.Min.X, point.Y+1)
		} else if point.In(a.Inner) {
			buffer.SetCell(cells[i], point)
			point = point.Add(image.Pt(1, 0))
		}
	}

	component.RenderAlert(a.alert, a.Rectangle, buffer)
}


================================================
FILE: component/barchart/barchart.go
================================================
package barchart

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	rw "github.com/mattn/go-runewidth"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"math"
)

const (
	barIndent int = 1
)

// BarChart presents categorical data with rectangular bars
type BarChart struct {
	*ui.Block
	*data.Consumer
	bars     []bar
	scale    int
	maxValue float64
	count    int64
	palette  console.Palette
}

type bar struct {
	label string
	color ui.Color
	value float64
	delta float64
}

func NewBarChart(c config.BarChartConfig, palette console.Palette) *BarChart {

	chart := BarChart{
		Block:    component.NewBlock(c.Title, true, palette),
		Consumer: data.NewConsumer(),
		bars:     []bar{},
		scale:    *c.Scale,
		maxValue: -math.MaxFloat64,
		palette:  palette,
	}

	for _, i := range c.Items {
		chart.addBar(*i.Label, *i.Color)
	}

	go func() {
		for {
			select {
			case sample := <-chart.SampleChannel:
				chart.consumeSample(sample)
			case alert := <-chart.AlertChannel:
				chart.Alert = alert
			}
		}
	}()

	return &chart
}

func (b *BarChart) consumeSample(sample *data.Sample) {

	b.count++

	float, err := util.ParseFloat(sample.Value)

	if err != nil {
		b.HandleConsumeFailure("Failed to parse a number", err, sample)
		return
	}

	b.HandleConsumeSuccess()

	index := -1
	for i, bar := range b.bars {
		if bar.label == sample.Label {
			index = i
		}
	}

	bar := b.bars[index]
	bar.delta = float - bar.value
	bar.value = float
	b.bars[index] = bar

	if float > b.maxValue {
		b.maxValue = float
	}

	// normalize bars height once in a while
	if b.count%500 == 0 {
		b.reselectMaxValue()
	}
}

func (b *BarChart) addBar(label string, color ui.Color) {
	b.bars = append(b.bars, bar{label: label, color: color, value: 0})
}

func (b *BarChart) reselectMaxValue() {
	maxValue := -math.MaxFloat64
	for _, bar := range b.bars {
		if bar.value > maxValue {
			maxValue = bar.value
		}
	}
	b.maxValue = maxValue
}

// Draw renders the barchart
func (b *BarChart) Draw(buffer *ui.Buffer) {
	b.Block.Draw(buffer)

	barWidth := int(math.Floor(float64(b.Inner.Dx()-len(b.bars)*barIndent) / float64(len(b.bars))))
	barXCoordinate := b.Inner.Min.X + barIndent

	labelStyle := ui.NewStyle(b.palette.BaseColor)

	for _, bar := range b.bars {

		// draw bar
		height := int((bar.value / b.maxValue) * float64(b.Inner.Dy()-1))
		if height <= 1 {
			height = 2
		}

		maxYCoordinate := b.Inner.Max.Y - height
		for x := barXCoordinate; x < ui.MinInt(barXCoordinate+barWidth, b.Inner.Max.X-barIndent); x++ {
			for y := b.Inner.Max.Y - 2; y >= maxYCoordinate; y-- {
				c := ui.NewCell(console.SymbolHorizontalBar, ui.NewStyle(bar.color))
				buffer.SetCell(c, image.Pt(x, y))
			}
		}

		// draw label
		labelXCoordinate := barXCoordinate +
			int(float64(barWidth)/2) -
			int(float64(rw.StringWidth(bar.label))/2)
		buffer.SetString(
			bar.label,
			labelStyle,
			image.Pt(labelXCoordinate, b.Inner.Max.Y-1))

		// draw value & delta
		value := util.FormatValue(bar.value, b.scale)
		if bar.delta != 0 {
			value = fmt.Sprintf("%s/%s", value, util.FormatDelta(bar.delta, b.scale))
		}
		valueXCoordinate := barXCoordinate +
			int(float64(barWidth)/2) -
			int(float64(rw.StringWidth(value))/2)
		buffer.SetString(
			value,
			labelStyle,
			image.Pt(valueXCoordinate, maxYCoordinate-1))

		barXCoordinate += barWidth + barIndent
	}

	component.RenderAlert(b.Alert, b.Rectangle, buffer)
}


================================================
FILE: component/block.go
================================================
package component

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/console"
)

func NewBlock(title string, border bool, palette console.Palette) *ui.Block {
	style := ui.Style{Fg: palette.BaseColor, Bg: ui.ColorClear}
	block := ui.NewBlock()
	block.Border = border
	block.BorderStyle = style
	block.TitleStyle = style
	if len(title) > 0 {
		block.Title = fmt.Sprintf(" %s ", title)
	}
	return block
}


================================================
FILE: component/component.go
================================================
package component

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/data"
)

type Component struct {
	ui.Drawable
	*data.Consumer
	Type     config.ComponentType
	Title    string
	Location config.Location
	Size     config.Size
	RateMs   int
}

func NewComponent(dbl ui.Drawable, cmr *data.Consumer, cfg config.ComponentConfig) *Component {
	return &Component{
		Drawable: dbl,
		Consumer: cmr,
		Type:     cfg.Type,
		Title:    cfg.Title,
		Location: cfg.GetLocation(),
		Size:     cfg.GetSize(),
		RateMs:   *cfg.RateMs,
	}
}

func (c *Component) Move(x, y int) {
	c.Location.X += x
	c.Location.Y += y
	c.normalize()
}

func (c *Component) Resize(x, y int) {
	c.Size.X += x
	c.Size.Y += y
	c.normalize()
}

func (c *Component) normalize() {
	if c.Location.X < 0 {
		c.Location.X = 0
	}
	if c.Location.Y < 0 {
		c.Location.Y = 0
	}
}


================================================
FILE: component/gauge/gauge.go
================================================
package gauge

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
)

const (
	MinValueLabel = "min"
	MaxValueLabel = "max"
	CurValueLabel = "cur"
)

// Gauge displays cur value between specified min and max values
type Gauge struct {
	*ui.Block
	*data.Consumer
	minValue    float64
	maxValue    float64
	curValue    float64
	color       ui.Color
	scale       int
	percentOnly bool
	palette     console.Palette
}

func NewGauge(c config.GaugeConfig, palette console.Palette) *Gauge {

	g := Gauge{
		Block:       component.NewBlock(c.Title, true, palette),
		Consumer:    data.NewConsumer(),
		color:       *c.Color,
		scale:       *c.Scale,
		percentOnly: *c.PercentOnly,
		palette:     palette,
	}

	go func() {
		for {
			select {
			case sample := <-g.SampleChannel:
				g.ConsumeSample(sample)
			case alert := <-g.AlertChannel:
				g.Alert = alert
			}
		}
	}()

	return &g
}

func (g *Gauge) ConsumeSample(sample *data.Sample) {

	float, err := util.ParseFloat(sample.Value)
	if err != nil {
		g.HandleConsumeFailure("Failed to parse a number", err, sample)
		return
	}

	g.HandleConsumeSuccess()

	switch sample.Label {
	case MinValueLabel:
		g.minValue = float
		break
	case MaxValueLabel:
		g.maxValue = float
		break
	case CurValueLabel:
		g.curValue = float
		break
	}
}

func (g *Gauge) Draw(buffer *ui.Buffer) {

	g.Block.Draw(buffer)

	percent := calculatePercent(g)

	var label string
	if g.percentOnly {
		label = fmt.Sprintf(" %v%% ", util.FormatValue(percent, g.scale))
	} else {
		label = fmt.Sprintf(" %v%% (%v) ", util.FormatValue(percent, g.scale), util.FormatValue(g.curValue, g.scale))
	}

	// draw bar
	barWidth := int((percent / 100) * float64(g.Inner.Dx()))
	if barWidth == 0 {
		barWidth = 1
	} else if barWidth > g.Dx()-2 {
		barWidth = g.Dx() - 2
	}
	buffer.Fill(
		ui.NewCell(console.SymbolVerticalBar, ui.NewStyle(g.color)),
		image.Rect(g.Inner.Min.X+1, g.Inner.Min.Y, g.Inner.Min.X+barWidth, g.Inner.Max.Y),
	)

	// draw label
	labelXCoordinate := g.Inner.Min.X + (g.Inner.Dx() / 2) - int(float64(len(label))/2)
	labelYCoordinate := g.Inner.Min.Y + ((g.Inner.Dy() - 1) / 2)
	if labelYCoordinate < g.Inner.Max.Y {
		for i, char := range label {
			style := ui.NewStyle(g.palette.BaseColor)
			if labelXCoordinate+i+1 <= g.Inner.Min.X+barWidth {
				style = ui.NewStyle(g.palette.BaseColor, ui.ColorClear)
			}
			buffer.SetCell(ui.NewCell(char, style), image.Pt(labelXCoordinate+i, labelYCoordinate))
		}
	}

	component.RenderAlert(g.Alert, g.Rectangle, buffer)
}

func calculatePercent(g *Gauge) float64 {
	if g.curValue != g.minValue && g.maxValue != g.minValue {
		return (100 * (g.curValue - g.minValue)) / (g.maxValue - g.minValue)
	}
	return 0
}


================================================
FILE: component/gauge/gauge_test.go
================================================
package gauge

import "testing"

func Test_calculatePercent(t *testing.T) {
	type args struct {
		g *Gauge
	}
	tests := []struct {
		name string
		args args
		want float64
	}{
		{"should calculate percent between 0 and 60", args{&Gauge{minValue: 0, maxValue: 60, curValue: 45}}, 75},
		{"should calculate percent between 10 and 60", args{&Gauge{minValue: 10, maxValue: 60, curValue: 59}}, 98},
		{"should calculate percent between -20 and 60", args{&Gauge{minValue: -20, maxValue: 60, curValue: 0}}, 25},
		{"should calculate percent when cur value = min value", args{&Gauge{minValue: -10, maxValue: 60, curValue: -10}}, 0},
		{"should calculate percent when min value = max value", args{&Gauge{minValue: -124, maxValue: -124, curValue: -124}}, 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := calculatePercent(tt.args.g); got != tt.want {
				t.Errorf("calculatePercent() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: component/layout/layout.go
================================================
package layout

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/component/runchart"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"math"
	"time"
)

// Layout represents component arrangement on the screen
type Layout struct {
	ui.Block
	Components       []*component.Component
	statusbar        *component.StatusBar
	menu             *component.Menu
	ChangeModeEvents chan Mode
	mode             Mode
	selection        int
	positionsChanged bool
	startupTime      time.Time
}

type Mode rune

const (
	ModeDefault          Mode = 0
	ModeIntro            Mode = 1
	ModePause            Mode = 2
	ModeComponentSelect  Mode = 3
	ModeMenuOptionSelect Mode = 4
	ModeComponentMove    Mode = 5
	ModeComponentResize  Mode = 6
	ModeChartPinpoint    Mode = 7
)

const (
	minDimension    = 3
	statusbarHeight = 1
)

func NewLayout(statusline *component.StatusBar, menu *component.Menu) *Layout {

	width, height := ui.TerminalDimensions()
	block := *ui.NewBlock()
	block.SetRect(0, 0, width, height)
	statusline.SetRect(0, height-statusbarHeight, width, height)

	return &Layout{
		Block:            block,
		Components:       make([]*component.Component, 0),
		statusbar:        statusline,
		menu:             menu,
		mode:             ModeDefault,
		selection:        0,
		ChangeModeEvents: make(chan Mode, 10),
		startupTime:      time.Now(),
	}
}

func (l *Layout) AddComponent(cpt *component.Component) {
	l.Components = append(l.Components, cpt)
}

func (l *Layout) StartWithIntro() {
	l.mode = ModeIntro
}

func (l *Layout) changeMode(m Mode) {
	if m == ModeComponentResize || m == ModeComponentMove {
		l.positionsChanged = true
	}
	l.mode = m
	l.ChangeModeEvents <- m
}

func (l *Layout) HandleMouseClick(x int, y int) {
	if l.mode == ModeIntro {
		return
	}
	l.menu.Idle()
	selected, i := l.findComponentAtPoint(image.Point{X: x, Y: y})
	if selected == nil {
		l.changeMode(ModeDefault)
	} else {
		l.selection = i
		l.menu.Highlight(selected)
		l.changeMode(ModeComponentSelect)
	}
}

func (l *Layout) HandleKeyboardEvent(e string) {

	selected := l.getSelection()

	switch e {
	case console.KeyPause1, console.KeyPause2:
		if l.mode == ModePause {
			l.changeMode(ModeDefault)
			l.statusbar.TogglePause()
		} else {
			if selected.Type == config.TypeRunChart {
				selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection}
			}
			l.menu.Idle()
			l.changeMode(ModePause)
			l.statusbar.TogglePause()
		}
	case console.KeyEnter:
		switch l.mode {
		case ModeComponentSelect:
			l.menu.Choose()
			l.changeMode(ModeMenuOptionSelect)
		case ModeMenuOptionSelect:
			option := l.menu.GetSelectedOption()
			switch option {
			case component.MenuOptionMove:
				l.changeMode(ModeComponentMove)
				l.menu.MoveOrResize()
			case component.MenuOptionResize:
				l.changeMode(ModeComponentResize)
				l.menu.MoveOrResize()
			case component.MenuOptionPinpoint:
				l.changeMode(ModeChartPinpoint)
				l.menu.Idle()
				selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 0}
			case component.MenuOptionResume:
				l.changeMode(ModeDefault)
				l.menu.Idle()
			}
		case ModeComponentMove:
			fallthrough
		case ModeComponentResize:
			l.menu.Idle()
			l.changeMode(ModeDefault)
			break
		}
	case console.KeyEsc:
		l.resetAlerts()
		switch l.mode {
		case ModeChartPinpoint:
			selected.CommandChannel <- &data.Command{Type: runchart.CommandDisableSelection}
			fallthrough
		case ModeComponentSelect:
			fallthrough
		case ModeMenuOptionSelect:
			l.menu.Idle()
			l.changeMode(ModeDefault)
		case ModeComponentMove:
			fallthrough
		case ModeComponentResize:
			l.menu.Idle()
			l.changeMode(ModeDefault)
		}
	case console.KeyLeft:
		switch l.mode {
		case ModeDefault:
			l.changeMode(ModeComponentSelect)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeChartPinpoint:
			selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: -1}
		case ModeComponentSelect:
			l.moveSelection(e)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeComponentMove:
			selected.Move(-1, 0)
		case ModeComponentResize:
			selected.Resize(-1, 0)
		}
	case console.KeyRight:
		switch l.mode {
		case ModeDefault:
			l.changeMode(ModeComponentSelect)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeChartPinpoint:
			selected.CommandChannel <- &data.Command{Type: runchart.CommandMoveSelection, Value: 1}
		case ModeComponentSelect:
			l.moveSelection(e)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeComponentMove:
			selected.Move(1, 0)
		case ModeComponentResize:
			selected.Resize(1, 0)
		}
	case console.KeyUp:
		switch l.mode {
		case ModeDefault:
			l.changeMode(ModeComponentSelect)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeComponentSelect:
			l.moveSelection(e)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeMenuOptionSelect:
			l.menu.Up()
		case ModeComponentMove:
			selected.Move(0, -1)
		case ModeComponentResize:
			selected.Resize(0, -1)
		}
	case console.KeyDown:
		switch l.mode {
		case ModeDefault:
			l.changeMode(ModeComponentSelect)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeComponentSelect:
			l.moveSelection(e)
			l.menu.Highlight(l.getComponent(l.selection))
		case ModeMenuOptionSelect:
			l.menu.Down()
		case ModeComponentMove:
			selected.Move(0, 1)
		case ModeComponentResize:
			selected.Resize(0, 1)
		}
	}
}

func (l *Layout) ChangeDimensions(width, height int) {
	l.SetRect(0, 0, width, height)
}

func (l *Layout) getComponent(i int) *component.Component {
	return l.Components[i]
}

func (l *Layout) getSelection() *component.Component {
	return l.Components[l.selection]
}

func (l *Layout) moveSelection(direction string) {

	previouslySelected := l.getSelection()
	newlySelectedIndex := l.selection + 1

	for i, current := range l.Components {

		if current == previouslySelected {
			continue
		}

		if newlySelectedIndex >= len(l.Components) {
			newlySelectedIndex = i
		}

		var previouslySelectedCornerPoint image.Point
		var newlySelectedCornerPoint image.Point
		var currentCornerPoint image.Point

		switch direction {
		case console.KeyLeft:
			previouslySelectedCornerPoint = util.GetRectLeftSideCenter(previouslySelected.GetRect())
			newlySelectedCornerPoint = util.GetRectRightSideCenter(l.getComponent(newlySelectedIndex).GetRect())
			currentCornerPoint = util.GetRectRightSideCenter(current.GetRect())
		case console.KeyRight:
			previouslySelectedCornerPoint = util.GetRectRightSideCenter(previouslySelected.GetRect())
			newlySelectedCornerPoint = util.GetRectLeftSideCenter(l.getComponent(newlySelectedIndex).GetRect())
			currentCornerPoint = util.GetRectLeftSideCenter(current.GetRect())
		case console.KeyUp:
			previouslySelectedCornerPoint = util.GetRectTopSideCenter(previouslySelected.GetRect())
			newlySelectedCornerPoint = util.GetRectBottomSideCenter(l.getComponent(newlySelectedIndex).GetRect())
			currentCornerPoint = util.GetRectBottomSideCenter(current.GetRect())
		case console.KeyDown:
			previouslySelectedCornerPoint = util.GetRectBottomSideCenter(previouslySelected.GetRect())
			newlySelectedCornerPoint = util.GetRectTopSideCenter(l.getComponent(newlySelectedIndex).GetRect())
			currentCornerPoint = util.GetRectTopSideCenter(current.GetRect())
		}

		switch direction {
		case console.KeyLeft:
			fallthrough
		case console.KeyRight:
			if ui.AbsInt(currentCornerPoint.X-previouslySelectedCornerPoint.X) <= ui.AbsInt(newlySelectedCornerPoint.X-previouslySelectedCornerPoint.X) {
				if ui.AbsInt(currentCornerPoint.Y-previouslySelectedCornerPoint.Y) <= ui.AbsInt(newlySelectedCornerPoint.Y-previouslySelectedCornerPoint.Y) {
					newlySelectedIndex = i
				}
			}
		case console.KeyUp:
			fallthrough
		case console.KeyDown:
			if ui.AbsInt(currentCornerPoint.Y-previouslySelectedCornerPoint.Y) <= ui.AbsInt(newlySelectedCornerPoint.Y-previouslySelectedCornerPoint.Y) {
				if ui.AbsInt(currentCornerPoint.X-previouslySelectedCornerPoint.X) <= ui.AbsInt(newlySelectedCornerPoint.X-previouslySelectedCornerPoint.X) {
					newlySelectedIndex = i
				}
			}
		}
	}

	if newlySelectedIndex < len(l.Components) {
		l.selection = newlySelectedIndex
	}
}

func (l *Layout) Draw(buffer *ui.Buffer) {

	columnWidth := float64(l.GetRect().Dx()) / float64(console.ColumnsCount)
	rowHeight := float64(l.GetRect().Dy()-statusbarHeight) / float64(console.RowsCount)

	for _, c := range l.Components {
		rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight)
		c.SetRect(rectangle.Min.X, rectangle.Min.Y, rectangle.Max.X, rectangle.Max.Y)
	}

	for _, c := range l.Components {
		c.Draw(buffer)
	}

	l.statusbar.SetRect(
		0, l.GetRect().Dy()-statusbarHeight,
		l.GetRect().Dx(), l.GetRect().Dy())

	l.statusbar.Draw(buffer)
	l.menu.Draw(buffer)
}

func (l *Layout) findComponentAtPoint(point image.Point) (*component.Component, int) {

	columnWidth := float64(l.GetRect().Dx()) / float64(console.ColumnsCount)
	rowHeight := float64(l.GetRect().Dy()-statusbarHeight) / float64(console.RowsCount)

	for i, c := range l.Components {

		rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight)

		if point.In(rectangle) {
			return c, i
		}
	}

	return nil, -1
}

func calculateComponentCoordinates(c *component.Component, columnWidth float64, rowHeight float64) image.Rectangle {

	x1 := math.Floor(float64(c.Location.X) * columnWidth)
	y1 := math.Floor(float64(c.Location.Y) * rowHeight)
	x2 := x1 + math.Floor(float64(c.Size.X))*columnWidth
	y2 := y1 + math.Floor(float64(c.Size.Y))*rowHeight

	if x2-x1 < minDimension {
		x2 = x1 + minDimension
	}

	if y2-y1 < minDimension {
		y2 = y1 + minDimension
	}

	return image.Rectangle{Min: image.Point{
		X: int(x1), Y: int(y1)},
		Max: image.Point{X: int(x2), Y: int(y2)},
	}
}

func (l *Layout) resetAlerts() {
	for _, c := range l.Components {
		c.AlertChannel <- nil
	}
}

func (l *Layout) WerePositionsChanged() bool {
	return l.positionsChanged
}


================================================
FILE: component/menu.go
================================================
package component

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"image"
)

type Menu struct {
	*ui.Block
	options   []menuOption
	component Component
	mode      menuMode
	option    menuOption
	palette   console.Palette
}

type menuMode rune

const (
	menuModeIdle          menuMode = 0
	menuModeHighlight     menuMode = 1
	menuModeOptionSelect  menuMode = 2
	menuModeMoveAndResize menuMode = 3
)

type menuOption string

const (
	MenuOptionMove     menuOption = "MOVE"
	MenuOptionResize   menuOption = "RESIZE"
	MenuOptionPinpoint menuOption = "PINPOINT"
	MenuOptionResume   menuOption = "RESUME"
)

const (
	minimalMenuHeight = 8
)

func NewMenu(palette console.Palette) *Menu {
	return &Menu{
		Block:   NewBlock("", true, palette),
		options: []menuOption{MenuOptionMove, MenuOptionResize, MenuOptionPinpoint, MenuOptionResume},
		mode:    menuModeIdle,
		option:  MenuOptionMove,
		palette: palette,
	}
}

func (m *Menu) GetSelectedOption() menuOption {
	return m.option
}

func (m *Menu) Highlight(component *Component) {
	m.component = *component
	m.updateDimensions()
	m.mode = menuModeHighlight
	m.Title = component.Title
}

func (m *Menu) Choose() {
	m.mode = menuModeOptionSelect
}

func (m *Menu) Idle() {
	m.mode = menuModeIdle
}

func (m *Menu) Up() {
	for i := 1; i < len(m.options); i++ {
		if m.options[i] == m.option {
			m.option = m.options[i-1]
			break
		}
	}
	if m.option == MenuOptionPinpoint && m.component.Type != config.TypeRunChart {
		m.Up()
	}
}

func (m *Menu) Down() {
	for i := 0; i < len(m.options)-1; i++ {
		if m.options[i] == m.option {
			m.option = m.options[i+1]
			break
		}
	}
	if m.option == MenuOptionPinpoint && m.component.Type != config.TypeRunChart {
		m.Down()
	}
}

func (m *Menu) MoveOrResize() {
	m.mode = menuModeMoveAndResize
}

func (m *Menu) Draw(buffer *ui.Buffer) {

	if m.mode == menuModeIdle {
		return
	}

	m.updateDimensions()
	buffer.Fill(ui.NewCell(' ', ui.NewStyle(m.palette.ReverseColor)), m.GetRect())

	if m.Dy() > minimalMenuHeight {
		m.drawInnerBorder(buffer)
	}

	m.Block.Draw(buffer)

	switch m.mode {
	case menuModeHighlight:
		m.renderHighlight(buffer)
	case menuModeMoveAndResize:
		m.renderMoveAndResize(buffer)
	case menuModeOptionSelect:
		m.renderOptions(buffer)
	}
}

func (m *Menu) renderHighlight(buffer *ui.Buffer) {

	optionsText := "<ENTER> to view options"
	resumeText := "<ESC> to resume"

	if m.Dy() <= minimalMenuHeight {
		buffer.SetString(
			optionsText,
			ui.NewStyle(console.ColorDarkGrey),
			util.GetMiddlePoint(m.Block.Rectangle, optionsText, -1),
		)
		return
	}

	m.printAllDirectionsArrowSign(buffer, -1)

	optionsTextPoint := util.GetMiddlePoint(m.Block.Rectangle, optionsText, 3)
	if optionsTextPoint.Y+1 < m.Inner.Max.Y {
		buffer.SetString(
			optionsText,
			ui.NewStyle(console.ColorDarkGrey),
			util.GetMiddlePoint(m.Block.Rectangle, optionsText, 3),
		)
	}

	resumeTextPoint := util.GetMiddlePoint(m.Block.Rectangle, resumeText, 4)
	if resumeTextPoint.Y+1 < m.Inner.Max.Y {
		buffer.SetString(
			resumeText,
			ui.NewStyle(console.ColorDarkGrey),
			resumeTextPoint,
		)
	}
}

func (m *Menu) renderMoveAndResize(buffer *ui.Buffer) {

	saveText := "<ENTER> to save changes"

	if m.Dy() <= minimalMenuHeight {
		buffer.SetString(saveText, ui.NewStyle(console.ColorDarkGrey), util.GetMiddlePoint(m.Block.Rectangle, saveText, -1))
		return
	}

	m.printAllDirectionsArrowSign(buffer, -1)
	buffer.SetString(saveText, ui.NewStyle(console.ColorDarkGrey), util.GetMiddlePoint(m.Block.Rectangle, saveText, 3))
}

func (m *Menu) printAllDirectionsArrowSign(buffer *ui.Buffer, y int) {

	arrows := []string{
		"  ↑  ",
		"←   →",
		"  ↓  ",
	}

	for i, a := range arrows {
		util.PrintString(
			a,
			ui.NewStyle(console.ColorOlive),
			util.GetMiddlePoint(m.Block.Rectangle, a, i+y),
			buffer,
		)
	}
}

func (m *Menu) renderOptions(buffer *ui.Buffer) {

	highlightedStyle := ui.NewStyle(m.palette.ReverseColor, console.ColorOlive)
	regularStyle := ui.NewStyle(m.palette.BaseColor, m.palette.ReverseColor)

	offset := 1
	for _, option := range m.options {

		style := regularStyle
		if m.option == option {
			style = highlightedStyle
		}

		if option != MenuOptionPinpoint || m.component.Type == config.TypeRunChart {
			offset += 2
			point := util.GetMiddlePoint(m.Block.Rectangle, string(option), offset-6)
			buffer.SetString(string(option), style, point)
		}
	}
}

func (m *Menu) updateDimensions() {
	r := m.component.GetRect()
	m.SetRect(r.Min.X, r.Min.Y, r.Max.X, r.Max.Y)
}

func (m *Menu) drawInnerBorder(buffer *ui.Buffer) {

	verticalCell := ui.Cell{ui.VERTICAL_LINE, m.BorderStyle}
	horizontalCell := ui.Cell{ui.HORIZONTAL_LINE, m.BorderStyle}

	// draw lines
	buffer.Fill(horizontalCell, image.Rect(m.Min.X+2, m.Min.Y+2, m.Max.X-2, m.Min.Y))
	buffer.Fill(horizontalCell, image.Rect(m.Min.X+2, m.Max.Y-2, m.Max.X-2, m.Max.Y))
	buffer.Fill(verticalCell, image.Rect(m.Min.X+2, m.Min.Y+1, m.Min.X+3, m.Max.Y-1))
	buffer.Fill(verticalCell, image.Rect(m.Max.X-2, m.Min.Y, m.Max.X-3, m.Max.Y))

	// draw corners
	buffer.SetCell(ui.Cell{ui.TOP_LEFT, m.BorderStyle}, image.Pt(m.Min.X+2, m.Min.Y+1))
	buffer.SetCell(ui.Cell{ui.TOP_RIGHT, m.BorderStyle}, image.Pt(m.Max.X-3, m.Min.Y+1))
	buffer.SetCell(ui.Cell{ui.BOTTOM_LEFT, m.BorderStyle}, image.Pt(m.Min.X+2, m.Max.Y-2))
	buffer.SetCell(ui.Cell{ui.BOTTOM_RIGHT, m.BorderStyle}, image.Pt(m.Max.X-3, m.Max.Y-2))
}


================================================
FILE: component/runchart/grid.go
================================================
package runchart

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component/util"
	"image"
	"math"
	"time"
)

const defaultValueLength = 4

type chartGrid struct {
	timeRange    TimeRange
	timePerPoint time.Duration
	valueExtrema ValueExtrema
	linesCount   int
	maxTimeWidth int
	minTimeWidth int
}

func (c *RunChart) newChartGrid() chartGrid {

	linesCount := (c.Inner.Max.X - c.Inner.Min.X - c.grid.minTimeWidth) / xAxisGridWidth
	timeRange := c.getTimeRange(linesCount)

	return chartGrid{
		timeRange:    timeRange,
		timePerPoint: c.timescale / time.Duration(xAxisGridWidth),
		valueExtrema: getLocalExtrema(c.lines, timeRange),
		linesCount:   linesCount,
		maxTimeWidth: c.Inner.Max.X,
		minTimeWidth: defaultValueLength,
	}
}

func (c *RunChart) renderAxes(buffer *ui.Buffer) {

	// draw y axis labels
	if c.grid.valueExtrema.max != c.grid.valueExtrema.min {
		labelsCount := (c.Inner.Dy() - xAxisLabelsHeight - 1) / (yAxisLabelsIndent + yAxisLabelsHeight)
		valuePerY := (c.grid.valueExtrema.max - c.grid.valueExtrema.min) / float64(c.Inner.Dy()-xAxisLabelsHeight-3)
		for i := 0; i < int(labelsCount); i++ {
			val := c.grid.valueExtrema.max - (valuePerY * float64(i) * (yAxisLabelsIndent + yAxisLabelsHeight))
			fmt := util.FormatValue(val, c.scale)
			if len(fmt) > c.grid.minTimeWidth {
				c.grid.minTimeWidth = len(fmt)
			}
			buffer.SetString(
				fmt,
				ui.NewStyle(c.palette.BaseColor),
				image.Pt(c.Inner.Min.X, 1+c.Inner.Min.Y+i*(yAxisLabelsIndent+yAxisLabelsHeight)))
		}
	} else {
		fmt := util.FormatValue(c.grid.valueExtrema.max, c.scale)
		c.grid.minTimeWidth = len(fmt)
		buffer.SetString(
			fmt,
			ui.NewStyle(c.palette.BaseColor),
			image.Pt(c.Inner.Min.X, c.Inner.Min.Y+c.Inner.Dy()/2))
	}

	// draw origin cell
	buffer.SetCell(
		ui.NewCell(ui.BOTTOM_LEFT, ui.NewStyle(c.palette.BaseColor)),
		image.Pt(c.Inner.Min.X+c.grid.minTimeWidth, c.Inner.Max.Y-xAxisLabelsHeight-1))

	// draw x axis line
	for i := c.grid.minTimeWidth + 1; i < c.Inner.Dx(); i++ {
		buffer.SetCell(
			ui.NewCell(ui.HORIZONTAL_DASH, ui.NewStyle(c.palette.BaseColor)),
			image.Pt(i+c.Inner.Min.X, c.Inner.Max.Y-xAxisLabelsHeight-1))
	}

	// draw grid lines
	for y := 0; y < c.Inner.Dy()-xAxisLabelsHeight-2; y = y + 2 {
		for x := 1; x <= c.grid.linesCount; x++ {
			buffer.SetCell(
				ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(c.palette.MediumColor)),
				image.Pt(c.grid.maxTimeWidth-x*xAxisGridWidth, y+c.Inner.Min.Y+1))
		}
	}

	// draw y axis line
	for i := 0; i < c.Inner.Dy()-xAxisLabelsHeight-1; i++ {
		buffer.SetCell(
			ui.NewCell(ui.VERTICAL_DASH, ui.NewStyle(c.palette.BaseColor)),
			image.Pt(c.Inner.Min.X+c.grid.minTimeWidth, i+c.Inner.Min.Y))
	}

	// draw x axis time labels
	for i := 1; i <= c.grid.linesCount; i++ {
		labelTime := c.grid.timeRange.max.Add(time.Duration(-i) * c.timescale)
		buffer.SetString(
			labelTime.Format("15:04:05"),
			ui.NewStyle(c.palette.BaseColor),
			image.Pt(c.grid.maxTimeWidth-xAxisLabelsWidth/2-i*(xAxisGridWidth), c.Inner.Max.Y-1))
	}
}

func (c *RunChart) getTimeRange(linesCount int) TimeRange {

	if c.mode == ModePinpoint {
		return c.grid.timeRange
	}

	width := time.Duration(c.timescale.Nanoseconds() * int64(linesCount))
	max := time.Now()

	return TimeRange{
		max: max,
		min: max.Add(-width),
	}
}

func getLocalExtrema(items []TimeLine, timeRange TimeRange) ValueExtrema {

	if len(items) == 0 {
		return ValueExtrema{0, 0}
	}

	var max, min = -math.MaxFloat64, math.MaxFloat64

	for _, item := range items {
		started := false
		for i := len(item.points) - 1; i > 0; i-- {
			point := item.points[i]
			if timeRange.isInRange(point.time) {
				started = true
			} else if started == true && !timeRange.isInRange(point.time) {
				break
			}
			if point.value > max && timeRange.isInRange(point.time) {
				max = point.value
			}
			if point.value < min && timeRange.isInRange(point.time) {
				min = point.value
			}
		}
	}

	return ValueExtrema{max: max, min: min}
}

func (r *TimeRange) isInRange(time time.Time) bool {
	return time.After(r.min) && time.Before(r.max)
}


================================================
FILE: component/runchart/legend.go
================================================
package runchart

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component/util"
	"image"
	"math"
)

const (
	xAxisLegendIndent = 10
	yAxisLegendIndent = 1
	heightOnDefault   = 2
	heightOnPinpoint  = 4
	heightOnDetails   = 6

	timeFormat = "15:04:05.000"
)

type legend struct {
	Enabled bool
	Details bool
}

func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Rectangle) {

	if !c.legend.Enabled {
		return
	}

	height := heightOnDefault

	if c.mode == ModePinpoint {
		height = heightOnPinpoint
	} else if c.legend.Details {
		height = heightOnDetails
	}

	rowCount := (c.Dy() - 2*yAxisLegendIndent) / (height + yAxisLegendIndent)
	columnCount := int(math.Ceil(float64(len(c.lines)) / float64(rowCount)))
	columnWidth := getColumnWidth(c.mode, c.lines, c.scale)

	for col := 0; col < columnCount; col++ {
		for row := 0; row < rowCount; row++ {

			lineIndex := row + rowCount*col
			if len(c.lines) <= lineIndex {
				break
			}

			line := c.lines[row+rowCount*col]

			x := c.Inner.Max.X - (columnWidth+xAxisLegendIndent)*(col+1)
			y := c.Inner.Min.Y + yAxisLegendIndent + row*height

			titleStyle := ui.NewStyle(line.color)
			detailsStyle := ui.NewStyle(c.palette.BaseColor)

			buffer.SetString(string(ui.DOT), titleStyle, image.Pt(x-2, y))
			buffer.SetString(line.label, titleStyle, image.Pt(x, y))

			if c.mode == ModePinpoint {
				buffer.SetString(fmt.Sprintf("time  %s", line.selectionPoint.time.Format("15:04:05.000")), detailsStyle, image.Pt(x, y+1))
				buffer.SetString(fmt.Sprintf("value %s", util.FormatValue(line.selectionPoint.value, c.scale)), detailsStyle, image.Pt(x, y+2))
				continue
			}

			if !c.legend.Details {
				continue
			}

			details := [4]string{
				fmt.Sprintf("cur  %s", util.FormatValue(getCurrentValue(line), c.scale)),
				fmt.Sprintf("dlt %s", util.FormatDelta(getDiffWithPreviousValue(line), c.scale)),
				fmt.Sprintf("max  %s", util.FormatValue(line.extrema.max, c.scale)),
				fmt.Sprintf("min  %s", util.FormatValue(line.extrema.min, c.scale)),
			}

			for i, detail := range details {
				buffer.SetString(detail, detailsStyle, image.Pt(x, y+i+yAxisLegendIndent))
			}
		}
	}
}

func getColumnWidth(mode Mode, lines []TimeLine, scale int) int {

	if mode == ModePinpoint {
		return len(timeFormat)
	}

	width := len(util.FormatValue(0, scale))
	for _, line := range lines {
		if len(line.label) > width {
			width = len(line.label)
		}
	}
	return width
}

func getDiffWithPreviousValue(line TimeLine) float64 {
	if len(line.points) < 2 {
		return 0
	}
	return line.points[len(line.points)-1].value - line.points[len(line.points)-2].value
}

func getCurrentValue(line TimeLine) float64 {
	if len(line.points) == 0 {
		return 0
	}
	return line.points[len(line.points)-1].value
}


================================================
FILE: component/runchart/runchart.go
================================================
package runchart

import (
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"math"
	"sync"
	"time"

	ui "github.com/gizak/termui/v3"
)

const (
	xAxisGridWidth     = xAxisLabelsIndent + xAxisLabelsWidth
	xAxisLabelsHeight  = 1
	xAxisLabelsWidth   = 8
	xAxisLabelsIndent  = 2
	yAxisLabelsHeight  = 1
	yAxisLabelsIndent  = 1
	historyReserveMin  = 2
	xBrailleMultiplier = 2
	yBrailleMultiplier = 4
)

type Mode int

const (
	ModeDefault  Mode = 0
	ModePinpoint Mode = 1
)

const (
	CommandDisableSelection = "DISABLE_SELECTION"
	CommandMoveSelection    = "MOVE_SELECTION"
)

// RunChart displays observed data in a time sequence
type RunChart struct {
	*ui.Block
	*data.Consumer
	lines     []TimeLine
	grid      chartGrid
	timescale time.Duration
	mutex     *sync.Mutex
	mode      Mode
	selection time.Time
	scale     int
	legend    legend
	palette   console.Palette
}

type TimePoint struct {
	value      float64
	time       time.Time
	coordinate int
}

type TimeLine struct {
	points              []TimePoint
	extrema             ValueExtrema
	color               ui.Color
	label               string
	selectionCoordinate int
	selectionPoint      TimePoint
}

type TimeRange struct {
	max time.Time
	min time.Time
}

type ValueExtrema struct {
	max float64
	min float64
}

func NewRunChart(c config.RunChartConfig, palette console.Palette) *RunChart {

	chart := RunChart{
		Block:     component.NewBlock(c.Title, true, palette),
		Consumer:  data.NewConsumer(),
		lines:     []TimeLine{},
		timescale: calculateTimescale(*c.RateMs),
		mutex:     &sync.Mutex{},
		scale:     *c.Scale,
		mode:      ModeDefault,
		legend:    legend{Enabled: c.Legend.Enabled, Details: c.Legend.Details},
		palette:   palette,
	}

	for _, i := range c.Items {
		chart.AddLine(*i.Label, *i.Color)
	}

	go func() {
		for {
			select {
			case sample := <-chart.SampleChannel:
				chart.consumeSample(sample)
			case alert := <-chart.AlertChannel:
				chart.Alert = alert
			case command := <-chart.CommandChannel:
				switch command.Type {
				case CommandDisableSelection:
					chart.disableSelection()
				case CommandMoveSelection:
					chart.moveSelection(command.Value.(int))
				}
			}
		}
	}()

	return &chart
}

func (c *RunChart) newTimePoint(value float64) TimePoint {
	now := time.Now()
	return TimePoint{
		value:      value,
		time:       now,
		coordinate: c.calculateTimeCoordinate(now),
	}
}

func (c *RunChart) Draw(buffer *ui.Buffer) {

	c.mutex.Lock()
	c.Block.Draw(buffer)
	c.grid = c.newChartGrid()

	drawArea := image.Rect(
		c.Inner.Min.X+c.grid.minTimeWidth+2, c.Inner.Min.Y,
		c.Inner.Max.X, c.Inner.Max.Y-xAxisLabelsHeight-1,
	)

	c.renderAxes(buffer)
	c.renderLines(buffer, drawArea)
	c.renderLegend(buffer, drawArea)
	component.RenderAlert(c.Alert, c.Rectangle, buffer)
	c.mutex.Unlock()
}

func (c *RunChart) AddLine(Label string, color ui.Color) {
	line := TimeLine{
		points:  []TimePoint{},
		color:   color,
		label:   Label,
		extrema: ValueExtrema{max: -math.MaxFloat64, min: math.MaxFloat64},
	}
	c.lines = append(c.lines, line)
}

func (c *RunChart) consumeSample(sample *data.Sample) {

	float, err := util.ParseFloat(sample.Value)
	if err != nil {
		c.HandleConsumeFailure("Failed to parse a number", err, sample)
		return
	}

	c.HandleConsumeSuccess()

	c.mutex.Lock()

	index := -1
	for i, line := range c.lines {
		if line.label == sample.Label {
			index = i
		}
	}

	line := c.lines[index]

	if float < line.extrema.min {
		line.extrema.min = float
	}
	if float > line.extrema.max {
		line.extrema.max = float
	}

	line.points = append(line.points, c.newTimePoint(float))
	c.lines[index] = line

	if len(line.points)%100 == 0 {
		c.trimOutOfRangeValues()
	}

	c.mutex.Unlock()
}

func (c *RunChart) renderLines(buffer *ui.Buffer, drawArea image.Rectangle) {

	canvas := ui.NewCanvas()
	canvas.Rectangle = drawArea

	if len(c.lines) == 0 || len(c.lines[0].points) == 0 {
		return
	}

	selectionCoordinate := c.calculateTimeCoordinate(c.selection)
	selectionPoints := make(map[int]image.Point)

	probe := c.lines[0].points[0]
	probeCalculatedCoordinate := c.calculateTimeCoordinate(probe.time)
	delta := probe.coordinate - probeCalculatedCoordinate

	for i, line := range c.lines {

		xPoint := make(map[int]image.Point)
		xOrder := make([]int, 0)

		// move selection on a delta, if it was instantiated after cursor move
		if line.selectionCoordinate != 0 {
			line.selectionCoordinate -= delta
			c.lines[i].selectionCoordinate = line.selectionCoordinate
		}

		for j, timePoint := range line.points {

			timePoint.coordinate -= delta
			line.points[j] = timePoint

			var y int
			if c.grid.valueExtrema.max == c.grid.valueExtrema.min {
				y = (drawArea.Dy() - 2) / 2
			} else {
				valuePerY := (c.grid.valueExtrema.max - c.grid.valueExtrema.min) / float64(drawArea.Dy()-2)
				y = int(float64(timePoint.value-c.grid.valueExtrema.min) / valuePerY)
			}

			point := image.Pt(timePoint.coordinate, drawArea.Max.Y-y-1)

			if _, exists := xPoint[point.X]; exists {
				continue
			}

			if !point.In(drawArea) {
				continue
			}

			if line.selectionCoordinate == 0 {
				// instantiate selection coordinate as the closest point to the cursor time
				if len(line.points) > j+1 && ui.AbsInt(timePoint.coordinate-selectionCoordinate) > ui.AbsInt(line.points[j+1].coordinate-selectionCoordinate) {
					selectionPoints[i] = point
					c.lines[i].selectionPoint = timePoint
				}
			} else if timePoint.coordinate == line.selectionCoordinate {
				selectionPoints[i] = point
			}

			xPoint[point.X] = point
			xOrder = append(xOrder, point.X)
		}

		for i, x := range xOrder {

			currentPoint := xPoint[x]
			var previousPoint image.Point

			if i == 0 {
				previousPoint = currentPoint
			} else {
				previousPoint = xPoint[xOrder[i-1]]
			}

			canvas.SetLine(
				braillePoint(previousPoint),
				braillePoint(currentPoint),
				line.color,
			)
		}
	}

	canvas.Draw(buffer)

	if c.mode == ModePinpoint {
		for lineIndex, point := range selectionPoints {
			buffer.SetCell(ui.NewCell(console.SymbolSelection, ui.NewStyle(c.lines[lineIndex].color)), point)
			if c.lines[lineIndex].selectionCoordinate == 0 {
				c.lines[lineIndex].selectionCoordinate = point.X
			}
		}
	}
}

func (c *RunChart) trimOutOfRangeValues() {

	minRangeTime := c.grid.timeRange.min.Add(-time.Minute * time.Duration(historyReserveMin))

	for i, item := range c.lines {
		lastOutOfRangeValueIndex := -1

		for j, point := range item.points {
			if point.time.Before(minRangeTime) {
				lastOutOfRangeValueIndex = j
			}
		}

		if lastOutOfRangeValueIndex > 0 {
			item.points = append(item.points[:0], item.points[lastOutOfRangeValueIndex+1:]...)
			c.lines[i] = item
		}
	}
}

func (c *RunChart) calculateTimeCoordinate(t time.Time) int {
	timeDeltaWithGridMaxTime := c.grid.timeRange.max.Sub(t).Nanoseconds()
	timeDeltaToPaddingRelation := float64(timeDeltaWithGridMaxTime) / float64(c.timescale.Nanoseconds())
	return c.grid.maxTimeWidth - int(math.Ceil(float64(xAxisGridWidth)*timeDeltaToPaddingRelation))
}

func (c *RunChart) moveSelection(shift int) {

	if c.mode == ModeDefault {
		c.mode = ModePinpoint
		c.selection = getMidRangeTime(c.grid.timeRange)
		return
	}

	c.selection = c.selection.Add(c.grid.timePerPoint * time.Duration(shift))
	if c.selection.After(c.grid.timeRange.max) {
		c.selection = c.grid.timeRange.max
	} else if c.selection.Before(c.grid.timeRange.min) {
		c.selection = c.grid.timeRange.min
	}

	for i := range c.lines {
		c.lines[i].selectionCoordinate = 0
	}
}

func (c *RunChart) disableSelection() {
	if c.mode == ModePinpoint {
		c.mode = ModeDefault
		return
	}
}

func getMidRangeTime(r TimeRange) time.Time {
	delta := r.max.Sub(r.min)
	return r.max.Add(-delta / 2)
}

// time duration between grid lines
func calculateTimescale(rateMs int) time.Duration {

	multiplier := rateMs * xAxisGridWidth / 2
	timescale := time.Duration(time.Millisecond * time.Duration(multiplier)).Round(time.Second)

	if timescale.Seconds() == 0 {
		return time.Second
	}

	return timescale
}

func braillePoint(point image.Point) image.Point {
	return image.Point{X: point.X * xBrailleMultiplier, Y: point.Y * yBrailleMultiplier}
}


================================================
FILE: component/sparkline/sparkline.go
================================================
package sparkline

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/component/util"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
	"sync"
)

// SparkLine displays general shape of a measurement variation over time
type SparkLine struct {
	*ui.Block
	*data.Consumer
	values   []float64
	maxValue float64
	minValue float64
	scale    int
	gradient []ui.Color
	palette  console.Palette
	mutex    *sync.Mutex
}

func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *SparkLine {

	line := &SparkLine{
		Block:    component.NewBlock(c.Title, true, palette),
		Consumer: data.NewConsumer(),
		values:   []float64{},
		scale:    *c.Scale,
		gradient: *c.Gradient,
		palette:  palette,
		mutex:    &sync.Mutex{},
	}

	go func() {
		for {
			select {
			case sample := <-line.SampleChannel:
				line.consumeSample(sample)
			case alert := <-line.AlertChannel:
				line.Alert = alert
			}
		}
	}()

	return line
}

func (s *SparkLine) consumeSample(sample *data.Sample) {

	float, err := util.ParseFloat(sample.Value)
	if err != nil {
		s.HandleConsumeFailure("Failed to parse a number", err, sample)
		return
	}

	s.HandleConsumeSuccess()

	s.values = append(s.values, float)
	max, min := s.values[0], s.values[0]

	for i := len(s.values) - 1; i >= 0; i-- {
		if len(s.values)-i > s.Dx() {
			break
		}
		if s.values[i] > max {
			max = s.values[i]
		}
		if s.values[i] < min {
			min = s.values[i]
		}
	}

	s.maxValue = max
	s.minValue = min

	if len(s.values)%100 == 0 {
		s.mutex.Lock()
		s.trimOutOfRangeValues(s.Dx())
		s.mutex.Unlock()
	}
}

func (s *SparkLine) trimOutOfRangeValues(maxSize int) {
	if maxSize < len(s.values) {
		s.values = append(s.values[:0], s.values[len(s.values)-maxSize:]...)
	}
}

func (s *SparkLine) Draw(buffer *ui.Buffer) {

	s.mutex.Lock()

	textStyle := ui.NewStyle(s.palette.BaseColor)

	height := s.Dy() - 2
	minValue := util.FormatValue(s.minValue, s.scale)
	maxValue := util.FormatValue(s.maxValue, s.scale)
	curValue := util.FormatValue(0, s.scale)

	if len(s.values) > 0 {
		curValue = util.FormatValue(s.values[len(s.values)-1], s.scale)
	}

	indent := 2 + util.Max([]int{
		len(minValue), len(maxValue), len(curValue),
	})

	for i := len(s.values) - 1; i >= 0; i-- {

		n := len(s.values) - i

		if n > s.Dx()-indent-3 {
			break
		}

		top := 0

		if s.maxValue != s.minValue {
			top = int((s.values[i] - s.minValue) * float64(height) / (s.maxValue - s.minValue))
		}

		for j := 0; j <= top; j++ {
			buffer.SetCell(ui.NewCell(console.SymbolVerticalBar, ui.NewStyle(console.GetGradientColor(s.gradient, j, height))), image.Pt(s.Inner.Max.X-n-indent, s.Inner.Max.Y-j-1))
		}

		if i == len(s.values)-1 {
			buffer.SetString(curValue, textStyle, image.Pt(s.Inner.Max.X-n-indent+2, s.Inner.Max.Y-top-1))
			if s.maxValue != s.minValue {
				buffer.SetString(minValue, textStyle, image.Pt(s.Inner.Max.X-n-indent+2, s.Max.Y-2))
				buffer.SetString(maxValue, textStyle, image.Pt(s.Inner.Max.X-n-indent+2, s.Min.Y+1))
			}
		}
	}

	s.mutex.Unlock()

	s.Block.Draw(buffer)
	component.RenderAlert(s.Alert, s.Rectangle, buffer)
}


================================================
FILE: component/sparkline/sparkline_test.go
================================================
package sparkline

import (
	"testing"
)

func TestSparkLine_trimOutOfRangeValues(t *testing.T) {
	type Sparkline struct {
		maxSize      int
		expectedSize int
		values       []float64
	}
	tests := []struct {
		name      string
		sparkline Sparkline
	}{
		{"should trimOutOfRangeValues values to the max size", Sparkline{maxSize: 5, expectedSize: 5, values: []float64{1, 2, 3, 4, 5, 6, 7, 8}}},
		{"should not trimOutOfRangeValues values if max size is bigger than values len", Sparkline{maxSize: 5, expectedSize: 3, values: []float64{1, 2, 3}}},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &SparkLine{
				values: tt.sparkline.values,
			}
			s.trimOutOfRangeValues(tt.sparkline.maxSize)
			if len(s.values) != tt.sparkline.expectedSize {
				t.Errorf("Values size after trimOutOfRangeValues is %v, but needed to be %v", len(s.values), tt.sparkline.expectedSize)
			}
		})
	}
}


================================================
FILE: component/statusbar.go
================================================
package component

import (
	"fmt"
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/console"
	"image"
)

const (
	pauseText      = "  P A U S E D  "
	bindingsIndent = 3
)

type StatusBar struct {
	*ui.Block
	keyBindings []string
	text        string
	pause       bool
}

func NewStatusBar(configFileName string, palette console.Palette) *StatusBar {

	block := *ui.NewBlock()
	block.Border = false
	text := fmt.Sprintf(" %s %s @ %s", console.AppTitle, console.AppVersion, configFileName)

	return &StatusBar{
		Block: NewBlock("", false, palette),
		text:  text,
		keyBindings: []string{
			"(q) quit",
			"(p) pause",
			"(<->) selection",
			"(ESC) reset alerts",
		},
	}
}

func (s *StatusBar) Draw(buffer *ui.Buffer) {

	buffer.Fill(ui.NewCell(' ', ui.NewStyle(console.ColorClear, console.GetMenuColorReverse())), s.GetRect())

	indent := bindingsIndent
	for _, binding := range s.keyBindings {
		buffer.SetString(binding, ui.NewStyle(console.GetMenuColor(), console.GetMenuColorReverse()), image.Pt(s.Max.X-len(binding)-indent, s.Min.Y))
		indent += bindingsIndent + len(binding)
	}

	buffer.SetString(s.text, ui.NewStyle(console.GetMenuColor(), console.GetMenuColorReverse()), s.Min)

	if s.pause {
		buffer.SetString(pauseText, ui.NewStyle(console.GetMenuColorReverse(), console.GetMenuColor()), image.Pt(s.Max.X-s.Dx()/2-len(pauseText)/2, s.Min.Y))
	}

	s.Block.Draw(buffer)
}

func (s *StatusBar) TogglePause() {
	s.pause = !s.pause
}


================================================
FILE: component/textbox/textbox.go
================================================
package textbox

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/component"
	"github.com/sqshq/sampler/config"
	"github.com/sqshq/sampler/console"
	"github.com/sqshq/sampler/data"
	"image"
)

// TextBox represents a component with regular text
type TextBox struct {
	*ui.Block
	*data.Consumer
	alert  *data.Alert
	text   string
	border bool
	style  ui.Style
}

func NewTextBox(c config.TextBoxConfig, palette console.Palette) *TextBox {

	color := c.Color
	if color == nil {
		color = &palette.BaseColor
	}

	box := TextBox{
		Block:    component.NewBlock(c.Title, *c.Border, palette),
		Consumer: data.NewConsumer(),
		style:    ui.NewStyle(*color),
	}

	go func() {
		for {
			select {
			case sample := <-box.SampleChannel:
				box.text = sample.Value
			case alert := <-box.AlertChannel:
				box.alert = alert
			}
		}
	}()

	return &box
}

func (t *TextBox) Draw(buffer *ui.Buffer) {

	t.Block.Draw(buffer)

	cells := ui.ParseStyles(t.text, ui.Theme.Paragraph.Text)
	cells = ui.WrapCells(cells, uint(t.Inner.Dx()-2))

	rows := ui.SplitCells(cells, '\n')

	for y, row := range rows {
		if y+t.Inner.Min.Y >= t.Inner.Max.Y-1 {
			break
		}
		row = ui.TrimCells(row, t.Inner.Dx()-2)
		for _, cx := range ui.BuildCellWithXArray(row) {
			x, cell := cx.X, cx.Cell
			cell.Style = t.style
			buffer.SetCell(cell, image.Pt(x+1, y+1).Add(t.Inner.Min))
		}
	}

	component.RenderAlert(t.alert, t.Rectangle, buffer)
}


================================================
FILE: component/util/format.go
================================================
package util

import (
	"bytes"
	"fmt"
	ui "github.com/gizak/termui/v3"
	"image"
	"math"
	"strconv"
	"strings"
)

var AsciiLogo = []string{
	"                               __         ",
	"   _________  ____ ___  ____  / /__  _____",
	"  / ___/ __ `/ __ `__ \\/ __ \\/ / _ \\/ ___/",
	" (__  ) /_/ / / / / / / /_/ / /  __/ /    ",
	"/____/\\__,_/_/ /_/ /_/ .___/_/\\___/_/     ",
	"                    /_/                   ",
}

func FormatValue(value float64, scale int) string {
	if math.Abs(value) == math.MaxFloat64 {
		return "Inf"
	}
	return formatTrailingDigits(addRadixChars(value), scale)
}

func FormatDelta(value float64, scale int) string {

	abs := math.Abs(value)
	val := value
	scl := scale

	postfix := ""

	if abs > 1000 && abs < 1000000 {
		val = float64(value) / 1000
		postfix = "k"
	} else if abs > 1000000 && abs < 1000000000 {
		val = float64(value) / 1000000
		postfix = "M"
	} else if abs > 1000000000 {
		val = float64(value) / 1000000000
		postfix = "B"
	}

	if abs > 1000 {
		scl = 1
	}

	if val == 0 {
		return " 0"
	} else if val > 0 {
		return fmt.Sprintf("+%s%s", FormatValue(val, scl), postfix)
	} else {
		return fmt.Sprintf("%s%s", FormatValue(val, scl), postfix)
	}
}

func addRadixChars(value float64) string {
	buf := &bytes.Buffer{}
	if value < 0 {
		buf.Write([]byte{'-'})
		value = 0 - value
	}

	radix := []byte{','}

	parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".")
	pos := 0
	if len(parts[0])%3 != 0 {
		pos += len(parts[0]) % 3
		buf.WriteString(parts[0][:pos])
		buf.Write(radix)
	}
	for ; pos < len(parts[0]); pos += 3 {
		buf.WriteString(parts[0][pos : pos+3])
		buf.Write(radix)
	}
	buf.Truncate(buf.Len() - 1)

	if len(parts) > 1 {
		buf.Write([]byte{'.'})
		buf.WriteString(parts[1])
	}
	return buf.String()
}

func formatTrailingDigits(value string, scale int) string {

	if i := strings.Index(value, "."); i >= 0 {

		formatted := value

		if scale <= 0 {
			formatted = value[:i]
		}

		i++
		if i+scale < len(value) {
			formatted = value[:i+scale]
		}

		formatted = strings.TrimRight(formatted, "0")

		return strings.TrimRight(formatted, ".")
	}

	return value
}

func GetMiddlePoint(rectangle image.Rectangle, text string, offset int) image.Point {
	return image.Pt(rectangle.Min.X+rectangle.Dx()/2-len(text)/2, rectangle.Max.Y-rectangle.Dy()/2+offset)
}

func PrintString(s string, style ui.Style, p image.Point, buffer *ui.Buffer) {
	for i, char := range s {
		buffer.SetCell(ui.Cell{Rune: char, Style: style}, image.Pt(p.X+i, p.Y))
	}
}


================================================
FILE: component/util/format_test.go
================================================
package util

import "testing"

func TestFormatValue(t *testing.T) {
	type args struct {
		value float64
		scale int
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{"should format float value with scale = 1", args{94.123, 1}, "94.1"},
		{"should format float value with scale = 0", args{94.123, 0}, "94"},
		{"should format float value with trailing zeros", args{94.100, 5}, "94.1"},
		{"should format float value with radix char and trailing zeros", args{9423000.00123, 2}, "9,423,000"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := FormatValue(tt.args.value, tt.args.scale); got != tt.want {
				t.Errorf("FormatValue() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: component/util/geometry.go
================================================
package util

import (
	"image"
)

func GetRectLeftSideCenter(rect image.Rectangle) image.Point {
	return image.Point{
		X: rect.Min.X,
		Y: rect.Min.Y + rect.Dy()/2,
	}
}

func GetRectRightSideCenter(rect image.Rectangle) image.Point {
	return image.Point{
		X: rect.Max.X,
		Y: rect.Min.Y + rect.Dy()/2,
	}
}

func GetRectTopSideCenter(rect image.Rectangle) image.Point {
	return image.Point{
		X: rect.Min.X + rect.Dx()/2,
		Y: rect.Min.Y,
	}
}

func GetRectBottomSideCenter(rect image.Rectangle) image.Point {
	return image.Point{
		X: rect.Min.X + rect.Dx()/2,
		Y: rect.Max.Y,
	}
}

func GetRectCoordinates(area image.Rectangle, width int, height int) (int, int, int, int) {
	x1 := area.Min.X + area.Dx()/2 - width/2
	y1 := area.Min.Y + area.Dy()/2 - height
	return x1, y1, x1 + width, y1 + height + 2
}


================================================
FILE: component/util/geometry_test.go
================================================
package util

import "testing"
import "image"

func TestGetRectLeftSideCenter(t *testing.T) {
	rect := image.Rect(10, 10, 20, 20)
	expected := image.Point{
		X: 10,
		Y: 15,
	}
	result := GetRectLeftSideCenter(rect)
	if result != expected {
		t.Errorf("GetRectLeftSideCenter was incorrect. Expected %v, got %v.", expected, result)
	}
}

func TestGetRectRightSideCenter(t *testing.T) {
	rect := image.Rect(10, 10, 20, 21)
	expected := image.Point{
		X: 20,
		Y: 15,
	}
	result := GetRectRightSideCenter(rect)
	if result != expected {
		t.Errorf("GetRectRightSideCenter was incorrect. Expected %v, got %v.", expected, result)
	}
}

func TestGetRectTopSideCenter(t *testing.T) {
	rect := image.Rect(10, 10, 20, 20)
	expected := image.Point{
		X: 15,
		Y: 10,
	}
	result := GetRectTopSideCenter(rect)
	if result != expected {
		t.Errorf("GetRectTopSideCenter was incorrect. Expected %v, got %v.", expected, result)
	}
}

func TestGetRectBottomSideCenter(t *testing.T) {
	rect := image.Rect(10, 9, 20, 20)
	expected := image.Point{
		X: 15,
		Y: 20,
	}
	result := GetRectBottomSideCenter(rect)
	if result != expected {
		t.Errorf("GetRectBottomSideCenter was incorrect. Expected %v, got %v.", expected, result)
	}
}


================================================
FILE: component/util/math.go
================================================
package util

func Max(numbers []int) int {

	max := numbers[0]

	for _, n := range numbers {
		if n > max {
			max = n
		}
	}

	return max
}


================================================
FILE: component/util/parse.go
================================================
package util

import (
	"strconv"
	"strings"
)

func ParseFloat(input string) (float64, error) {

	clean := strings.TrimSpace(input)
	clean = strings.Replace(clean, ",", ".", -1) // replace decimal comma with decimal point

	if strings.Contains(clean, "\n") {
		lastIndex := strings.LastIndex(clean, "\n")
		clean = clean[lastIndex+1:]
	}

	return strconv.ParseFloat(clean, 64)
}


================================================
FILE: component/util/parse_test.go
================================================
package util

import "testing"

func TestParseFloat(t *testing.T) {
	type args struct {
		input string
	}
	tests := []struct {
		name    string
		args    args
		want    float64
		wantErr bool
	}{
		{"should parse a regular number", args{"123"}, 123, false},
		{"should parse a float with decimal point", args{"123.456"}, 123.456, false},
		{"should parse a float with decimal comma", args{"123,456"}, 123.456, false},
		{"should parse a regular number with spaces, tabs and line breaks", args{"         \t 123 \t \n    "}, 123, false},
		{"should parse a last line in the given string", args{"123\n456"}, 456, false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParseFloat(tt.args.input)
			if (err != nil) != tt.wantErr {
				t.Errorf("ParseFloat() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if got != tt.want {
				t.Errorf("ParseFloat() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: config/arrangement.go
================================================
package config

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/console"
	"image"
)

func (c *Config) setDefaultArrangement() {

	components := getComponents(c)

	if allHaveNoPosition(components) {
		setSingleComponentPosition(components[0])
	}

	for _, component := range components {
		if component.Position == nil || len(component.Position) == 0 {

			lc := getLargestComponent(components)
			le := getLargestEmptySpaceRectangle(components)

			if getSquare(lc) > le.Dx()*le.Dy()*2 {
				arrangeIntoLargestComponent(component, lc)
			} else {
				arrangeIntoLargestEmptySpace(component, le)
			}
		}
	}
}

func arrangeIntoLargestComponent(component *ComponentConfig, lc *ComponentConfig) {

	lr := lc.GetRectangle()
	var w, h, ws, hs int

	if lr.Dx()/2 > lr.Dy() {
		w = lr.Dx() / 2
		h = lr.Dy()
		ws = w
		hs = 0
	} else {
		w = lr.Dx()
		h = lr.Dy() / 2
		ws = 0
		hs = h
	}
	component.Position = [][]int{
		{lr.Min.X + ws, lr.Min.Y + hs},
		{w, h},
	}
	lc.Position = [][]int{
		{lr.Min.X, lr.Min.Y},
		{w, h},
	}
}

func arrangeIntoLargestEmptySpace(component *ComponentConfig, le image.Rectangle) {
	component.Position = [][]int{
		{le.Min.X, le.Min.Y},
		{le.Dx(), le.Dy()},
	}
}

func allHaveNoPosition(components []*ComponentConfig) bool {
	for _, component := range components {
		if component.Position != nil {
			return false
		}
	}
	return true
}

func getLargestComponent(components []*ComponentConfig) *ComponentConfig {

	largestComponent := components[0]

	for _, component := range components {
		if getSquare(component) > getSquare(largestComponent) {
			largestComponent = component
		}
	}

	return largestComponent
}

func getSquare(c *ComponentConfig) int {
	r := c.GetRectangle()
	return r.Dx() * r.Dy()
}

func getComponents(c *Config) []*ComponentConfig {

	var components []*ComponentConfig

	for i := range c.RunCharts {
		components = append(components, &c.RunCharts[i].ComponentConfig)
	}
	for i := range c.BarCharts {
		components = append(components, &c.BarCharts[i].ComponentConfig)
	}
	for i := range c.Gauges {
		components = append(components, &c.Gauges[i].ComponentConfig)
	}
	for i := range c.SparkLines {
		components = append(components, &c.SparkLines[i].ComponentConfig)
	}
	for i := range c.AsciiBoxes {
		components = append(components, &c.AsciiBoxes[i].ComponentConfig)
	}
	for i := range c.TextBoxes {
		components = append(components, &c.TextBoxes[i].ComponentConfig)
	}

	return components
}

func setSingleComponentPosition(c *ComponentConfig) {
	w := int(console.ColumnsCount)
	h := int(console.RowsCount * 0.6)
	c.Position = [][]int{
		{(console.ColumnsCount - w) / 2, (console.RowsCount - h) / 2},
		{w, h},
	}
}

func getLargestEmptySpaceRectangle(components []*ComponentConfig) image.Rectangle {

	grid := [console.RowsCount][console.ColumnsCount]int{}

	for _, component := range components {
		rect := component.GetRectangle()
		for r := ui.MinInt(console.RowsCount, rect.Min.Y); r < ui.MinInt(console.RowsCount, rect.Max.Y); r++ {
			for c := ui.MinInt(console.ColumnsCount, rect.Min.X); c < ui.MinInt(console.ColumnsCount, rect.Max.X); c++ {
				grid[r][c] = 1
			}
		}
	}

	mr := image.ZR

	for row := 0; row < console.RowsCount; row++ {
		histogram := createHistogram(grid, row)
		r := calcMaxRectangle(histogram, row)
		if r.Dx()*r.Dy() > mr.Dx()*mr.Dy() {
			mr = r
		}
	}

	return mr
}

func calcMaxRectangle(histogram [console.ColumnsCount]int, row int) image.Rectangle {

	maxRectangle := image.ZR
	maxArea := 0

	for i := 0; i < len(histogram); i++ {

		height := histogram[i]
		if height > maxArea {
			maxArea = height
			maxRectangle = image.Rect(i, row, i, row+height)
		}

		for j := i - 1; j >= 0; j-- {
			width := i - j + 1
			height = ui.MinInt(height, histogram[j])
			if width*height > maxArea {
				maxArea = width * height
				maxRectangle = image.Rect(j, row, j+width, row+height)
			}
		}
	}

	return maxRectangle
}

func createHistogram(grid [console.RowsCount][console.ColumnsCount]int, row int) [console.ColumnsCount]int {
	histogram := [console.ColumnsCount]int{}
	for column := 0; column < console.ColumnsCount; column++ {
		histogram[column] = countEmptyCellsBelow(grid, row, column)
	}
	return histogram
}

func countEmptyCellsBelow(grid [console.RowsCount][console.ColumnsCount]int, row int, column int) int {
	count := 0
	for r := row; r < console.RowsCount; r++ {
		if grid[r][column] == 1 {
			return count
		}
		count++
	}
	return count
}


================================================
FILE: config/component.go
================================================
package config

import (
	ui "github.com/gizak/termui/v3"
	"github.com/sqshq/sampler/console"
	"image"
)

type ComponentType rune

const (
	TypeRunChart  ComponentType = 0
	TypeBarChart  ComponentType = 1
	TypeSparkLine ComponentType = 2
	TypeTextBox   ComponentType = 3
	TypeAsciiBox  ComponentType = 4
	TypeGauge     ComponentType = 5
)

type ComponentConfig struct {
	Title    string          `yaml:"title"`
	Position [][]int         `yaml:"position,flow"`
	RateMs   *int            `yaml:"rate-ms,omitempty"`
	Triggers []TriggerConfig `yaml:"triggers,omitempty"`
	Type     ComponentType   `yaml:",omitempty"`
}

func (c *ComponentConfig) GetLocation() Location {
	return Location{X: c.Position[0][0], Y: c.Position[0][1]}
}

func (c *ComponentConfig) GetSize() Size {
	return Size{X: c.Position[1][0], Y: c.Position[1][1]}
}

func (c *ComponentConfig) GetRectangle() image.Rectangle {
	if c.Position == nil || len(c.Position) == 0 {
		return image.ZR
	}
	return image.Rect(
		c.Position[0][0],
		c.Position[0][1],
		c.Position[0][0]+c.Position[1][0],
		c.Position[0][1]+c.Position[1][1])
}

type TriggerConfig struct {
	Title     string         `yaml:"title"`
	Condition string         `yaml:"condition"`
	Actions   *ActionsConfig `yaml:"actions,omitempty"`
}

type ActionsConfig struct {
	TerminalBell *bool   `yaml:"terminal-bell,omitempty"`
	Sound        *bool   `yaml:"sound,omitempty"`
	Visual       *bool   `yaml:"visual,omitempty"`
	Script       *string `yaml:"script,omitempty"`
}

type GaugeConfig struct {
	ComponentConfig `yaml:",inline"`
	Scale           *int      `yaml:"scale,omitempty"`
	Color           *ui.Color `yaml:"color,omitempty"`
	PercentOnly     *bool     `yaml:"percent-only,omitempty"`
	Cur             Item      `yaml:"cur"`
	Max             Item      `yaml:"max"`
	Min             Item      `yaml:"min"`
}

type SparkLineConfig struct {
	ComponentConfig `yaml:",inline"`
	Scale           *int        `yaml:"scale,omitempty"`
	Item            Item        `yaml:",inline"`
	Gradient        *[]ui.Color `yaml:",omitempty"`
}

type BarChartConfig struct {
	ComponentConfig `yaml:",inline"`
	Scale           *int   `yaml:"scale,omitempty"`
	Items           []Item `yaml:"items"`
}

type AsciiBoxConfig struct {
	ComponentConfig `yaml:",inline"`
	Item            `yaml:",inline"`
	Border          *bool              `yaml:"border,omitempty"`
	Font            *console.AsciiFont `yaml:"font,omitempty"`
}

type TextBoxConfig struct {
	ComponentConfig `yaml:",inline"`
	Item            `yaml:",inline"`
	Border          *bool `yaml:"border,omitempty"`
}

type RunChartConfig struct {
	ComponentConfig `yaml:",inline"`
	Legend          *LegendConfig `yaml:"legend,omitempty"`
	Scale           *int          `yaml:"scale,omitempty"`
	Items           []Item        `yaml:"items"`
}

type LegendConfig struct {
	Enabled bool `yaml:"enabled"`
	Details bool `yaml:"details"`
}

type Item struct {
	Label               *string   `yaml:"label,omitempty"`
	Color               *ui.Color `yaml:"color,omitempty"`
	Pty                 *bool     `yaml:"pty,omitempty"`
	InitScript          *string   `yaml:"init,omitempty"`
	MultiStepInitScript *[]string `yaml:"multistep-init,omitempty"`
	SampleScript        *string   `yaml:"sample"`
	TransformScript     *string   `yaml:"transform,omitempty"`
}

type Location struct {
	X int
	Y int
}

type Size struct {
	X int
	Y int
}

type ComponentSettings struct {
	Type     ComponentType
	Title    string
	Size     Size
	Location Location
}

func getPosition(location Location, size Size) [][]int {
	return [][]int{
		{location.X, location.Y},
		{size.X, size.Y},
	}
}


================================================
FILE: config/config.go
================================================
package config

import (
	"fmt"
	"github.com/jessevdk/go-flags"
	"github.com/sqshq/sampler/console"
	"gopkg.in/yaml.v3"
	"io/ioutil"
	"log"
	"os"
)

type Config struct {
	Theme      *console.Theme    `yaml:"theme,omitempty"`
	Variables  map[string]string `yaml:"variables,omitempty"`
	RunCharts  []RunChartConfig  `yaml:"runcharts,omitempty"`
	BarCharts  []BarChartConfig  `yaml:"barcharts,omitempty"`
	Gauges     []GaugeConfig     `yaml:"gauges,omitempty"`
	SparkLines []SparkLineConfig `yaml:"sparklines,omitempty"`
	TextBoxes  []TextBoxConfig   `yaml:"textboxes,omitempty"`
	AsciiBoxes []AsciiBoxConfig  `yaml:"asciiboxes,omitempty"`
}

func LoadConfig() (*Config, Options) {

	var opt Options
	_, err := flags.Parse(&opt)

	if err != nil {
		console.Exit("")
	}

	if opt.Version == true {
		console.Exit(console.AppVersion)
	}

	if opt.ConfigFile == nil {
		console.Exit("Please specify config file using --config flag. Example: sampler --config example.yml")
	}

	cfg := readFile(opt.ConfigFile)
	cfg.validate()
	cfg.setDefaults()

	return cfg, opt
}

func Update(settings []ComponentSettings, options Options) {
	cfg := readFile(options.ConfigFile)
	for _, s := range settings {
		componentConfig := cfg.findComponent(s.Type, s.Title)
		componentConfig.Position = getPosition(s.Location, s.Size)
	}
	saveFile(cfg, options.ConfigFile)
}

func (c *Config) findComponent(componentType ComponentType, componentTitle string) *ComponentConfig {

	switch componentType {
	case TypeRunChart:
		for i, component := range c.RunCharts {
			if component.Title == componentTitle {
				return &c.RunCharts[i].ComponentConfig
			}
		}
	case TypeBarChart:
		for i, component := range c.BarCharts {
			if component.Title == componentTitle {
				return &c.BarCharts[i].ComponentConfig
			}
		}
	case TypeGauge:
		for i, component := range c.Gauges {
			if component.Title == componentTitle {
				return &c.Gauges[i].ComponentConfig
			}
		}
	case TypeSparkLine:
		for i, component := range c.SparkLines {
			if component.Title == componentTitle {
				return &c.SparkLines[i].ComponentConfig
			}
		}
	case TypeAsciiBox:
		for i, component := range c.AsciiBoxes {
			if component.Title == componentTitle {
				return &c.AsciiBoxes[i].ComponentConfig
			}
		}
	case TypeTextBox:
		for i, component := range c.TextBoxes {
			if component.Title == componentTitle {
				return &c.TextBoxes[i].ComponentConfig
			}
		}
	}

	panic(fmt.Sprintf(
		"Failed to find component type %v with title %v", componentType, componentTitle))
}

func readFile(location *string) *Config {

	yamlFile, err := ioutil.ReadFile(*location)
	if err != nil {
		log.Fatalf("Failed to read config file: %s", *location)
	}

	cfg := new(Config)
	err = yaml.Unmarshal(yamlFile, cfg)

	if err != nil {
		log.Fatalf("Failed to read config file: %v", err)
	}

	return cfg
}

func saveFile(config *Config, fileName *string) {
	file, err := yaml.Marshal(config)
	if err != nil {
		log.Fatalf("Failed to marshal config file: %v", err)
	}
	err = ioutil.WriteFile(*fileName, file, os.ModePerm)
	if err != nil {
		log.Fatalf("Failed to save config file: %v", err)
	}
}


================================================
FILE: config/default.go
================================================
package config

import (
	"github.com/sqshq/sampler/console"
)

const (
	defaultRateMs = 1000
	defaultScale  = 1
	defaultTheme  = console.ThemeDark
)

func (c *Config) setDefaults() {
	c.setDefaultValues()
	c.setDefaultItemSettings()
	c.setDefaultArrangement()
}

func (c *Config) setDefaultValues() {

	if c.Theme == nil {
		t := defaultTheme
		c.Theme = &t
	}

	for i, chart := range c.RunCharts {

		setDefaultTriggersValues(chart.Triggers)
		chart.ComponentConfig.Type = TypeRunChart

		if chart.RateMs == nil {
			r := defaultRateMs
			chart.RateMs = &r
		}
		if chart.Scale == nil {
			p := defaultScale
			chart.Scale = &p
		}
		if chart.Legend == nil {
			chart.Legend = &LegendConfig{true, true}
			c.RunCharts[i] = chart
		}
		c.RunCharts[i] = chart
	}

	for i, line := range c.SparkLines {

		setDefaultTriggersValues(line.Triggers)
		line.ComponentConfig.Type = TypeSparkLine
		line.Item.Label = &line.Title

		if line.RateMs == nil {
			r := defaultRateMs
			line.RateMs = &r
		}
		if line.Scale == nil {
			p := defaultScale
			line.Scale = &p
		}
		c.SparkLines[i] = line
	}

	for i, chart := range c.BarCharts {

		setDefaultTriggersValues(chart.Triggers)
		chart.ComponentConfig.Type = TypeBarChart

		if chart.RateMs == nil {
			r := defaultRateMs
			chart.RateMs = &r
		}
		if chart.Scale == nil {
			p := defaultScale
			chart.Scale = &p
		}
		c.BarCharts[i] = chart
	}

	for i, g := range c.Gauges {

		setDefaultTriggersValues(g.Triggers)
		g.ComponentConfig.Type = TypeGauge

		if g.RateMs == nil {
			r := defaultRateMs
			g.RateMs = &r
		}
		if g.Scale == nil {
			p := defaultScale
			g.Scale = &p
		}

		cur := "cur"
		max := "max"
		min := "min"

		g.Cur.Label = &cur
		g.Max.Label = &max
		g.Min.Label = &min

		c.Gauges[i] = g
	}

	for i, box := range c.AsciiBoxes {

		setDefaultTriggersValues(box.Triggers)
		box.ComponentConfig.Type = TypeAsciiBox

		if box.RateMs == nil {
			r := defaultRateMs
			box.RateMs = &r
		}
		if box.Label == nil {
			label := box.Title
			box.Label = &label
		}
		if box.Font == nil {
			font := console.AsciiFont2D
			box.Font = &font
		}
		if box.Border == nil {
			border := true
			box.Border = &border
		}
		c.AsciiBoxes[i] = box
	}

	for i, box := range c.TextBoxes {

		setDefaultTriggersValues(box.Triggers)
		box.ComponentConfig.Type = TypeTextBox

		if box.RateMs == nil {
			r := defaultRateMs
			box.RateMs = &r
		}
		if box.Label == nil {
			label := box.Title
			box.Label = &label
		}
		if box.Border == nil {
			border := true
			box.Border = &border
		}

		c.TextBoxes[i] = box
	}
}

func setDefaultTriggersValues(triggers []TriggerConfig) {

	defaultTerminalBell := false
	defaultSound := false
	defaultVisual := false

	for i, trigger := range triggers {

		if trigger.Actions == nil {
			trigger.Actions = &ActionsConfig{TerminalBell: &defaultTerminalBell, Sound: &defaultSound, Visual: &defaultVisual, Script: nil}
		} else {
			if trigger.Actions.TerminalBell == nil {
				trigger.Actions.TerminalBell = &defaultTerminalBell
			}
			if trigger.Actions.Sound == nil {
				trigger.Actions.Sound = &defaultSound
			}
			if trigger.Actions.Visual == nil {
				trigger.Actions.Visual = &defaultVisual
			}
		}

		triggers[i] = trigger
	}
}

func (c *Config) setDefaultItemSettings() {

	palette := console.GetPalette(*c.Theme)
	colorsCount := len(palette.ContentColors)
	defaultPty := false
	defaultPercentOnly := false

	for _, ch := range c.RunCharts {
		for j, item := range ch.Items {
			if item.Color == nil {
				item.Color = &palette.ContentColors[j%colorsCount]
			}
			if item.Pty == nil {
				item.Pty = &defaultPty
			}
			ch.Items[j] = item
		}
	}

	for _, b := range c.BarCharts {
		for j, item := range b.Items {
			if item.Color == nil {
				item.Color = &palette.ContentColors[j%colorsCount]
			}
			if item.Pty == nil {
				item.Pty = &defaultPty
			}
			b.Items[j] = item
		}
	}

	for i, s := range c.SparkLines {
		s.Gradient = &palette.GradientColors[i%(len(palette.GradientColors))]
		if s.Item.Pty == nil {
			s.Item.Pty = &defaultPty
		}
		c.SparkLines[i] = s
	}

	for i, g := range c.Gauges {
		if g.Min.Pty == nil {
			g.Min.Pty = &defaultPty
		}
		if g.Max.Pty == nil {
			g.Max.Pty = &defaultPty
		}
		if g.Cur.Pty == nil {
			g.Cur.Pty = &defaultPty
		}
		if g.Color == nil {
			g.Color = &palette.ContentColors[i%colorsCount]
		}
		if g.PercentOnly == nil {
			g.PercentOnly = &defaultPercentOnly
		}
		c.Gauges[i] = g
	}

	for i, a := range c.AsciiBoxes {
		if a.Item.Pty == nil {
			a.Item.Pty = &defaultPty
		}
		c.AsciiBoxes[i] = a
	}

	for i, t := range c.TextBoxes {
		if t.Item.Pty == nil {
			t.Item.Pty = &defaultPty
		}
		c.TextBoxes[i] = t
	}
}


================================================
FILE: config/options.go
================================================
package config

// Options with cli flags
type Options struct {
	ConfigFile  *string  `short:"c" long:"config" description:"Path to YAML config file"`
	Environment []string `short:"e" long:"env" description:"Specify name=value variable to use in script placeholder as $name. This flag takes precedence over the same name variables, specified in config yml"`
	Version     bool     `short:"v" long:"version" description:"Print version"`
}


================================================
FILE: config/validator.go
================================================
package config

import (
	"fmt"
	"github.com/sqshq/sampler/console"
)

func (c *Config) validate() {

	var components []ComponentConfig

	for _, c := range c.RunCharts {
		components = append(components, c.ComponentConfig)
		validateLabelsUniqueness(c.Title, c.Items)
		validateItemsScripts(c.Title, c.Items)
	}
	for _, c := range c.BarCharts {
		components = append(components, c.ComponentConfig)
		validateLabelsUniqueness(c.Title, c.Items)
		validateItemsScripts(c.Title, c.Items)
	}
	for _, c := range c.SparkLines {
		components = append(components, c.ComponentConfig)
		validateItemScripts(c.Title, c.Item)
	}
	for _, c := range c.Gauges {
		components = append(components, c.ComponentConfig)
		validateItemScripts(c.Title, c.Min)
		validateItemScripts(c.Title, c.Max)
		validateItemScripts(c.Title, c.Cur)
	}
	for _, c := range c.AsciiBoxes {
		components = append(components, c.ComponentConfig)
		validateItemScripts(c.Title, c.Item)
	}
	for _, c := range c.TextBoxes {
		components = append(components, c.ComponentConfig)
		validateItemScripts(c.Title, c.Item)
	}

	validateTitlesUniqueness(components)
}

func validateItemsScripts(title string, items []Item) {
	for _, i := range items {
		validateItemScripts(title, i)
	}
}

func validateItemScripts(title string, i Item) {
	if i.InitScript != nil && i.MultiStepInitScript != nil {
		console.Exit(fmt.Sprintf("Config validation error: both init and multistep-init scripts are not allowed for '%s'", title))
	}
	if i.SampleScript == nil {
		console.Exit(fmt.Sprintf("Config validation error: sample script should be specified for '%s'", title))
	}
}

func validateLabelsUniqueness(title string, items []Item) {
	labels := make(map[string]bool)
	for _, i := range items {
		label := *i.Label
		if _, contains := labels[label]; contains {
			console.Exit(fmt.Sprintf("Config validation error: item labels should be unique. Please rename '%s' for '%s'", label, title))
		}
		labels[label] = true
	}
}

func validateTitlesUniqueness(components []ComponentConfig) {
	titles := make(map[string]bool)
	for _, c := range components {
		if _, contains := titles[c.Title]; contains {
			console.Exit(fmt.Sprintf("Config validation error: component titles should be unique. Please rename '%s'", c.Title))
		}
		titles[c.Title] = true
	}
}


================================================
FILE: console/console.go
================================================
package console

import (
	"fmt"
	"log"
	"os"
	"time"

	ui "github.com/gizak/termui/v3"
)

const (
	MaxRenderInterval = 1000 * time.Millisecond
	MinRenderInterval = 100 * time.Millisecond
	ColumnsCount      = 80
	RowsCount         = 40
	AppTitle          = "sampler"
	AppVersion        = "1.1.0"
)

const (
	BellCharacter = "\a"
)

type AsciiFont string

const (
	AsciiFont2D AsciiFont = "2d"
	AsciiFont3D AsciiFont = "3d"
)

func Init() {

	fmt.Printf("\033]0;%s\007", AppTitle)

	if err := ui.Init(); err != nil {
		log.Fatalf("Failed to initialize ui: %v", err)
	}
}

// Close function calls Close from termui package,
// which closes termbox-go
func Close() {
	ui.Close()
}

// Exit function exits the program successfully
func Exit(message string) {
	if len(message) > 0 {
		println(message)
	}
	os.Exit(0)
}


================================================
FILE: console/palette.go
================================================
package console

import (
	"fmt"
	"runtime"

	ui "github.com/gizak/termui/v3"
)

type Theme string

const (
	ThemeDark  Theme = "dark"
	ThemeLight Theme = "light"
)

const (
	ColorOlive       ui.Color = 178
	ColorDeepSkyBlue ui.Color = 39
	ColorDeepPink    ui.Color = 198
	ColorCian        ui.Color = 43
	ColorOrange      ui.Color = 166
	ColorPurple      ui.Color = 129
	ColorGreen       ui.Color = 64
	ColorDarkRed     ui.Color = 88
	ColorBlueViolet  ui.Color = 57
	ColorDarkGrey    ui.Color = 238
	ColorLightGrey   ui.Color = 254
	ColorGrey        ui.Color = 242
	ColorWhite       ui.Color = 15
	ColorBlack       ui.Color = 0
	ColorClear       ui.Color = -1
)

const (
	menuColorNix            ui.Color = 255
	menuColorReverseNix     ui.Color = 235
	menuColorWindows        ui.Color = 255
	menuColorReverseWindows ui.Color = 0
)

type Palette struct {
	ContentColors  []ui.Color
	GradientColors [][]ui.Color
	BaseColor      ui.Color
	MediumColor    ui.Color
	ReverseColor   ui.Color
}

// GetPalette returns a color palette based on specified theme
func GetPalette(theme Theme) Palette {
	switch theme {
	case ThemeDark:
		return Palette{
			ContentColors:  []ui.Color{ColorOlive, ColorDeepSkyBlue, ColorDeepPink, ColorWhite, ColorGrey, ColorGreen, ColorOrange, ColorCian, ColorPurple},
			GradientColors: [][]ui.Color{{39, 33, 62, 93, 164, 161}, {95, 138, 180, 179, 178, 178}},
			BaseColor:      ColorWhite,
			MediumColor:    ColorDarkGrey,
			ReverseColor:   ColorBlack,
		}
	case ThemeLight:
		return Palette{
			ContentColors:  []ui.Color{ColorBlack, ColorDarkRed, ColorBlueViolet, ColorGrey, ColorGreen},
			GradientColors: [][]ui.Color{{250, 248, 246, 244, 242, 240, 238, 236, 234, 232, 16}},
			BaseColor:      ColorBlack,
			MediumColor:    ColorLightGrey,
			ReverseColor:   ColorWhite,
		}
	default:
		panic(fmt.Sprintf("Specified theme is not supported: %v", theme))
	}
}

// GetMenuColor returns a color based on the
// operating system target
func GetMenuColor() ui.Color {
	switch runtime.GOOS {
	case "windows":
		return menuColorWindows
	default:
		return menuColorNix
	}
}

// GetMenuColorReverse returns a color based on the
// operating system target
func GetMenuColorReverse() ui.Color {
	switch runtime.GOOS {
	case "windows":
		return menuColorReverseWindows
	default:
		return menuColorReverseNix
	}
}

func GetGradientColor(gradient []ui.Color, cur int, max int) ui.Color {
	ratio := float64(len(gradient)) / float64(max)
	index := int(ratio * float64(cur))
	if index > len(gradient)-1 {
		index = len(gradient) - 1
	}
	return gradient[index]
}


================================================
FILE: console/palette_test.go
================================================
package console

import (
	"testing"

	ui "github.com/gizak/termui/v3"
)

func TestGetPalette(t *testing.T) {
	var (
		darkPalette = Palette{
			BaseColor:    ColorWhite,
			ReverseColor: ColorBlack,
		}
		lightPalette = Palette{
			BaseColor:    ColorBlack,
			ReverseColor: ColorWhite,
		}
	)

	tests := []struct {
		name  string
		input Theme
		Palette
	}{
		{"should return dark theme with base color white", ThemeDark, darkPalette},
		{"should return light theme with base color black", ThemeLight, lightPalette},
	}

	for _, test := range tests {
		palette := GetPalette(test.input)
		if got := palette.BaseColor; got != test.BaseColor {
			t.Errorf("GetPalette(%q) = %d, want %d", test.input, got, test.BaseColor)
		}
	}
}

func TestGetPaletteInvalidTheme(t *testing.T) {
	const invalid Theme = "invalid"

	defer func() {
		if r := recover(); r == nil {
			t.Errorf("GetPalette(%q) should have panicked", invalid)
		}
	}()

	GetPalette(invalid)
}

func TestGetGradientColor(t *testing.T) {
	type args struct {
		gradient []ui.Color
		cur      int
		max      int
	}

	var (
		lightThemeGradientInput = args{
			gradient: []ui.Color{
				250, 248, 246, 244, 242, 240, 238, 236, 234, 232, 16,
			},
			cur: 200,
			max: 250,
		}

		darkThemeGradientInput = args{
			gradient: []ui.Color{
				39, 33, 62, 93, 164, 161,
			},
			cur: 40,
			max: 180,
		}

		grey ui.Color = 234

		blue ui.Color = 33
	)

	tests := []struct {
		name string
		args
		want ui.Color
	}{
		{"should return color grey", lightThemeGradientInput, grey},
		{"should return color blue", darkThemeGradientInput, blue},
	}

	for _, test := range tests {
		gradientColor := GetGradientColor(
			test.gradient,
			test.cur,
			test.max,
		)

		if got := gradientColor; got != test.want {
			t.Errorf("GetGradientColor(%v) = %d, want %d", test.args, got, test.want)
		}
	}
}


================================================
FILE: console/signal.go
================================================
package console

const (
	SignalResize = "<Resize>"
	SignalClick  = "<MouseLeft>"
)

const (
	KeyPause1 = "p"
	KeyPause2 = "P"
	KeyQuit1  = "q"
	KeyQuit2  = "Q"
	KeyQuit3  = "<C-c>"
	KeyLeft   = "<Left>"
	KeyRight  = "<Right>"
	KeyUp     = "<Up>"
	KeyDown   = "<Down>"
	KeyEnter  = "<Enter>"
	KeyEsc    = "<Escape>"
)


================================================
FILE: console/symbol.go
================================================
package console

const (
	SymbolSelection     rune = '▲'
	SymbolVerticalBar   rune = '▎'
	SymbolHorizontalBar rune = '═'
)


================================================
FILE: data/consumer.go
================================================
package data

import (
	ui "github.com/gizak/termui/v3"
	"strings"
)

type Consumer struct {
	SampleChannel  chan *Sample
	AlertChannel   chan *Alert
	CommandChannel chan *Command
	Alert          *Alert
}

func (c *Consumer) HandleConsumeSuccess() {
	if c.Alert != nil && c.Alert.Recoverable {
		c.Alert = nil
	}
}

func (c *Consumer) HandleConsumeFailure(title string, err error, sample *Sample) {
	c.AlertChannel <- &Alert{
		Title:       strings.ToUpper(title),
		Text:        err.Error(),
		Color:       sample.Color,
		Recoverable: true,
	}
}

type Sample struct {
	Label string
	Value string
	Color *ui.Color
}

type Alert struct {
	Title       string
	Text        string
	Color       *ui.Color
	Recoverable bool
}

type Command struct {
	Type  string
	Value interface{}
}

func NewConsumer() *Consumer {
	return &Consumer{
		SampleChannel:  make(chan *Sample, 10),
		AlertChannel:   make(chan *Alert, 10),
		CommandChannel: make(chan *Command, 10),
	}
}


================================================
FILE: data/consumer_test.go
================================================
package data

import (
	"errors"
	"reflect"
	"testing"
)

func TestConsumer_HandleConsumeSuccess(t *testing.T) {
	tests := []struct {
		name          string
		existingAlert *Alert
		expectedAlert *Alert
	}{
		{"alert nil", nil, nil},
		{"alert present, not recoverable", &Alert{Recoverable: false}, &Alert{Recoverable: false}},
		{"alert present, recoverable", &Alert{Recoverable: true}, nil},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := NewConsumer()
			c.Alert = tt.existingAlert
			c.HandleConsumeSuccess()
			if !reflect.DeepEqual(c.Alert, tt.expectedAlert) {
				t.Errorf("unexpected alert state after HandleConsumeSuccess(), want %v, got %v", tt.expectedAlert, c.Alert)
			}
		})
	}
}

func TestConsumer_HandleConsumeFailure(t *testing.T) {
	tests := []struct {
		name   string
		title  string
		err    error
		sample *Sample
		want   *Alert
	}{
		{
			"basic",
			"test",
			errors.New("test"),
			&Sample{
				Label: "test",
				Value: "test",
				Color: nil,
			},
			&Alert{
				Title:       "TEST",
				Text:        "test",
				Color:       nil,
				Recoverable: true,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			c := NewConsumer()
			c.HandleConsumeFailure(tt.title, tt.err, tt.sample)
			got := <-c.AlertChannel
			if !reflect.DeepEqual(tt.want, got) {
				t.Errorf("unexpected alert state after HandleConsumeFailure(), want %v, got %v", got, tt.want)
			}
		})
	}
}

func TestNewConsumer(t *testing.T) {
	tests := []struct {
		name      string
		checkFunc func(c *Consumer) bool
	}{
		{
			"initialized after creation",
			func(c *Consumer) bool {
				return c != nil
			},
		},
		{
			"alert is nil after creation",
			func(c *Consumer) bool {
				return c.Alert == nil
			},
		},
		{
			"command channel is initialized after creation",
			func(c *Consumer) bool {
				return c.CommandChannel != nil
			},
		},
		{
			"alert channel is initialized after creation",
			func(c *Consumer) bool {
				return c.AlertChannel != nil
			},
		},
		{
			"sample channel is initialized after creation",
			func(c *Consumer) bool {
				return c.SampleChannel != nil
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := NewConsumer(); !tt.checkFunc(got) {
				t.Errorf("unexpected consumer state after NewConsumer() = %v", got)
			}
		})
	}
}


================================================
FILE: data/error.go
================================================
package data

import (
	"fmt"
	"os/exec"
)

func getErrorMessage(err error) string {

	exitErr, ok := err.(*exec.ExitError)
	message := err.Error()

	if ok {
		stderr := string(exitErr.Stderr)
		if len(stderr) != 0 {
			message = fmt.Sprintf("%.200s", stderr)
		}
	}

	return message
}


================================================
FILE: data/int.go
================================================
package data

import "time"

const (
	startupTimeout  = 200 * time.Millisecond
	minAwaitTimeout = 100 * time.Millisecond
	maxAwaitTimeout = 1 * time.Second
)

type InteractiveShell interface {
	init() error
	execute() (string, error)
}


================================================
FILE: data/int_basic.go
================================================
package data

import (
	"bufio"
	"errors"
	"fmt"
	"github.com/lunixbochs/vtclean"
	"io"
	"os/exec"
	"strings"
	"time"
)

// BasicInteractiveShell represents non-PTY interactive shell sampling metadata
type BasicInteractiveShell struct {
	item      *Item
	variables []string
	stdoutCh  chan string
	stderrCh  chan string
	stdin     io.WriteCloser
	cmd       *exec.Cmd
	errCount  int
	timeout   time.Duration
}

func (s *BasicInteractiveShell) init() error {

	cmd := exec.Command("sh", "-c", s.item.initScripts[0])
	enrichEnvVariables(cmd, s.variables)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}

	stderr, err := cmd.StderrPipe()
	if err != nil {
		return err
	}

	stdin, err := cmd.StdinPipe()
	if err != nil {
		return err
	}

	stdoutScanner := bufio.NewScanner(stdout)
	stderrScanner := bufio.NewScanner(stderr)

	stdoutCh := make(chan string)
	stderrCh := make(chan string)

	go func() {
		for stdoutScanner.Scan() {
			stdoutCh <- stdoutScanner.Text()
			stderrCh <- stderrScanner.Text()
		}
	}()

	s.stdoutCh = stdoutCh
	s.stderrCh = stderrCh
	s.stdin = stdin
	s.cmd = cmd

	err = cmd.Start()
	if err != nil {
		return err
	}

	for i := 1; i < len(s.item.initScripts); i++ {
		_, err := io.WriteString(s.stdin, fmt.Sprintf(" %s\n", s.item.initScripts[i]))
		if err != nil {
			return err
		}
		time.Sleep(startupTimeout) // TODO wait until cmd complete
	}

	return nil
}

func (s *BasicInteractiveShell) execute() (string, error) {

	if s.stdin == nil {
		return "", nil
	}

	_, err := io.WriteString(s.stdin, fmt.Sprintf(" %s\n", s.item.sampleScript))
	if err != nil {
		s.errCount++
		if s.errCount > errorThreshold {
			_ = s.cmd.Wait()
			s.item.basicShell = nil // restart session
		}
		return "", fmt.Errorf("failed to execute command: %s", err)
	}

	timeout := make(chan bool, 1)

	go func() {
		time.Sleep(s.timeout)
		timeout <- true
	}()

	var resultText strings.Builder
	var errorText strings.Builder

	for {
		select {
		case stdout := <-s.stdoutCh:
			if len(stdout) > 0 {
				resultText.WriteString(stdout)
				resultText.WriteString("\n")
			}
		case stderr := <-s.stderrCh:
			if len(stderr) > 0 {
				errorText.WriteString(stderr)
				errorText.WriteString("\n")
			}
		case <-timeout:
			if errorText.Len() > 0 {
				return "", errors.New(errorText.String())
			}
			return s.item.transform(vtclean.Clean(resultText.String(), false))
		}
	}
}


================================================
FILE: data/int_pty.go
================================================
//+build !windows

package data

import (
	"bufio"
	"fmt"
	"github.com/kr/pty"
	"github.com/lunixbochs/vtclean"
	"io"
	"os/exec"
	"strings"
	"time"
)

// PtyInteractiveShell represents PTY interactive shell sampling metadata
type PtyInteractiveShell struct {
	item      *Item
	variables []string
	cmd       *exec.Cmd
	file      io.WriteCloser
	ch        chan string
	errCount  int
	timeout   time.Duration
}

func (s *PtyInteractiveShell) init() error {

	cmd := exec.Command("sh", "-c", s.item.initScripts[0])
	enrichEnvVariables(cmd, s.variables)

	file, err := pty.Start(cmd)
	if err != nil {
		return err
	}

	scanner := bufio.NewScanner(file)
	channel := make(chan string)

	go func() {
		for scanner.Scan() {
			channel <- scanner.Text()
		}
	}()

	s.cmd = cmd
	s.file = file
	s.ch = channel

	_, err = file.Read(make([]byte, 4096))
	if err != nil {
		return err
	}

	time.Sleep(startupTimeout)

	for i := 1; i < len(s.item.initScripts); i++ {
		_, err = io.WriteString(s.file, fmt.Sprintf(" %s\n", s.item.initScripts[i]))
		if err != nil {
			return err
		}
		time.Sleep(startupTimeout) // TODO wait until cmd complete
	}

	return nil
}

func (s *PtyInteractiveShell) execute() (string, error) {

	_, err := io.WriteString(s.file, fmt.Sprintf(" %s\n", s.item.sampleScript))
	if err != nil {
		s.errCount++
		if s.errCount > errorThreshold {
			_ = s.cmd.Wait()
			_ = s.file.Close()
			s.item.ptyShell = nil // restart session
		}
		return "", fmt.Errorf("failed to execute command: %s", err)
	}

	softTimeout := make(chan bool, 1)
	hardTimeout := make(chan bool, 1)

	go func() {
		time.Sleep(s.getAwaitT
Download .txt
gitextract_kb9i7998/

├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE.md
├── README.md
├── asset/
│   ├── bindata.go
│   ├── file.go
│   └── player.go
├── component/
│   ├── alert.go
│   ├── asciibox/
│   │   └── asciibox.go
│   ├── barchart/
│   │   └── barchart.go
│   ├── block.go
│   ├── component.go
│   ├── gauge/
│   │   ├── gauge.go
│   │   └── gauge_test.go
│   ├── layout/
│   │   └── layout.go
│   ├── menu.go
│   ├── runchart/
│   │   ├── grid.go
│   │   ├── legend.go
│   │   └── runchart.go
│   ├── sparkline/
│   │   ├── sparkline.go
│   │   └── sparkline_test.go
│   ├── statusbar.go
│   ├── textbox/
│   │   └── textbox.go
│   └── util/
│       ├── format.go
│       ├── format_test.go
│       ├── geometry.go
│       ├── geometry_test.go
│       ├── math.go
│       ├── parse.go
│       └── parse_test.go
├── config/
│   ├── arrangement.go
│   ├── component.go
│   ├── config.go
│   ├── default.go
│   ├── options.go
│   └── validator.go
├── console/
│   ├── console.go
│   ├── palette.go
│   ├── palette_test.go
│   ├── signal.go
│   └── symbol.go
├── data/
│   ├── consumer.go
│   ├── consumer_test.go
│   ├── error.go
│   ├── int.go
│   ├── int_basic.go
│   ├── int_pty.go
│   ├── int_pty_windows.go
│   ├── item.go
│   ├── sampler.go
│   └── trigger.go
├── event/
│   └── handler.go
├── example.yml
├── go.mod
├── go.sum
└── main.go
Download .txt
SYMBOL INDEX (375 symbols across 49 files)

FILE: asset/bindata.go
  function bindataRead (line 20) | func bindataRead(data []byte, name string) ([]byte, error) {
  type asset (line 40) | type asset struct
  type bindataFileInfo (line 45) | type bindataFileInfo struct
    method Name (line 52) | func (fi bindataFileInfo) Name() string {
    method Size (line 55) | func (fi bindataFileInfo) Size() int64 {
    method Mode (line 58) | func (fi bindataFileInfo) Mode() os.FileMode {
    method ModTime (line 61) | func (fi bindataFileInfo) ModTime() time.Time {
    method IsDir (line 64) | func (fi bindataFileInfo) IsDir() bool {
    method Sys (line 67) | func (fi bindataFileInfo) Sys() interface{} {
  function data3dFlfBytes (line 73) | func data3dFlfBytes() ([]byte, error) {
  function data3dFlf (line 80) | func data3dFlf() (*asset, error) {
  function data2dFlfBytes (line 93) | func data2dFlfBytes() ([]byte, error) {
  function data2dFlf (line 100) | func data2dFlf() (*asset, error) {
  function dataQuindarToneMp3Bytes (line 113) | func dataQuindarToneMp3Bytes() ([]byte, error) {
  function dataQuindarToneMp3 (line 120) | func dataQuindarToneMp3() (*asset, error) {
  function Asset (line 134) | func Asset(name string) ([]byte, error) {
  function MustAsset (line 148) | func MustAsset(name string) []byte {
  function AssetInfo (line 160) | func AssetInfo(name string) (os.FileInfo, error) {
  function AssetNames (line 173) | func AssetNames() []string {
  function AssetDir (line 201) | func AssetDir(name string) ([]string, error) {
  type bintree (line 223) | type bintree struct
  function RestoreAsset (line 237) | func RestoreAsset(dir, name string) error {
  function RestoreAssets (line 262) | func RestoreAssets(dir, name string) error {
  function _filePath (line 278) | func _filePath(dir, name string) string {

FILE: asset/file.go
  type AssetFile (line 8) | type AssetFile struct
    method Read (line 16) | func (a AssetFile) Read(p []byte) (n int, err error) {
    method Close (line 20) | func (a AssetFile) Close() error {
  function NewAssetFile (line 12) | func NewAssetFile(data []byte) AssetFile {

FILE: asset/player.go
  type AudioPlayer (line 10) | type AudioPlayer struct
    method Beep (line 37) | func (a *AudioPlayer) Beep() {
    method Close (line 49) | func (a *AudioPlayer) Close() {
  function NewAudioPlayer (line 15) | func NewAudioPlayer() *AudioPlayer {

FILE: component/alert.go
  function RenderAlert (line 13) | func RenderAlert(alert *data.Alert, area image.Rectangle, buffer *ui.Buf...

FILE: component/asciibox/asciibox.go
  type AsciiBox (line 16) | type AsciiBox struct
    method renderText (line 70) | func (a *AsciiBox) renderText(sample *data.Sample) {
    method Draw (line 80) | func (a *AsciiBox) Draw(buffer *ui.Buffer) {
  constant asciiFontExtension (line 27) | asciiFontExtension = ".flf"
  function NewAsciiBox (line 29) | func NewAsciiBox(c config.AsciiBoxConfig, palette console.Palette) *Asci...

FILE: component/barchart/barchart.go
  constant barIndent (line 17) | barIndent int = 1
  type BarChart (line 21) | type BarChart struct
    method consumeSample (line 67) | func (b *BarChart) consumeSample(sample *data.Sample) {
    method addBar (line 102) | func (b *BarChart) addBar(label string, color ui.Color) {
    method reselectMaxValue (line 106) | func (b *BarChart) reselectMaxValue() {
    method Draw (line 117) | func (b *BarChart) Draw(buffer *ui.Buffer) {
  type bar (line 31) | type bar struct
  function NewBarChart (line 38) | func NewBarChart(c config.BarChartConfig, palette console.Palette) *BarC...

FILE: component/block.go
  function NewBlock (line 9) | func NewBlock(title string, border bool, palette console.Palette) *ui.Bl...

FILE: component/component.go
  type Component (line 9) | type Component struct
    method Move (line 31) | func (c *Component) Move(x, y int) {
    method Resize (line 37) | func (c *Component) Resize(x, y int) {
    method normalize (line 43) | func (c *Component) normalize() {
  function NewComponent (line 19) | func NewComponent(dbl ui.Drawable, cmr *data.Consumer, cfg config.Compon...

FILE: component/gauge/gauge.go
  constant MinValueLabel (line 15) | MinValueLabel = "min"
  constant MaxValueLabel (line 16) | MaxValueLabel = "max"
  constant CurValueLabel (line 17) | CurValueLabel = "cur"
  type Gauge (line 21) | type Gauge struct
    method ConsumeSample (line 58) | func (g *Gauge) ConsumeSample(sample *data.Sample) {
    method Draw (line 81) | func (g *Gauge) Draw(buffer *ui.Buffer) {
  function NewGauge (line 33) | func NewGauge(c config.GaugeConfig, palette console.Palette) *Gauge {
  function calculatePercent (line 122) | func calculatePercent(g *Gauge) float64 {

FILE: component/gauge/gauge_test.go
  function Test_calculatePercent (line 5) | func Test_calculatePercent(t *testing.T) {

FILE: component/layout/layout.go
  type Layout (line 17) | type Layout struct
    method AddComponent (line 66) | func (l *Layout) AddComponent(cpt *component.Component) {
    method StartWithIntro (line 70) | func (l *Layout) StartWithIntro() {
    method changeMode (line 74) | func (l *Layout) changeMode(m Mode) {
    method HandleMouseClick (line 82) | func (l *Layout) HandleMouseClick(x int, y int) {
    method HandleKeyboardEvent (line 97) | func (l *Layout) HandleKeyboardEvent(e string) {
    method ChangeDimensions (line 223) | func (l *Layout) ChangeDimensions(width, height int) {
    method getComponent (line 227) | func (l *Layout) getComponent(i int) *component.Component {
    method getSelection (line 231) | func (l *Layout) getSelection() *component.Component {
    method moveSelection (line 235) | func (l *Layout) moveSelection(direction string) {
    method Draw (line 298) | func (l *Layout) Draw(buffer *ui.Buffer) {
    method findComponentAtPoint (line 320) | func (l *Layout) findComponentAtPoint(point image.Point) (*component.C...
    method resetAlerts (line 358) | func (l *Layout) resetAlerts() {
    method WerePositionsChanged (line 364) | func (l *Layout) WerePositionsChanged() bool {
  type Mode (line 29) | type Mode
  constant ModeDefault (line 32) | ModeDefault          Mode = 0
  constant ModeIntro (line 33) | ModeIntro            Mode = 1
  constant ModePause (line 34) | ModePause            Mode = 2
  constant ModeComponentSelect (line 35) | ModeComponentSelect  Mode = 3
  constant ModeMenuOptionSelect (line 36) | ModeMenuOptionSelect Mode = 4
  constant ModeComponentMove (line 37) | ModeComponentMove    Mode = 5
  constant ModeComponentResize (line 38) | ModeComponentResize  Mode = 6
  constant ModeChartPinpoint (line 39) | ModeChartPinpoint    Mode = 7
  constant minDimension (line 43) | minDimension    = 3
  constant statusbarHeight (line 44) | statusbarHeight = 1
  function NewLayout (line 47) | func NewLayout(statusline *component.StatusBar, menu *component.Menu) *L...
  function calculateComponentCoordinates (line 337) | func calculateComponentCoordinates(c *component.Component, columnWidth f...

FILE: component/menu.go
  type Menu (line 11) | type Menu struct
    method GetSelectedOption (line 52) | func (m *Menu) GetSelectedOption() menuOption {
    method Highlight (line 56) | func (m *Menu) Highlight(component *Component) {
    method Choose (line 63) | func (m *Menu) Choose() {
    method Idle (line 67) | func (m *Menu) Idle() {
    method Up (line 71) | func (m *Menu) Up() {
    method Down (line 83) | func (m *Menu) Down() {
    method MoveOrResize (line 95) | func (m *Menu) MoveOrResize() {
    method Draw (line 99) | func (m *Menu) Draw(buffer *ui.Buffer) {
    method renderHighlight (line 124) | func (m *Menu) renderHighlight(buffer *ui.Buffer) {
    method renderMoveAndResize (line 159) | func (m *Menu) renderMoveAndResize(buffer *ui.Buffer) {
    method printAllDirectionsArrowSign (line 172) | func (m *Menu) printAllDirectionsArrowSign(buffer *ui.Buffer, y int) {
    method renderOptions (line 190) | func (m *Menu) renderOptions(buffer *ui.Buffer) {
    method updateDimensions (line 211) | func (m *Menu) updateDimensions() {
    method drawInnerBorder (line 216) | func (m *Menu) drawInnerBorder(buffer *ui.Buffer) {
  type menuMode (line 20) | type menuMode
  constant menuModeIdle (line 23) | menuModeIdle          menuMode = 0
  constant menuModeHighlight (line 24) | menuModeHighlight     menuMode = 1
  constant menuModeOptionSelect (line 25) | menuModeOptionSelect  menuMode = 2
  constant menuModeMoveAndResize (line 26) | menuModeMoveAndResize menuMode = 3
  type menuOption (line 29) | type menuOption
  constant MenuOptionMove (line 32) | MenuOptionMove     menuOption = "MOVE"
  constant MenuOptionResize (line 33) | MenuOptionResize   menuOption = "RESIZE"
  constant MenuOptionPinpoint (line 34) | MenuOptionPinpoint menuOption = "PINPOINT"
  constant MenuOptionResume (line 35) | MenuOptionResume   menuOption = "RESUME"
  constant minimalMenuHeight (line 39) | minimalMenuHeight = 8
  function NewMenu (line 42) | func NewMenu(palette console.Palette) *Menu {

FILE: component/runchart/grid.go
  constant defaultValueLength (line 11) | defaultValueLength = 4
  type chartGrid (line 13) | type chartGrid struct
  method newChartGrid (line 22) | func (c *RunChart) newChartGrid() chartGrid {
  method renderAxes (line 37) | func (c *RunChart) renderAxes(buffer *ui.Buffer) {
  method getTimeRange (line 101) | func (c *RunChart) getTimeRange(linesCount int) TimeRange {
  function getLocalExtrema (line 116) | func getLocalExtrema(items []TimeLine, timeRange TimeRange) ValueExtrema {
  method isInRange (line 145) | func (r *TimeRange) isInRange(time time.Time) bool {

FILE: component/runchart/legend.go
  constant xAxisLegendIndent (line 12) | xAxisLegendIndent = 10
  constant yAxisLegendIndent (line 13) | yAxisLegendIndent = 1
  constant heightOnDefault (line 14) | heightOnDefault   = 2
  constant heightOnPinpoint (line 15) | heightOnPinpoint  = 4
  constant heightOnDetails (line 16) | heightOnDetails   = 6
  constant timeFormat (line 18) | timeFormat = "15:04:05.000"
  type legend (line 21) | type legend struct
  method renderLegend (line 26) | func (c *RunChart) renderLegend(buffer *ui.Buffer, rectangle image.Recta...
  function getColumnWidth (line 87) | func getColumnWidth(mode Mode, lines []TimeLine, scale int) int {
  function getDiffWithPreviousValue (line 102) | func getDiffWithPreviousValue(line TimeLine) float64 {
  function getCurrentValue (line 109) | func getCurrentValue(line TimeLine) float64 {

FILE: component/runchart/runchart.go
  constant xAxisGridWidth (line 18) | xAxisGridWidth     = xAxisLabelsIndent + xAxisLabelsWidth
  constant xAxisLabelsHeight (line 19) | xAxisLabelsHeight  = 1
  constant xAxisLabelsWidth (line 20) | xAxisLabelsWidth   = 8
  constant xAxisLabelsIndent (line 21) | xAxisLabelsIndent  = 2
  constant yAxisLabelsHeight (line 22) | yAxisLabelsHeight  = 1
  constant yAxisLabelsIndent (line 23) | yAxisLabelsIndent  = 1
  constant historyReserveMin (line 24) | historyReserveMin  = 2
  constant xBrailleMultiplier (line 25) | xBrailleMultiplier = 2
  constant yBrailleMultiplier (line 26) | yBrailleMultiplier = 4
  type Mode (line 29) | type Mode
  constant ModeDefault (line 32) | ModeDefault  Mode = 0
  constant ModePinpoint (line 33) | ModePinpoint Mode = 1
  constant CommandDisableSelection (line 37) | CommandDisableSelection = "DISABLE_SELECTION"
  constant CommandMoveSelection (line 38) | CommandMoveSelection    = "MOVE_SELECTION"
  type RunChart (line 42) | type RunChart struct
    method newTimePoint (line 120) | func (c *RunChart) newTimePoint(value float64) TimePoint {
    method Draw (line 129) | func (c *RunChart) Draw(buffer *ui.Buffer) {
    method AddLine (line 147) | func (c *RunChart) AddLine(Label string, color ui.Color) {
    method consumeSample (line 157) | func (c *RunChart) consumeSample(sample *data.Sample) {
    method renderLines (line 195) | func (c *RunChart) renderLines(buffer *ui.Buffer, drawArea image.Recta...
    method trimOutOfRangeValues (line 290) | func (c *RunChart) trimOutOfRangeValues() {
    method calculateTimeCoordinate (line 310) | func (c *RunChart) calculateTimeCoordinate(t time.Time) int {
    method moveSelection (line 316) | func (c *RunChart) moveSelection(shift int) {
    method disableSelection (line 336) | func (c *RunChart) disableSelection() {
  type TimePoint (line 56) | type TimePoint struct
  type TimeLine (line 62) | type TimeLine struct
  type TimeRange (line 71) | type TimeRange struct
  type ValueExtrema (line 76) | type ValueExtrema struct
  function NewRunChart (line 81) | func NewRunChart(c config.RunChartConfig, palette console.Palette) *RunC...
  function getMidRangeTime (line 343) | func getMidRangeTime(r TimeRange) time.Time {
  function calculateTimescale (line 349) | func calculateTimescale(rateMs int) time.Duration {
  function braillePoint (line 361) | func braillePoint(point image.Point) image.Point {

FILE: component/sparkline/sparkline.go
  type SparkLine (line 15) | type SparkLine struct
    method consumeSample (line 53) | func (s *SparkLine) consumeSample(sample *data.Sample) {
    method trimOutOfRangeValues (line 88) | func (s *SparkLine) trimOutOfRangeValues(maxSize int) {
    method Draw (line 94) | func (s *SparkLine) Draw(buffer *ui.Buffer) {
  function NewSparkLine (line 27) | func NewSparkLine(c config.SparkLineConfig, palette console.Palette) *Sp...

FILE: component/sparkline/sparkline_test.go
  function TestSparkLine_trimOutOfRangeValues (line 7) | func TestSparkLine_trimOutOfRangeValues(t *testing.T) {

FILE: component/statusbar.go
  constant pauseText (line 11) | pauseText      = "  P A U S E D  "
  constant bindingsIndent (line 12) | bindingsIndent = 3
  type StatusBar (line 15) | type StatusBar struct
    method Draw (line 40) | func (s *StatusBar) Draw(buffer *ui.Buffer) {
    method TogglePause (line 59) | func (s *StatusBar) TogglePause() {
  function NewStatusBar (line 22) | func NewStatusBar(configFileName string, palette console.Palette) *Statu...

FILE: component/textbox/textbox.go
  type TextBox (line 13) | type TextBox struct
    method Draw (line 49) | func (t *TextBox) Draw(buffer *ui.Buffer) {
  function NewTextBox (line 22) | func NewTextBox(c config.TextBoxConfig, palette console.Palette) *TextBox {

FILE: component/util/format.go
  function FormatValue (line 22) | func FormatValue(value float64, scale int) string {
  function FormatDelta (line 29) | func FormatDelta(value float64, scale int) string {
  function addRadixChars (line 61) | func addRadixChars(value float64) string {
  function formatTrailingDigits (line 90) | func formatTrailingDigits(value string, scale int) string {
  function GetMiddlePoint (line 113) | func GetMiddlePoint(rectangle image.Rectangle, text string, offset int) ...
  function PrintString (line 117) | func PrintString(s string, style ui.Style, p image.Point, buffer *ui.Buf...

FILE: component/util/format_test.go
  function TestFormatValue (line 5) | func TestFormatValue(t *testing.T) {

FILE: component/util/geometry.go
  function GetRectLeftSideCenter (line 7) | func GetRectLeftSideCenter(rect image.Rectangle) image.Point {
  function GetRectRightSideCenter (line 14) | func GetRectRightSideCenter(rect image.Rectangle) image.Point {
  function GetRectTopSideCenter (line 21) | func GetRectTopSideCenter(rect image.Rectangle) image.Point {
  function GetRectBottomSideCenter (line 28) | func GetRectBottomSideCenter(rect image.Rectangle) image.Point {
  function GetRectCoordinates (line 35) | func GetRectCoordinates(area image.Rectangle, width int, height int) (in...

FILE: component/util/geometry_test.go
  function TestGetRectLeftSideCenter (line 6) | func TestGetRectLeftSideCenter(t *testing.T) {
  function TestGetRectRightSideCenter (line 18) | func TestGetRectRightSideCenter(t *testing.T) {
  function TestGetRectTopSideCenter (line 30) | func TestGetRectTopSideCenter(t *testing.T) {
  function TestGetRectBottomSideCenter (line 42) | func TestGetRectBottomSideCenter(t *testing.T) {

FILE: component/util/math.go
  function Max (line 3) | func Max(numbers []int) int {

FILE: component/util/parse.go
  function ParseFloat (line 8) | func ParseFloat(input string) (float64, error) {

FILE: component/util/parse_test.go
  function TestParseFloat (line 5) | func TestParseFloat(t *testing.T) {

FILE: config/arrangement.go
  method setDefaultArrangement (line 9) | func (c *Config) setDefaultArrangement() {
  function arrangeIntoLargestComponent (line 32) | func arrangeIntoLargestComponent(component *ComponentConfig, lc *Compone...
  function arrangeIntoLargestEmptySpace (line 58) | func arrangeIntoLargestEmptySpace(component *ComponentConfig, le image.R...
  function allHaveNoPosition (line 65) | func allHaveNoPosition(components []*ComponentConfig) bool {
  function getLargestComponent (line 74) | func getLargestComponent(components []*ComponentConfig) *ComponentConfig {
  function getSquare (line 87) | func getSquare(c *ComponentConfig) int {
  function getComponents (line 92) | func getComponents(c *Config) []*ComponentConfig {
  function setSingleComponentPosition (line 118) | func setSingleComponentPosition(c *ComponentConfig) {
  function getLargestEmptySpaceRectangle (line 127) | func getLargestEmptySpaceRectangle(components []*ComponentConfig) image....
  function calcMaxRectangle (line 153) | func calcMaxRectangle(histogram [console.ColumnsCount]int, row int) imag...
  function createHistogram (line 179) | func createHistogram(grid [console.RowsCount][console.ColumnsCount]int, ...
  function countEmptyCellsBelow (line 187) | func countEmptyCellsBelow(grid [console.RowsCount][console.ColumnsCount]...

FILE: config/component.go
  type ComponentType (line 9) | type ComponentType
  constant TypeRunChart (line 12) | TypeRunChart  ComponentType = 0
  constant TypeBarChart (line 13) | TypeBarChart  ComponentType = 1
  constant TypeSparkLine (line 14) | TypeSparkLine ComponentType = 2
  constant TypeTextBox (line 15) | TypeTextBox   ComponentType = 3
  constant TypeAsciiBox (line 16) | TypeAsciiBox  ComponentType = 4
  constant TypeGauge (line 17) | TypeGauge     ComponentType = 5
  type ComponentConfig (line 20) | type ComponentConfig struct
    method GetLocation (line 28) | func (c *ComponentConfig) GetLocation() Location {
    method GetSize (line 32) | func (c *ComponentConfig) GetSize() Size {
    method GetRectangle (line 36) | func (c *ComponentConfig) GetRectangle() image.Rectangle {
  type TriggerConfig (line 47) | type TriggerConfig struct
  type ActionsConfig (line 53) | type ActionsConfig struct
  type GaugeConfig (line 60) | type GaugeConfig struct
  type SparkLineConfig (line 70) | type SparkLineConfig struct
  type BarChartConfig (line 77) | type BarChartConfig struct
  type AsciiBoxConfig (line 83) | type AsciiBoxConfig struct
  type TextBoxConfig (line 90) | type TextBoxConfig struct
  type RunChartConfig (line 96) | type RunChartConfig struct
  type LegendConfig (line 103) | type LegendConfig struct
  type Item (line 108) | type Item struct
  type Location (line 118) | type Location struct
  type Size (line 123) | type Size struct
  type ComponentSettings (line 128) | type ComponentSettings struct
  function getPosition (line 135) | func getPosition(location Location, size Size) [][]int {

FILE: config/config.go
  type Config (line 13) | type Config struct
    method findComponent (line 57) | func (c *Config) findComponent(componentType ComponentType, componentT...
  function LoadConfig (line 24) | func LoadConfig() (*Config, Options) {
  function Update (line 48) | func Update(settings []ComponentSettings, options Options) {
  function readFile (line 102) | func readFile(location *string) *Config {
  function saveFile (line 119) | func saveFile(config *Config, fileName *string) {

FILE: config/default.go
  constant defaultRateMs (line 8) | defaultRateMs = 1000
  constant defaultScale (line 9) | defaultScale  = 1
  constant defaultTheme (line 10) | defaultTheme  = console.ThemeDark
  method setDefaults (line 13) | func (c *Config) setDefaults() {
  method setDefaultValues (line 19) | func (c *Config) setDefaultValues() {
  function setDefaultTriggersValues (line 150) | func setDefaultTriggersValues(triggers []TriggerConfig) {
  method setDefaultItemSettings (line 176) | func (c *Config) setDefaultItemSettings() {

FILE: config/options.go
  type Options (line 4) | type Options struct

FILE: config/validator.go
  method validate (line 8) | func (c *Config) validate() {
  function validateItemsScripts (line 44) | func validateItemsScripts(title string, items []Item) {
  function validateItemScripts (line 50) | func validateItemScripts(title string, i Item) {
  function validateLabelsUniqueness (line 59) | func validateLabelsUniqueness(title string, items []Item) {
  function validateTitlesUniqueness (line 70) | func validateTitlesUniqueness(components []ComponentConfig) {

FILE: console/console.go
  constant MaxRenderInterval (line 13) | MaxRenderInterval = 1000 * time.Millisecond
  constant MinRenderInterval (line 14) | MinRenderInterval = 100 * time.Millisecond
  constant ColumnsCount (line 15) | ColumnsCount      = 80
  constant RowsCount (line 16) | RowsCount         = 40
  constant AppTitle (line 17) | AppTitle          = "sampler"
  constant AppVersion (line 18) | AppVersion        = "1.1.0"
  constant BellCharacter (line 22) | BellCharacter = "\a"
  type AsciiFont (line 25) | type AsciiFont
  constant AsciiFont2D (line 28) | AsciiFont2D AsciiFont = "2d"
  constant AsciiFont3D (line 29) | AsciiFont3D AsciiFont = "3d"
  function Init (line 32) | func Init() {
  function Close (line 43) | func Close() {
  function Exit (line 48) | func Exit(message string) {

FILE: console/palette.go
  type Theme (line 10) | type Theme
  constant ThemeDark (line 13) | ThemeDark  Theme = "dark"
  constant ThemeLight (line 14) | ThemeLight Theme = "light"
  constant ColorOlive (line 18) | ColorOlive       ui.Color = 178
  constant ColorDeepSkyBlue (line 19) | ColorDeepSkyBlue ui.Color = 39
  constant ColorDeepPink (line 20) | ColorDeepPink    ui.Color = 198
  constant ColorCian (line 21) | ColorCian        ui.Color = 43
  constant ColorOrange (line 22) | ColorOrange      ui.Color = 166
  constant ColorPurple (line 23) | ColorPurple      ui.Color = 129
  constant ColorGreen (line 24) | ColorGreen       ui.Color = 64
  constant ColorDarkRed (line 25) | ColorDarkRed     ui.Color = 88
  constant ColorBlueViolet (line 26) | ColorBlueViolet  ui.Color = 57
  constant ColorDarkGrey (line 27) | ColorDarkGrey    ui.Color = 238
  constant ColorLightGrey (line 28) | ColorLightGrey   ui.Color = 254
  constant ColorGrey (line 29) | ColorGrey        ui.Color = 242
  constant ColorWhite (line 30) | ColorWhite       ui.Color = 15
  constant ColorBlack (line 31) | ColorBlack       ui.Color = 0
  constant ColorClear (line 32) | ColorClear       ui.Color = -1
  constant menuColorNix (line 36) | menuColorNix            ui.Color = 255
  constant menuColorReverseNix (line 37) | menuColorReverseNix     ui.Color = 235
  constant menuColorWindows (line 38) | menuColorWindows        ui.Color = 255
  constant menuColorReverseWindows (line 39) | menuColorReverseWindows ui.Color = 0
  type Palette (line 42) | type Palette struct
  function GetPalette (line 51) | func GetPalette(theme Theme) Palette {
  function GetMenuColor (line 76) | func GetMenuColor() ui.Color {
  function GetMenuColorReverse (line 87) | func GetMenuColorReverse() ui.Color {
  function GetGradientColor (line 96) | func GetGradientColor(gradient []ui.Color, cur int, max int) ui.Color {

FILE: console/palette_test.go
  function TestGetPalette (line 9) | func TestGetPalette(t *testing.T) {
  function TestGetPaletteInvalidTheme (line 38) | func TestGetPaletteInvalidTheme(t *testing.T) {
  function TestGetGradientColor (line 50) | func TestGetGradientColor(t *testing.T) {

FILE: console/signal.go
  constant SignalResize (line 4) | SignalResize = "<Resize>"
  constant SignalClick (line 5) | SignalClick  = "<MouseLeft>"
  constant KeyPause1 (line 9) | KeyPause1 = "p"
  constant KeyPause2 (line 10) | KeyPause2 = "P"
  constant KeyQuit1 (line 11) | KeyQuit1  = "q"
  constant KeyQuit2 (line 12) | KeyQuit2  = "Q"
  constant KeyQuit3 (line 13) | KeyQuit3  = "<C-c>"
  constant KeyLeft (line 14) | KeyLeft   = "<Left>"
  constant KeyRight (line 15) | KeyRight  = "<Right>"
  constant KeyUp (line 16) | KeyUp     = "<Up>"
  constant KeyDown (line 17) | KeyDown   = "<Down>"
  constant KeyEnter (line 18) | KeyEnter  = "<Enter>"
  constant KeyEsc (line 19) | KeyEsc    = "<Escape>"

FILE: console/symbol.go
  constant SymbolSelection (line 4) | SymbolSelection     rune = '▲'
  constant SymbolVerticalBar (line 5) | SymbolVerticalBar   rune = '▎'
  constant SymbolHorizontalBar (line 6) | SymbolHorizontalBar rune = '═'

FILE: data/consumer.go
  type Consumer (line 8) | type Consumer struct
    method HandleConsumeSuccess (line 15) | func (c *Consumer) HandleConsumeSuccess() {
    method HandleConsumeFailure (line 21) | func (c *Consumer) HandleConsumeFailure(title string, err error, sampl...
  type Sample (line 30) | type Sample struct
  type Alert (line 36) | type Alert struct
  type Command (line 43) | type Command struct
  function NewConsumer (line 48) | func NewConsumer() *Consumer {

FILE: data/consumer_test.go
  function TestConsumer_HandleConsumeSuccess (line 9) | func TestConsumer_HandleConsumeSuccess(t *testing.T) {
  function TestConsumer_HandleConsumeFailure (line 31) | func TestConsumer_HandleConsumeFailure(t *testing.T) {
  function TestNewConsumer (line 68) | func TestNewConsumer(t *testing.T) {

FILE: data/error.go
  function getErrorMessage (line 8) | func getErrorMessage(err error) string {

FILE: data/int.go
  constant startupTimeout (line 6) | startupTimeout  = 200 * time.Millisecond
  constant minAwaitTimeout (line 7) | minAwaitTimeout = 100 * time.Millisecond
  constant maxAwaitTimeout (line 8) | maxAwaitTimeout = 1 * time.Second
  type InteractiveShell (line 11) | type InteractiveShell interface

FILE: data/int_basic.go
  type BasicInteractiveShell (line 15) | type BasicInteractiveShell struct
    method init (line 26) | func (s *BasicInteractiveShell) init() error {
    method execute (line 80) | func (s *BasicInteractiveShell) execute() (string, error) {

FILE: data/int_pty.go
  type PtyInteractiveShell (line 17) | type PtyInteractiveShell struct
    method init (line 27) | func (s *PtyInteractiveShell) init() error {
    method execute (line 68) | func (s *PtyInteractiveShell) execute() (string, error) {
    method getAwaitTimeout (line 122) | func (s *PtyInteractiveShell) getAwaitTimeout() time.Duration {

FILE: data/int_pty_windows.go
  type PtyInteractiveShell (line 8) | type PtyInteractiveShell struct
    method init (line 14) | func (s *PtyInteractiveShell) init() error {
    method execute (line 18) | func (s *PtyInteractiveShell) execute() (string, error) {

FILE: data/item.go
  constant errorThreshold (line 12) | errorThreshold = 10
  type Item (line 14) | type Item struct
    method nextValue (line 45) | func (i *Item) nextValue(variables []string) (string, error) {
    method execute (line 63) | func (i *Item) execute(variables []string, script string) (string, err...
    method initInteractiveShell (line 79) | func (i *Item) initInteractiveShell(v []string) error {
    method transform (line 93) | func (i *Item) transform(sample string) (string, error) {
  function NewItems (line 26) | func NewItems(cfgs []config.Item, rateMs int) []*Item {
  function enrichEnvVariables (line 102) | func enrichEnvVariables(cmd *exec.Cmd, variables []string) {
  function getInitScripts (line 109) | func getInitScripts(item config.Item) []string {

FILE: data/sampler.go
  type Sampler (line 9) | type Sampler struct
    method sample (line 57) | func (s *Sampler) sample(item *Item, options config.Options) {
    method Pause (line 87) | func (s *Sampler) Pause(pause bool) {
  function NewSampler (line 18) | func NewSampler(consumer *Consumer, items []*Item, triggers []*Trigger, ...
  function mergeVariables (line 76) | func mergeVariables(fileVariables map[string]string, optionsVariables []...

FILE: data/trigger.go
  constant TrueIndicator (line 14) | TrueIndicator = "1"
  constant InitialValue (line 15) | InitialValue  = "0"
  type Trigger (line 18) | type Trigger struct
    method Execute (line 70) | func (t *Trigger) Execute(sample *Sample) {
    method evaluate (line 96) | func (t *Trigger) evaluate(sample *Sample) bool {
    method runScript (line 120) | func (t *Trigger) runScript(script, label string, data Values) ([]byte...
  type Actions (line 29) | type Actions struct
  type Values (line 36) | type Values struct
  function NewTriggers (line 41) | func NewTriggers(cfgs []config.TriggerConfig, consumer *Consumer, option...
  function NewTrigger (line 52) | func NewTrigger(config config.TriggerConfig, consumer *Consumer, options...

FILE: event/handler.go
  constant refreshRateToRenderRateRatio (line 13) | refreshRateToRenderRateRatio = 0.5
  type Handler (line 16) | type Handler struct
    method HandleEvents (line 37) | func (h *Handler) HandleEvents() {
    method handleModeChange (line 68) | func (h *Handler) handleModeChange(m layout.Mode) {
    method pause (line 87) | func (h *Handler) pause(pause bool) {
    method updateConfigFile (line 93) | func (h *Handler) updateConfigFile() {
  function NewHandler (line 25) | func NewHandler(samplers []*data.Sampler, options config.Options, layout...
  function calcMinRenderRate (line 102) | func calcMinRenderRate(layout *layout.Layout) time.Duration {

FILE: main.go
  type Starter (line 21) | type Starter struct
    method startAll (line 29) | func (s *Starter) startAll() []*data.Sampler {
    method start (line 58) | func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer,...
  function main (line 67) | func main() {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (244K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 39,
    "preview": "custom: ['http://sampler.dev/sponsor']\n"
  },
  {
    "path": ".gitignore",
    "chars": 55,
    "preview": "*.dll\n*.so\n*.dylib\n*.test\n*.out\n*.db\n.idea/\n*.iml\n*.iws"
  },
  {
    "path": ".travis.yml",
    "chars": 232,
    "preview": "language: go\ngo:\n  - 1.12.x\n  - master\nos:\n  - linux\n  - osx\naddons:\n  apt:\n    packages:\n      - libasound2-dev\ndist: t"
  },
  {
    "path": "Dockerfile",
    "chars": 302,
    "preview": "FROM ubuntu:focal\n\nENV Sampler=/usr/local/bin/sampler\n\nRUN apt-get update && apt-get upgrade --yes && apt-get install li"
  },
  {
    "path": "LICENSE.md",
    "chars": 35148,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 15865,
    "preview": "# Sampler. Visualization for any shell command.\n[![Build Status](https://travis-ci.com/sqshq/sampler.svg?token=LdyRhxxjD"
  },
  {
    "path": "asset/bindata.go",
    "chars": 52189,
    "preview": "// Code generated by go-bindata. (@generated) DO NOT EDIT.\n// sources:\n// 3d.flf\n// 2d.flf\n// quindar-tone.mp3\npackage a"
  },
  {
    "path": "asset/file.go",
    "chars": 306,
    "preview": "package asset\n\nimport (\n\t\"bytes\"\n\t\"io\"\n)\n\ntype AssetFile struct {\n\treader io.Reader\n}\n\nfunc NewAssetFile(data []byte) As"
  },
  {
    "path": "asset/player.go",
    "chars": 907,
    "preview": "package asset\n\nimport (\n\t\"github.com/hajimehoshi/go-mp3\"\n\t\"github.com/hajimehoshi/oto\"\n\t\"io\"\n\t\"log\"\n)\n\ntype AudioPlayer "
  },
  {
    "path": "component/alert.go",
    "chars": 1249,
    "preview": "package component\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component/util\"\n\t\"github."
  },
  {
    "path": "component/asciibox/asciibox.go",
    "chars": 2183,
    "preview": "package asciibox\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\tfl \"github.com/mbndr/figlet4go\"\n\t\"github.com/sqshq/sampler/"
  },
  {
    "path": "component/barchart/barchart.go",
    "chars": 3533,
    "preview": "package barchart\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\trw \"github.com/mattn/go-runewidth\"\n\t\"github.com/sqsh"
  },
  {
    "path": "component/block.go",
    "chars": 429,
    "preview": "package component\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/console\"\n)\n\nfunc NewBlock"
  },
  {
    "path": "component/component.go",
    "chars": 888,
    "preview": "package component\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/config\"\n\t\"github.com/sqshq/sampl"
  },
  {
    "path": "component/gauge/gauge.go",
    "chars": 2877,
    "preview": "package gauge\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component\"\n\t\"github.com/sqshq"
  },
  {
    "path": "component/gauge/gauge_test.go",
    "chars": 955,
    "preview": "package gauge\n\nimport \"testing\"\n\nfunc Test_calculatePercent(t *testing.T) {\n\ttype args struct {\n\t\tg *Gauge\n\t}\n\ttests := "
  },
  {
    "path": "component/layout/layout.go",
    "chars": 10126,
    "preview": "package layout\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component\"\n\t\"github.com/sqshq/sampl"
  },
  {
    "path": "component/menu.go",
    "chars": 5484,
    "preview": "package component\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component/util\"\n\t\"github.com/sqs"
  },
  {
    "path": "component/runchart/grid.go",
    "chars": 4075,
    "preview": "package runchart\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component/util\"\n\t\"image\"\n\t\"math\"\n"
  },
  {
    "path": "component/runchart/legend.go",
    "chars": 2780,
    "preview": "package runchart\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component/util\"\n\t\"image\"\n\t"
  },
  {
    "path": "component/runchart/runchart.go",
    "chars": 8303,
    "preview": "package runchart\n\nimport (\n\t\"github.com/sqshq/sampler/component\"\n\t\"github.com/sqshq/sampler/component/util\"\n\t\"github.com"
  },
  {
    "path": "component/sparkline/sparkline.go",
    "chars": 3197,
    "preview": "package sparkline\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component\"\n\t\"github.com/sqshq/sa"
  },
  {
    "path": "component/sparkline/sparkline_test.go",
    "chars": 913,
    "preview": "package sparkline\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSparkLine_trimOutOfRangeValues(t *testing.T) {\n\ttype Sparkline struct"
  },
  {
    "path": "component/statusbar.go",
    "chars": 1454,
    "preview": "package component\n\nimport (\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/console\"\n\t\"image\"\n)\n\ncons"
  },
  {
    "path": "component/textbox/textbox.go",
    "chars": 1433,
    "preview": "package textbox\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component\"\n\t\"github.com/sqshq/samp"
  },
  {
    "path": "component/util/format.go",
    "chars": 2521,
    "preview": "package util\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\tui \"github.com/gizak/termui/v3\"\n\t\"image\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar As"
  },
  {
    "path": "component/util/format_test.go",
    "chars": 727,
    "preview": "package util\n\nimport \"testing\"\n\nfunc TestFormatValue(t *testing.T) {\n\ttype args struct {\n\t\tvalue float64\n\t\tscale int\n\t}\n"
  },
  {
    "path": "component/util/geometry.go",
    "chars": 810,
    "preview": "package util\n\nimport (\n\t\"image\"\n)\n\nfunc GetRectLeftSideCenter(rect image.Rectangle) image.Point {\n\treturn image.Point{\n\t"
  },
  {
    "path": "component/util/geometry_test.go",
    "chars": 1211,
    "preview": "package util\n\nimport \"testing\"\nimport \"image\"\n\nfunc TestGetRectLeftSideCenter(t *testing.T) {\n\trect := image.Rect(10, 10"
  },
  {
    "path": "component/util/math.go",
    "chars": 142,
    "preview": "package util\n\nfunc Max(numbers []int) int {\n\n\tmax := numbers[0]\n\n\tfor _, n := range numbers {\n\t\tif n > max {\n\t\t\tmax = n\n"
  },
  {
    "path": "component/util/parse.go",
    "chars": 380,
    "preview": "package util\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc ParseFloat(input string) (float64, error) {\n\n\tclean := strings.Trim"
  },
  {
    "path": "component/util/parse_test.go",
    "chars": 943,
    "preview": "package util\n\nimport \"testing\"\n\nfunc TestParseFloat(t *testing.T) {\n\ttype args struct {\n\t\tinput string\n\t}\n\ttests := []st"
  },
  {
    "path": "config/arrangement.go",
    "chars": 4453,
    "preview": "package config\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/console\"\n\t\"image\"\n)\n\nfunc (c *Confi"
  },
  {
    "path": "config/component.go",
    "chars": 3625,
    "preview": "package config\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/console\"\n\t\"image\"\n)\n\ntype Component"
  },
  {
    "path": "config/config.go",
    "chars": 3113,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"github.com/jessevdk/go-flags\"\n\t\"github.com/sqshq/sampler/console\"\n\t\"gopkg.in/yaml.v3\"\n"
  },
  {
    "path": "config/default.go",
    "chars": 4664,
    "preview": "package config\n\nimport (\n\t\"github.com/sqshq/sampler/console\"\n)\n\nconst (\n\tdefaultRateMs = 1000\n\tdefaultScale  = 1\n\tdefaul"
  },
  {
    "path": "config/options.go",
    "chars": 437,
    "preview": "package config\n\n// Options with cli flags\ntype Options struct {\n\tConfigFile  *string  `short:\"c\" long:\"config\" descripti"
  },
  {
    "path": "config/validator.go",
    "chars": 2288,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"github.com/sqshq/sampler/console\"\n)\n\nfunc (c *Config) validate() {\n\n\tvar components []"
  },
  {
    "path": "console/console.go",
    "chars": 814,
    "preview": "package console\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\tui \"github.com/gizak/termui/v3\"\n)\n\nconst (\n\tMaxRenderInterval = "
  },
  {
    "path": "console/palette.go",
    "chars": 2573,
    "preview": "package console\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\tui \"github.com/gizak/termui/v3\"\n)\n\ntype Theme string\n\nconst (\n\tThemeDark  "
  },
  {
    "path": "console/palette_test.go",
    "chars": 1845,
    "preview": "package console\n\nimport (\n\t\"testing\"\n\n\tui \"github.com/gizak/termui/v3\"\n)\n\nfunc TestGetPalette(t *testing.T) {\n\tvar (\n\t\td"
  },
  {
    "path": "console/signal.go",
    "chars": 318,
    "preview": "package console\n\nconst (\n\tSignalResize = \"<Resize>\"\n\tSignalClick  = \"<MouseLeft>\"\n)\n\nconst (\n\tKeyPause1 = \"p\"\n\tKeyPause2"
  },
  {
    "path": "console/symbol.go",
    "chars": 123,
    "preview": "package console\n\nconst (\n\tSymbolSelection     rune = '▲'\n\tSymbolVerticalBar   rune = '▎'\n\tSymbolHorizontalBar rune = '═'"
  },
  {
    "path": "data/consumer.go",
    "chars": 961,
    "preview": "package data\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"strings\"\n)\n\ntype Consumer struct {\n\tSampleChannel  chan *Sampl"
  },
  {
    "path": "data/consumer_test.go",
    "chars": 2357,
    "preview": "package data\n\nimport (\n\t\"errors\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestConsumer_HandleConsumeSuccess(t *testing.T) {\n\ttests "
  },
  {
    "path": "data/error.go",
    "chars": 286,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n)\n\nfunc getErrorMessage(err error) string {\n\n\texitErr, ok := err.(*exec.ExitErr"
  },
  {
    "path": "data/int.go",
    "chars": 236,
    "preview": "package data\n\nimport \"time\"\n\nconst (\n\tstartupTimeout  = 200 * time.Millisecond\n\tminAwaitTimeout = 100 * time.Millisecond"
  },
  {
    "path": "data/int_basic.go",
    "chars": 2390,
    "preview": "package data\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/lunixbochs/vtclean\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"time\"\n)\n"
  },
  {
    "path": "data/int_pty.go",
    "chars": 2499,
    "preview": "//+build !windows\n\npackage data\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"github.com/kr/pty\"\n\t\"github.com/lunixbochs/vtclean\"\n\t\"io\"\n\t\""
  },
  {
    "path": "data/int_pty_windows.go",
    "chars": 373,
    "preview": "package data\n\nimport (\n\t\"errors\"\n\t\"time\"\n)\n\ntype PtyInteractiveShell struct {\n\titem      *Item\n\tvariables []string\n\ttime"
  },
  {
    "path": "data/item.go",
    "chars": 2494,
    "preview": "package data\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/lunixbochs/vtclean\"\n\t\"github.com/sqshq/sampler/conf"
  },
  {
    "path": "data/sampler.go",
    "chars": 1854,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"github.com/sqshq/sampler/config\"\n\t\"time\"\n)\n\ntype Sampler struct {\n\tconsumer        *Cons"
  },
  {
    "path": "data/trigger.go",
    "chars": 3134,
    "preview": "package data\n\nimport (\n\t\"fmt\"\n\t\"github.com/sqshq/sampler/asset\"\n\t\"github.com/sqshq/sampler/config\"\n\t\"github.com/sqshq/sa"
  },
  {
    "path": "event/handler.go",
    "chars": 2839,
    "preview": "package event\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/component/layout\"\n\t\"github.com/sqshq"
  },
  {
    "path": "example.yml",
    "chars": 3314,
    "preview": "runcharts:\n  - title: Search engine response time\n    position: [[0, 0], [45, 19]]\n    rate-ms: 500\n    triggers:\n      "
  },
  {
    "path": "go.mod",
    "chars": 948,
    "preview": "module github.com/sqshq/sampler\n\ngo 1.17\n\nrequire (\n\tgithub.com/gizak/termui/v3 v3.0.0\n\tgithub.com/hajimehoshi/go-mp3 v0"
  },
  {
    "path": "go.sum",
    "chars": 3661,
    "preview": "github.com/cjbassi/drawille-go v0.0.0-20190126131713-27dc511fe6fd h1:XtfPmj9tQRilnrEmI1HjQhxXWRhEM+m8CACtaMJE/kM=\ngithub"
  },
  {
    "path": "main.go",
    "chars": 3072,
    "preview": "package main\n\nimport (\n\tui \"github.com/gizak/termui/v3\"\n\t\"github.com/sqshq/sampler/asset\"\n\t\"github.com/sqshq/sampler/com"
  }
]

About this extraction

This page contains the full source code of the sqshq/sampler GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (211.3 KB), approximately 74.0k tokens, and a symbol index with 375 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!