mirror of
https://github.com/Reuh/daccord.git
synced 2025-10-27 12:49:30 +00:00
Initial commit
This commit is contained in:
commit
7a5bddaffc
6 changed files with 1617 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
config.lua
|
||||
dev
|
||||
202
LICENSE
Normal file
202
LICENSE
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
207
classtoi.lua
Normal file
207
classtoi.lua
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
--- Reuh's class library version 0.1.4. Lua 5.1-5.3 and LuaJit compatible.
|
||||
-- Objects and classes behavior are identical, so you can consider this to be somewhat prototype-based.
|
||||
-- Features:
|
||||
-- * Multiple inheritance with class(parents...) or someclass(newstuff...)
|
||||
-- * Every metamethods supported
|
||||
-- * Everything in a class can be redefined (and will be usable in an object) (except __super)
|
||||
-- * Preserve parents metamethods if already set
|
||||
-- * Instanciate with class:new(...)
|
||||
-- * Test inheritance relations with class/object.is(thing, isThis)
|
||||
-- * Call object:new(...) on instanciation
|
||||
-- * If object:new(...) returns non-nil values, they will be returned instead of the instance
|
||||
-- * Call class.__inherit(class, inheritingClass) when creating a class inheriting the previous class. If class.__inherit returns a value, it will
|
||||
-- be used as the parent table instead of class, allowing some pretty fancy behavior (it's like an inheritance metamethod).
|
||||
-- * Implements Class Commons
|
||||
-- * I don't like to do this, but you can redefine every field and metamethod after class creation (except __index and __super).
|
||||
-- Not features / Things you may want to know:
|
||||
-- * Will set the metatable of all parent classes/tables if no metatable is set (the table will be its own metatable).
|
||||
-- * You can't redefine __super (any __super you define will be only avaible by searching in the default __super contents).
|
||||
-- * Redefining __super or __index after class creation will break everything (though it should be ok with new, is, __call and everything else).
|
||||
-- * When creating a new class, the methods new, is, __call, __index and __super will always be redefined, so trying to get theses fields
|
||||
-- will return the default method and not the one you've defined. However, theses defaults will be replaced by yours automatically on instanciation,
|
||||
-- except __super and __index, but __index should call your __index and act like you expect. __super will however always be the default one
|
||||
-- and doesn't proxy in any way yours.
|
||||
-- * __index metamethods will be called with an extra third argument, which is the current class being searched in the inheritance tree.
|
||||
-- You can safely ignore it.
|
||||
--
|
||||
-- Please also note that the last universal ancestor of the classes (defined here in BaseClass) sets the default __tostring method
|
||||
-- and __name attribute for nice class-name-printing. Unlike the previoulsy described attributes and methods however, it is done in a normal
|
||||
-- inheritance-way and can be rewritten without any problem (rewritting __name is especially useful to easily identify your classes).
|
||||
|
||||
-- Copyright (c) 2016-2017 Étienne "Reuh" Fildadut <fildadut@reuh.eu>
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
-- of this software and associated documentation files (the "Software"), to deal
|
||||
-- in the Software without restriction, including without limitation the rights
|
||||
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
-- copies of the Software, and to permit persons to whom the Software is
|
||||
-- furnished to do so, subject to the following conditions:
|
||||
--
|
||||
-- 1. The above copyright notice and this permission notice shall be included in
|
||||
-- all copies or substantial portions of the Software; and
|
||||
-- 2. You must cause any modified source files to carry prominent notices stating
|
||||
-- that you changed the files.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
|
||||
-- Lua versions compatibility
|
||||
local unpack = table.unpack or unpack
|
||||
|
||||
--- All Lua 5.3 metamethods.
|
||||
local metamethods = {
|
||||
"__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv",
|
||||
"__band", "__bor", "__bxor", "__bnot", "__shl", "__shr", "__tostring",
|
||||
"__concat", "__len", "__eq", "__lt", "__le", "__index", "__newindex", "__call", "__gc"
|
||||
}
|
||||
|
||||
local different --- When set, every class __index method will only return a value different from this one.
|
||||
--- When using a proxied method, contains the last indexed class.
|
||||
-- This is used for class.is(object); lastIndex will contain class so the is method can react accordingly, without having to be
|
||||
-- re-set for each class (and therefore doesn't break the "different" mecanism).
|
||||
local lastIndexed
|
||||
local makeclass, methods, BaseClass
|
||||
|
||||
--- Classes defaults methods: will be re-set on each class creation.
|
||||
-- If you overwrite them, you will only be able to call them from an object.
|
||||
-- Methods starting with a "!" are "proxied methods": they're not present in the class table and will only be called through __index,
|
||||
-- allowing more control over it (for example having access to lastIndexed).
|
||||
methods = {
|
||||
--- Create an object from the class.
|
||||
-- In pratise, this only subclass the class and call the new method on it, so technically an object is a class.
|
||||
-- Objects are exaclty like classes, but the __call metamethod will be replaced by one found in the parents,
|
||||
-- or nil if doesn't exist (so an object is not directly subclassable).
|
||||
-- (If no __call method is defined in a parent, you won't be able to call the object, but obj.__call will still
|
||||
-- returns the default (subclassing) method, from one of the parents classes.)
|
||||
-- The same happens with :new and :is, but since they're not metamethods, if not defined in a parent you won't
|
||||
-- notice any difference.
|
||||
-- TL;DR (since I think I'm not really clear): you can redefine __call, :new and :is in parents and use them in objects only.
|
||||
-- A new object will only be created if calling the method "class:new(...)", if you call for example "class.new(someTable, ...)", it
|
||||
-- will only execute the constructor defined in the class on someTable. This can be used to execute the parent constructor in a child
|
||||
-- object, for example.
|
||||
-- It should also be noted that if the new method returns non-nil value(s), they will be returned instead of the object.
|
||||
["!new"] = function(self, ...)
|
||||
if lastIndexed == self then
|
||||
local obj, ret = self(), nil
|
||||
-- Setting class methods to the ones found in parents (we use rawset in order to avoid calling the __newindex metamethod)
|
||||
different = methods["!new"] rawset(obj, "new", obj:__index("new") or nil)
|
||||
different = methods["!is"] rawset(obj, "is", obj:__index("is") or nil)
|
||||
different = methods.__call rawset(obj, "__call", obj:__index("__call") or nil)
|
||||
different = nil
|
||||
-- Call constructor
|
||||
if obj.new ~= methods["!new"] and type(obj.new) == "function" then ret = { obj:new(...) } end
|
||||
if not ret or #ret == 0 then
|
||||
return obj
|
||||
else
|
||||
return unpack(ret)
|
||||
end
|
||||
else
|
||||
different = methods["!new"]
|
||||
local new = lastIndexed:__index("new") or nil
|
||||
different = nil
|
||||
return new(self, ...)
|
||||
end
|
||||
end,
|
||||
--- Returns true if self is other or a subclass of other.
|
||||
-- If other is nil, will return true if self is a subclass of the class who called this method.
|
||||
-- Examples:
|
||||
-- class.is(a) will return true if a is any class or object
|
||||
-- (class()):is(class) will return true ((class()) is a subclass of class)
|
||||
-- (class()).is(class) will return false (class isn't a subclass of (class()))
|
||||
["!is"] = function(self, other)
|
||||
if type(self) ~= "table" then return false end
|
||||
if other == nil then other = lastIndexed end
|
||||
if self == other then return true end
|
||||
for _, t in ipairs(self.__super) do
|
||||
if t == other then return true end
|
||||
if t.is == methods["!is"] and t:is(other) then return true end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
--- Subclass the class: will create a class inheriting self and ... (... will have priority over self).
|
||||
__call = function(self, ...)
|
||||
local t = {...}
|
||||
table.insert(t, self)
|
||||
return makeclass(unpack(t))
|
||||
end,
|
||||
--- Internal value getting; this follows a precise search order.
|
||||
-- For example: class(Base1, Base2){stuff}
|
||||
-- When getting a value from the class, it will be first searched in stuff, then in Base1, then in all Base1 parents,
|
||||
-- then in Base2, then in Base2 parents.
|
||||
-- A way to describe this will be search in the latest added tables (from the farthest child to the first parents), from left-to-right.
|
||||
-- self always refer to the initial table the metamethod was called on, super refers to the class currently being searched for a value.
|
||||
__index = function(self, k, super)
|
||||
local proxied = methods["!"..tostring(k)]
|
||||
if proxied ~= nil and proxied ~= different then -- proxied methods
|
||||
lastIndexed = self
|
||||
return proxied
|
||||
end
|
||||
for _, t in ipairs((super or self).__super) do -- search in super (will follow __index metamethods)
|
||||
local val = rawget(t, k)
|
||||
if val ~= nil and val ~= different then return val end
|
||||
-- Also covers the case when different search is enabled and the raw t[k] returns an identical value, so the __index metamethod search will be tried for another value.
|
||||
if getmetatable(t) and getmetatable(t).__index then
|
||||
val = getmetatable(t).__index(self, k, t)
|
||||
if val ~= nil and val ~= different then return val end
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
--- Create a new class width parents ... (left-to-right priority).
|
||||
function makeclass(...)
|
||||
local class = {
|
||||
__super = {} -- parent classes/tables list
|
||||
}
|
||||
for k, v in pairs(methods) do -- copy class methods
|
||||
if k:sub(1, 1) ~= "!" then class[k] = v end -- except proxied methods
|
||||
end
|
||||
setmetatable(class, class)
|
||||
for _, t in ipairs({...}) do -- fill super
|
||||
if getmetatable(t) == nil then setmetatable(t, t) end -- auto-metatable the table
|
||||
if type(t.__inherit) == "function" then t = t:__inherit(class) or t end -- call __inherit callback
|
||||
table.insert(class.__super, t)
|
||||
end
|
||||
-- Metamethods query are always raw and thefore don't follow our __index, so we need to manually define thoses.
|
||||
for _, metamethod in ipairs(metamethods) do
|
||||
local inSuper = class:__index(metamethod)
|
||||
if inSuper and rawget(class, metamethod) == nil then
|
||||
rawset(class, metamethod, inSuper)
|
||||
end
|
||||
end
|
||||
return class
|
||||
end
|
||||
|
||||
--- The class which will be a parents for all the other classes.
|
||||
-- We add some pretty-printing default in here. We temporarly remove the metatable in order to avoid a stack overflow.
|
||||
BaseClass = makeclass {
|
||||
__name = "class",
|
||||
__tostring = function(self)
|
||||
local mt, name = getmetatable(self), self.__name
|
||||
setmetatable(self, nil)
|
||||
local str = ("%s (%s)"):format(tostring(name), tostring(self))
|
||||
setmetatable(self, mt)
|
||||
return str
|
||||
end
|
||||
}
|
||||
|
||||
--- Class Commons implementation.
|
||||
-- https://github.com/bartbes/Class-Commons
|
||||
if common_class and not common then
|
||||
common = {}
|
||||
-- class = common.class(name, table, parents...)
|
||||
function common.class(name, table, ...)
|
||||
return BaseClass(table, ...){ __name = name, new = table.init }
|
||||
end
|
||||
-- instance = common.instance(class, ...)
|
||||
function common.instance(class, ...)
|
||||
return class:new(...)
|
||||
end
|
||||
end
|
||||
|
||||
return BaseClass
|
||||
342
daccord.can
Normal file
342
daccord.can
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
--- Yet another MPD client, but this time I wrote it so it's the best.
|
||||
-- Focused on an instant and hopefully powerful search.
|
||||
-- Currently only a console interface is available.
|
||||
--
|
||||
-- Dependencies: candran, luasocket, lcurses
|
||||
--
|
||||
-- Highly unintuitive keybinding:
|
||||
-- * Up goes up, down goes down.
|
||||
-- * Tab switch between search and current playlist.
|
||||
-- * Control+W quits.
|
||||
-- * Control+Space toggle play/pause.
|
||||
-- * In search:
|
||||
-- * Control+A add all the results to the playlist.
|
||||
-- * Enter add currently selected song to the playlist.
|
||||
-- * Type "TagName=" to trigger tag completion: select a tag and press Enter to select it. Or type it manually and exit tag completion by typing a space.
|
||||
-- * In current playlist:
|
||||
-- * Enter play currently selected song.
|
||||
-- * Delete remove selected song from playlist.
|
||||
--
|
||||
-- Most of what was initialy planned isn't implemented yet. Hopefully all will be finished before you're six feet under.
|
||||
--
|
||||
-- Curently implemented:
|
||||
-- * Play/pause, current song status
|
||||
-- * Current playlist, jump to song, remove song
|
||||
-- * Instant search among all tags or specific tags
|
||||
-- * Instant tag value completion
|
||||
-- * Overly optimistic planned features list in a comment in the main file
|
||||
--
|
||||
-- Stuff which is planned:
|
||||
-- * Sticker search
|
||||
-- * Search requests where keyword and tag selectors can be assigned with a $probabilty, generating infinite playlists (composer=chopin$.5)
|
||||
-- * Relational operators for tags and stickers (song ratings) (rating>.5)
|
||||
-- * Random, single, consume and other MPD play mode are determined by :keywords in the search query (:rand, :limit=5, :asc=Track, etc.)
|
||||
-- * The endgoal would be that playlists would be entirely determined and contained in a single search query (add some saved storage interface).
|
||||
-- Which we would therfore mean we can regenerate them on the fly when the MPD database is updated, or probabilities where used in the query.
|
||||
-- * A non-console GUI. Should be doable considering everything is neatly contained in gui.can, but damn are thoses text widget I made weird.
|
||||
--
|
||||
-- This version of the software is licensed under the terms of the Apache License, version 2 (https://www.apache.org/licenses/LICENSE-2.0.txt).
|
||||
|
||||
-- Copyright 2017-2018 Étienne "Reuh" Fildadut
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
require("candran").setup()
|
||||
let gui = require("gui")
|
||||
let mpc = require("mpc")
|
||||
|
||||
-- Constants
|
||||
let VERSION = "0.0.3"
|
||||
|
||||
-- Configuration
|
||||
let config = {
|
||||
-- MPD server
|
||||
host = "localhost",
|
||||
port = 6600,
|
||||
password = "",
|
||||
-- Interface
|
||||
songDisplay = { "Track", { "Title", "file" }, "Artist", "Album" } -- list of tags or list of alternative tags (first one to exist will be used) to display for each song
|
||||
}
|
||||
(loadfile("config.lua", "t", config) or () end)()
|
||||
|
||||
-- Returns list of fields to display for the song s
|
||||
let songTable = (s)
|
||||
let t = {}
|
||||
for i, field in ipairs(config.songDisplay) do
|
||||
if type(field) ~= "table" then field = { field } end
|
||||
t[i] = "∅"
|
||||
for _, option in ipairs(field) do
|
||||
if s[option] then
|
||||
t[i] = tostring(s[option])
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
-- Connect
|
||||
mpc.log = () end
|
||||
mpc = mpc("localhost", 6600)
|
||||
if config.password ~= "" then
|
||||
mpc:password(config.password)
|
||||
end
|
||||
|
||||
-- Valid tags list
|
||||
let tags = [
|
||||
let r, l = mpc:tagtypes()
|
||||
for _, t in ipairs(l) do
|
||||
push t.tagtype
|
||||
end
|
||||
]
|
||||
table.insert(tags, "file")
|
||||
|
||||
-- State
|
||||
let tagCompleting = {
|
||||
tag = nil,
|
||||
start = nil,
|
||||
stop = nil
|
||||
}
|
||||
|
||||
let results, playlist = {}, {}
|
||||
let state = "stop"
|
||||
|
||||
gui {
|
||||
{
|
||||
type = "tabs",
|
||||
focused = true,
|
||||
|
||||
width = "extend",
|
||||
height = "extend",
|
||||
|
||||
-- Search
|
||||
{
|
||||
{
|
||||
id = "prompt",
|
||||
|
||||
type = "input",
|
||||
focused = true,
|
||||
|
||||
width = "extend",
|
||||
height = "1em",
|
||||
|
||||
onTextInput = :()
|
||||
let list = @byId("list")
|
||||
list:clear()
|
||||
|
||||
-- Tag complete
|
||||
tagCompleting.tag = nil
|
||||
if @sub(1, @cursorPosition):match("[A-Za-z_]+=[^\" ]*$") or @sub(1, @cursorPosition):match("[A-Za-z_]+=\"[^\"]*$") then
|
||||
let start, sel, val, stop
|
||||
if @sub(1, @cursorPosition):match("[A-Za-z_]+=[^\" ]*$") then
|
||||
start, sel, val, stop = @sub(1, @cursorPosition):match("()([A-Za-z_]+)=([^\" ]*)()$")
|
||||
else
|
||||
start, sel, val, stop = @sub(1, @cursorPosition):match("()([A-Za-z_]+)=\"([^\"]*)()$")
|
||||
end
|
||||
|
||||
for _, tag in ipairs(tags) do
|
||||
if tag:lower() == sel:lower() then
|
||||
let r, songs = mpc:list(tag)
|
||||
if r then results = songs end
|
||||
|
||||
for _, s in ipairs(results) do
|
||||
if s[tag]:lower():match(val:lower()) then -- filter val
|
||||
list:insert{tostring(s[tag])}
|
||||
end
|
||||
end
|
||||
|
||||
tagCompleting.tag = tag
|
||||
tagCompleting.start = start
|
||||
tagCompleting.stop = stop
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
-- Song search
|
||||
else
|
||||
-- Build query
|
||||
let query = {}
|
||||
|
||||
-- Any selectors
|
||||
let withoutSel = @content:gsub("[A-Za-z_]+=[^\" ]+", ""):gsub("[A-Za-z_]+=\"[^\"]*\"", "")
|
||||
for word in withoutSel:gmatch("[^%s]+") do
|
||||
table.insert(query, "any")
|
||||
table.insert(query, word)
|
||||
end
|
||||
|
||||
-- Tag selectors
|
||||
for tag, val in @content:gmatch("([A-Za-z_]+)=([^\" ]+)") do
|
||||
table.insert(query, tag)
|
||||
table.insert(query, val)
|
||||
end
|
||||
for tag, val in @content:gmatch("([A-Za-z_]+)=\"([^\"]*)\"") do
|
||||
table.insert(query, tag)
|
||||
table.insert(query, val)
|
||||
end
|
||||
|
||||
-- Search
|
||||
let r, songs = mpc:search(unpack(query))
|
||||
if r then results = songs end
|
||||
|
||||
-- Update widget
|
||||
for _, s in ipairs(results) do
|
||||
list:insert(songTable(s))
|
||||
end
|
||||
end
|
||||
end
|
||||
},
|
||||
{
|
||||
type = "fill",
|
||||
|
||||
width = "extend",
|
||||
height = "1em",
|
||||
|
||||
fill = "ACS_HLINE"
|
||||
},
|
||||
{
|
||||
id = "list",
|
||||
|
||||
type = "list",
|
||||
focused = true,
|
||||
|
||||
width = "extend",
|
||||
height = "extend",
|
||||
|
||||
onSelect = :(l)
|
||||
-- Tag complete
|
||||
if tagCompleting.tag then
|
||||
let prompt = @byId("prompt")
|
||||
if results[l[1]][tagCompleting.tag]:match(" ") then
|
||||
prompt:replace(tagCompleting.start, tagCompleting.stop, tagCompleting.tag.."=\""..results[l[1]][tagCompleting.tag].."\"")
|
||||
else
|
||||
prompt:replace(tagCompleting.start, tagCompleting.stop, tagCompleting.tag.."="..results[l[1]][tagCompleting.tag])
|
||||
end
|
||||
-- Song search
|
||||
else
|
||||
for _, i in ipairs(l) do
|
||||
if results[i] then
|
||||
mpc:add(results[i].file)
|
||||
end
|
||||
end
|
||||
@byId("playlist"):updateAfter(1)
|
||||
|
||||
let status = @byId("status")
|
||||
status:set("Added "..#l.." songs to the current playlist") -- FIXME
|
||||
end
|
||||
end
|
||||
}
|
||||
},
|
||||
|
||||
-- Playlist
|
||||
{
|
||||
{
|
||||
id = "playlist",
|
||||
|
||||
type = "list",
|
||||
focused = true,
|
||||
|
||||
width = "extend",
|
||||
height = "extend",
|
||||
|
||||
updateInterval = 5,
|
||||
|
||||
onUpdate = :()
|
||||
let r, songs = mpc:playlistinfo()
|
||||
if r then playlist = songs end
|
||||
|
||||
for i, s in ipairs(playlist) do
|
||||
let item = songTable(s)
|
||||
if @content[i] then
|
||||
if @content[i] ~= item then
|
||||
@remove(i)
|
||||
@insert(i, item)
|
||||
end
|
||||
else
|
||||
@insert(i, item)
|
||||
end
|
||||
end
|
||||
|
||||
while #@content > #playlist do
|
||||
@remove()
|
||||
end
|
||||
end,
|
||||
|
||||
onSelect = :(l)
|
||||
if #playlist > 0 then
|
||||
mpc:playid(playlist[l[1]].Id)
|
||||
end
|
||||
end,
|
||||
|
||||
onControl = :(control)
|
||||
if control == "delete" then
|
||||
mpc:deleteid(playlist[@selected].Id)
|
||||
@remove(@selected)
|
||||
end
|
||||
end
|
||||
}
|
||||
}
|
||||
},
|
||||
-- Status bar
|
||||
{
|
||||
id = "play-position",
|
||||
type = "slider",
|
||||
|
||||
width = "extend",
|
||||
height = "1em"
|
||||
},
|
||||
{
|
||||
id = "status",
|
||||
type = "label",
|
||||
focused = true,
|
||||
|
||||
width = "extend",
|
||||
height = "1em",
|
||||
|
||||
content = "No current song",
|
||||
|
||||
updateInterval = 1,
|
||||
|
||||
onUpdate = :()
|
||||
let r, s = mpc:currentsong()
|
||||
if r then
|
||||
if s.file then
|
||||
@set(table.concat(songTable(s), " - "))
|
||||
else
|
||||
@set("daccord v"..VERSION.." - nothing playing")
|
||||
end
|
||||
end
|
||||
|
||||
let r, s = mpc:status()
|
||||
if r then
|
||||
state = s.state
|
||||
pos = @byId("play-position")
|
||||
pos:setHead(state == "play" and "▶️" or "⏸")
|
||||
pos:setMax(s.duration)
|
||||
pos:set(s.elapsed)
|
||||
end
|
||||
end,
|
||||
|
||||
onControl = :(control)
|
||||
if control == "space" then
|
||||
if state == "play" then
|
||||
mpc:pause()
|
||||
else
|
||||
mpc:play()
|
||||
end
|
||||
end
|
||||
@onUpdate()
|
||||
end
|
||||
},
|
||||
|
||||
onClose = :() @exit() end
|
||||
}
|
||||
493
gui.can
Normal file
493
gui.can
Normal file
|
|
@ -0,0 +1,493 @@
|
|||
--- Yet another badly designed and implemented GUI library, but this time I wrote it so it's the best.
|
||||
-- Part of daccord. See daccord.can for more information.
|
||||
|
||||
-- Copyright 2017-2018 Étienne "Reuh" Fildadut
|
||||
--
|
||||
-- Licensed under the Apache License, Version 2.0 (the "License");
|
||||
-- you may not use this file except in compliance with the License.
|
||||
-- You may obtain a copy of the License at
|
||||
--
|
||||
-- http://www.apache.org/licenses/LICENSE-2.0
|
||||
--
|
||||
-- Unless required by applicable law or agreed to in writing, software
|
||||
-- distributed under the License is distributed on an "AS IS" BASIS,
|
||||
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
-- See the License for the specific language governing permissions and
|
||||
-- limitations under the License.
|
||||
|
||||
let curses = require("curses")
|
||||
let class = require("classtoi")
|
||||
let sleep = require("socket").sleep
|
||||
|
||||
let screen
|
||||
|
||||
os.setlocale("")
|
||||
|
||||
let everyWidget = {}
|
||||
|
||||
let widget = class {
|
||||
_exit = false,
|
||||
|
||||
_redraw = true,
|
||||
x = 0,
|
||||
y = 0,
|
||||
w = 0,
|
||||
h = 0,
|
||||
|
||||
parent = {},
|
||||
focused = false,
|
||||
|
||||
updateInterval = -1,
|
||||
_nextUpdate = os.time(), -- -1 = no update
|
||||
|
||||
new = :(data)
|
||||
table.insert(everyWidget, self)
|
||||
|
||||
-- Copy properties
|
||||
for k, v in pairs(data) do
|
||||
@[k] = v
|
||||
end
|
||||
|
||||
-- Dimensions
|
||||
if not @parent._firstX then @parent._firstX, @parent._firstY = 0, 0 end
|
||||
@x, @y = @parent._firstX, @parent._firstY
|
||||
|
||||
if @width == "extend" @w = @parent.w - @x
|
||||
elseif @width:match("%d+em$") @w = tonumber(@width:match("%d+"))
|
||||
if @height == "extend" then
|
||||
@h = @parent.h - @y
|
||||
@parent._lastVerticalExtend = self -- will be resized if vertical space is needed
|
||||
@parent._afterLastVerticalExtend = {}
|
||||
elseif @height:match("%d+em$") @h = tonumber(@height:match("%d+"))
|
||||
|
||||
if @y >= @parent.h then -- resize
|
||||
@parent._lastVerticalExtend:_resize(@parent._lastVerticalExtend.w, @parent._lastVerticalExtend.h - @h)
|
||||
@parent._firstY -= @h
|
||||
@y -= @h
|
||||
for _, el in ipairs(@parent._afterLastVerticalExtend) do -- move widgets
|
||||
el.y -= @h
|
||||
end
|
||||
end
|
||||
|
||||
@parent._firstX += @w
|
||||
if @parent._firstX >= @parent.w then @parent._firstX = 0 end -- newline
|
||||
@parent._firstY += @h
|
||||
if @parent._lastVerticalExtend ~= self and @parent._afterLastVerticalExtend then table.insert(@parent._afterLastVerticalExtend, self) end
|
||||
|
||||
-- Setup
|
||||
if @_setup then @_setup() end
|
||||
|
||||
screen:move(@parent.y + @y + @h, @parent.x + @x + @w)
|
||||
end,
|
||||
|
||||
exit = :()
|
||||
@_exit = true
|
||||
end,
|
||||
|
||||
byId = :(id)
|
||||
for _, el in ipairs(everyWidget) do
|
||||
if el.id == id return el
|
||||
end
|
||||
error("no element with id "..tostring(id))
|
||||
end,
|
||||
|
||||
updateAfter = :(time)
|
||||
@_nextUpdate = os.time() + time
|
||||
end
|
||||
}
|
||||
|
||||
let widgets = setmetatable({
|
||||
fill = widget {
|
||||
_draw = :()
|
||||
if type(@fill) == "string" then @fill = curses[@fill] end
|
||||
for y = @parent.y + @y, @parent.y + @y + @h - 1 do
|
||||
screen:move(y, @parent.x + @x)
|
||||
for x = @parent.x + @x, @parent.x + @x + @w - 1 do
|
||||
screen:addch(@fill or 32)
|
||||
end
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
input = widget {
|
||||
content = "",
|
||||
cursorPosition = 1,
|
||||
_input = :(charbuffer, control)
|
||||
let y, x = @parent.y + @y, @parent.x + @x + @cursorPosition-1
|
||||
if control == "backspace" then
|
||||
screen:mvdelch(y, x-1)
|
||||
if @cursorPosition > 1 then
|
||||
if @cursorPosition == 2 then -- utf8.offset(s, 0) returns the start of the last character, ie something we don't want
|
||||
@content = @content:sub(utf8.offset(@content, @cursorPosition))
|
||||
else
|
||||
@content = @content:sub(1, utf8.offset(@content, @cursorPosition-1)-1)
|
||||
.. @content:sub(utf8.offset(@content, @cursorPosition))
|
||||
end
|
||||
@cursorPosition -= 1
|
||||
@onTextInput()
|
||||
end
|
||||
elseif control == "right" then
|
||||
if @cursorPosition <= utf8.len(@content) then
|
||||
screen:addstr(@content:sub(utf8.offset(@content, @cursorPosition), utf8.offset(@content, @cursorPosition+1)-1))
|
||||
@cursorPosition += 1
|
||||
end
|
||||
elseif control == "left" then
|
||||
if @cursorPosition > 1 then
|
||||
screen:move(y, x-1)
|
||||
@cursorPosition -= 1
|
||||
end
|
||||
elseif charbuffer then
|
||||
screen:move(y, x)
|
||||
screen:winsstr(charbuffer)
|
||||
screen:move(y, x+1)
|
||||
@content = @content:sub(1, utf8.offset(@content, @cursorPosition)-1)
|
||||
.. charbuffer
|
||||
.. @content:sub(utf8.offset(@content, @cursorPosition))
|
||||
@cursorPosition += 1
|
||||
@onTextInput()
|
||||
end
|
||||
end,
|
||||
_draw = :()
|
||||
for y = @parent.y + @y, @parent.y + @y + @h - 1 do
|
||||
screen:move(y, @parent.x + @x)
|
||||
for x = @parent.x + @x, @parent.x + @x + @w - 1 do
|
||||
screen:addch(32)
|
||||
end
|
||||
end
|
||||
screen:mvaddstr(@parent.y + @y, @parent.x + @x, @content)
|
||||
end,
|
||||
_placeCursor = :()
|
||||
screen:move(@parent.y + @y, @parent.x + @x + @cursorPosition-1)
|
||||
return true
|
||||
end,
|
||||
sub = :(start, stop=utf8.len(@content))
|
||||
if stop < 1 or start >= utf8.len(@content) then
|
||||
return ""
|
||||
else
|
||||
return @content:sub(utf8.offset(@content, start), (utf8.offset(@content, stop+1) or (#@content+1))-1)
|
||||
end
|
||||
end,
|
||||
replace = :(start, stop, newText)
|
||||
if @cursorPosition >= stop then
|
||||
@cursorPosition += utf8.len(newText) - (stop - start)
|
||||
end
|
||||
@content = @sub(1, start-1) .. newText .. @sub(stop+1)
|
||||
@onTextInput()
|
||||
@_redraw = true
|
||||
end,
|
||||
onTextInput = :() end
|
||||
},
|
||||
|
||||
list = widget {
|
||||
content = {},
|
||||
columnWidth = {},
|
||||
selected = 1,
|
||||
scroll = 0,
|
||||
_redraw = true,
|
||||
_input = :(charbuffer, control)
|
||||
if control == "up" and @selected > 1 then
|
||||
@selected -= 1
|
||||
if @selected == @scroll then
|
||||
@scroll -= 1
|
||||
end
|
||||
@_redraw = true
|
||||
elseif control == "down" and @selected < #@content then
|
||||
@selected += 1
|
||||
if @selected == @scroll + @h + 1 then
|
||||
@scroll += 1
|
||||
end
|
||||
@_redraw = true
|
||||
end
|
||||
|
||||
if control == "enter" then
|
||||
@onSelect({@selected})
|
||||
elseif control == "A" then
|
||||
let len = #@content
|
||||
@onSelect([for i=1, len do i end])
|
||||
end
|
||||
end,
|
||||
_draw = :()
|
||||
let oY, oX = screen:getyx()
|
||||
for y = @parent.y + @y, @parent.y + @y + @h -1 do -- clear
|
||||
screen:mvaddstr(y, @parent.x + @x, (" "):rep(@w))
|
||||
end
|
||||
screen:move(@parent.y + @y, @parent.x + @x)
|
||||
for i=@scroll+1, @scroll + @h do -- draw
|
||||
if i == @selected then
|
||||
screen:standout()
|
||||
end
|
||||
let colx = @parent.x+@x
|
||||
for c=1, #@columnWidth do
|
||||
screen:mvaddstr(@parent.y+@y+i-1-@scroll, colx, (""):rep(@columnWidth[c])) -- FIXME: too lazy to do this the right way and extract utf8 substrings (also should probably check if the thing doesn't go too right)
|
||||
screen:mvaddstr(@parent.y+@y+i-1-@scroll, colx, @content[i] and @content[i][c] or "")
|
||||
colx += @columnWidth[c]
|
||||
end
|
||||
if i == @selected then
|
||||
screen:standend()
|
||||
end
|
||||
end
|
||||
screen:move(oY, oX)
|
||||
end,
|
||||
insert = :(pos, item)
|
||||
if item then
|
||||
table.insert(@content, pos, item)
|
||||
if @selected >= pos and #@content > 1 then
|
||||
@selected += 1
|
||||
end
|
||||
else
|
||||
item = pos
|
||||
table.insert(@content, item)
|
||||
end
|
||||
if #@content == 1 then -- column count is determined by 1st item (other items can do what they want, this isn't a dictatorship)
|
||||
@columnWidth = [for _=1,#item do 0 end]
|
||||
end
|
||||
if #item >= #@columnWidth then -- if the column fits into our dictatorship, update column width
|
||||
for c=1, #@columnWidth do
|
||||
if utf8.len(item[c]) > @columnWidth[c] then
|
||||
@columnWidth[c] = utf8.len(item[c]) + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@_redraw = true
|
||||
end,
|
||||
remove = :(pos=#@content)
|
||||
table.remove(@content, pos)
|
||||
if @selected >= pos and @selected > 1 then
|
||||
@selected -= 1
|
||||
end
|
||||
@_redraw = true
|
||||
end,
|
||||
clear = :()
|
||||
@.content = {}
|
||||
--@.columnWidth = {}
|
||||
@_redraw = true
|
||||
@selected = 1
|
||||
@scroll = 0
|
||||
end,
|
||||
onSelect = :() end
|
||||
},
|
||||
|
||||
tabs = widget {
|
||||
selected = 1,
|
||||
children = {},
|
||||
_children = {},
|
||||
_setup = :()
|
||||
for i, tab in ipairs(@) do
|
||||
@children[i] = { x = @x, y = @y, w = @w, h = @h }
|
||||
for _, el in ipairs(tab) do
|
||||
el.parent = @children[i]
|
||||
table.insert(@children[i], widgets[el.type]:new(el))
|
||||
end
|
||||
end
|
||||
@_children = @children[@selected]
|
||||
end,
|
||||
_input = :(charbuffer, control)
|
||||
if control == "tab" then
|
||||
@selected += 1
|
||||
if @selected > #@ then @selected = 1 end
|
||||
@_children = @children[@selected]
|
||||
for _, el in ipairs(@_children) do -- Force redraw
|
||||
el._redraw = true
|
||||
end
|
||||
end
|
||||
end,
|
||||
_resize = :(w, h)
|
||||
@w, @h = w, h
|
||||
for i, tab in ipairs(@) do
|
||||
for _, el in ipairs(@children[i]) do
|
||||
if el.x + el.w > w then el.w = w - el.x end
|
||||
if el.y + el.h > h then el.h = h - el.y end
|
||||
end
|
||||
end
|
||||
end
|
||||
},
|
||||
|
||||
label = widget {
|
||||
content = "Label",
|
||||
_draw = :()
|
||||
screen:mvaddstr(@parent.y + @y, @parent.x + @x, @content .. (" "):rep(@w - #@content))
|
||||
end,
|
||||
set = :(str)
|
||||
@content = str
|
||||
@_redraw = true
|
||||
end
|
||||
},
|
||||
|
||||
slider = widget {
|
||||
min = 0,
|
||||
max = 1,
|
||||
current = 0,
|
||||
head = "⏸",
|
||||
_draw = :()
|
||||
let len = math.ceil((@current - @min) / (@max - @min) * @w)
|
||||
screen:mvaddstr(@parent.y + @y, @parent.x + @x, ("="):rep(len-1))
|
||||
screen:addstr(@head .. (" "):rep(@w - len))
|
||||
end,
|
||||
set = :(current)
|
||||
@current = current
|
||||
@_redraw = true
|
||||
end,
|
||||
setMax = :(max)
|
||||
@max = max
|
||||
@_redraw = true
|
||||
end,
|
||||
setMin = :(min)
|
||||
@min = min
|
||||
@_redraw = true
|
||||
end,
|
||||
setHead = :(head)
|
||||
@head = head
|
||||
@_redraw = true
|
||||
end
|
||||
}
|
||||
}, {
|
||||
__index = (t, k)
|
||||
error("unknown widget "..tostring(k))
|
||||
end
|
||||
})
|
||||
|
||||
let recursiveApply = (list, fn)
|
||||
for _, el in ipairs(list) do
|
||||
if el._widget then
|
||||
el = el._widget
|
||||
end
|
||||
fn(el)
|
||||
if el._children then
|
||||
recursiveApply(el._children, fn)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return (ui)
|
||||
xpcall(()
|
||||
-- Init
|
||||
if not screen then
|
||||
screen = curses.initscr()
|
||||
curses.cbreak()
|
||||
curses.echo(false)
|
||||
screen:nodelay(true)
|
||||
end
|
||||
|
||||
-- Create widgets
|
||||
screen:clear()
|
||||
let h, w = screen:getmaxyx()
|
||||
let parent = {
|
||||
x = 0, y = 0,
|
||||
w = w, h = h
|
||||
}
|
||||
for _, el in ipairs(ui) do
|
||||
el.parent = parent
|
||||
el._widget = widgets[el.type]:new(el)
|
||||
end
|
||||
|
||||
-- Update loop
|
||||
while not widget._exit do
|
||||
-- Input
|
||||
local c = screen:getch()
|
||||
if c and c < 256 then
|
||||
let charbuffer = string.char(c)
|
||||
let control
|
||||
if c > 127 then -- multibyte char
|
||||
charbuffer ..= string.char(screen:getch())
|
||||
if c > 223 then
|
||||
charbuffer ..= string.char(screen:getch())
|
||||
if c > 239 then
|
||||
charbuffer ..= string.char(screen:getch())
|
||||
end
|
||||
end
|
||||
end
|
||||
if curses.unctrl(c):match("^%^") then -- control char
|
||||
charbuffer = nil
|
||||
let k = curses.unctrl(c)
|
||||
if k == "^?" then
|
||||
control = "backspace"
|
||||
elseif k == "^W" then
|
||||
control = "close"
|
||||
elseif k == "^J" then
|
||||
control = "enter"
|
||||
elseif k == "^I" then
|
||||
control = "tab"
|
||||
elseif k == "^@" then
|
||||
control = "space"
|
||||
elseif k == "^[" then
|
||||
let k = string.char(screen:getch()) .. string.char(screen:getch())
|
||||
if k == "[C" then
|
||||
control = "right"
|
||||
elseif k == "[D" then
|
||||
control = "left"
|
||||
elseif k == "[A" then
|
||||
control = "up"
|
||||
elseif k == "[B" then
|
||||
control = "down"
|
||||
elseif k == "[3" then
|
||||
k ..= string.char(screen:getch())
|
||||
if k == "[3~" then
|
||||
control = "delete"
|
||||
else
|
||||
error("unknown control "..tostring(k))
|
||||
end
|
||||
else
|
||||
error("unknown control "..tostring(k))
|
||||
end
|
||||
elseif k:match("^%^[A-Z]$") then
|
||||
control = k:match("^%^([A-Z])$")
|
||||
else
|
||||
error("unknown control "..tostring(k))
|
||||
end
|
||||
end
|
||||
|
||||
recursiveApply(ui, (el)
|
||||
if el.focused and el._input then
|
||||
el:_input(charbuffer, control)
|
||||
end
|
||||
if el.focused and control and el.onControl then
|
||||
el:onControl(control)
|
||||
end
|
||||
end)
|
||||
|
||||
if control == "close" then
|
||||
if ui.onClose then ui.onClose(widget) end
|
||||
end
|
||||
end
|
||||
|
||||
-- Update
|
||||
recursiveApply(ui, (el)
|
||||
if el._nextUpdate ~= -1 and os.difftime(el._nextUpdate, os.time()) <= 0 then
|
||||
if el.onUpdate then el:onUpdate() end
|
||||
if el.updateInterval ~= -1 then
|
||||
el._nextUpdate += el.updateInterval
|
||||
else
|
||||
el._nextUpdate = -1
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Redraw
|
||||
recursiveApply(ui, (el)
|
||||
if el._redraw and el._draw then
|
||||
el._redraw = false
|
||||
el:_draw()
|
||||
end
|
||||
end)
|
||||
|
||||
-- Place cursor
|
||||
let cursorVis = 0
|
||||
recursiveApply(ui, (el)
|
||||
if el._placeCursor and el:_placeCursor() then
|
||||
cursorVis = 2
|
||||
end
|
||||
end)
|
||||
curses.curs_set(cursorVis)
|
||||
|
||||
-- Done
|
||||
screen:refresh()
|
||||
sleep(0.03)
|
||||
end
|
||||
|
||||
curses.endwin()
|
||||
end, (err)
|
||||
curses.endwin()
|
||||
print(require("candran").messageHandler(err))
|
||||
os.exit(2)
|
||||
end)
|
||||
end
|
||||
371
mpc.lua
Normal file
371
mpc.lua
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
--- Music Player Daemon (MPD) client library.
|
||||
--
|
||||
-- Alows basic manipulation of the MPD protocol. This provides the client part, you will need a MPD-compatible server running on
|
||||
-- another machine.
|
||||
--
|
||||
-- Please see the MPD command reference on the [official documentation](http://www.musicpd.org/doc/protocol/command_reference.html).
|
||||
--
|
||||
-- You may want to generate a documentation for this file using LDoc: `ldoc .`. However, LDoc doesn't seem to
|
||||
-- appreciate my coding style so it doesn't display everything, but the rendered documentation should be usable enough.
|
||||
-- If you didn't understand something or think you missed something, please read the [source file](source/mpc.lua.html), which is
|
||||
-- largely commented.
|
||||
--
|
||||
-- Variables prefixed with a `_` are private. Don't use them if you don't know what you're doing.
|
||||
--
|
||||
-- *Requires* `luasocket` or `ctr.socket` (ctrµLua).
|
||||
--
|
||||
-- When I started this project in october 2015, there weren't really any other decent MPD client library for Lua.
|
||||
-- The ones available were either uncomplete or outdated, regarding either MPD or Lua. So I made this.
|
||||
-- I didn't want to publish it until I used it in something, since it's a pretty small library (I didn't know things like left-pad existed and people were ok with it).
|
||||
-- Well, too bad for me. As it turns out, today several new libraries have poped up and they look quite usable.
|
||||
-- Well, it's too late. Now this is yet another MPD library, but this time I wrote it so it's the best.
|
||||
--
|
||||
-- @author Reuh
|
||||
-- @release 0.2.0
|
||||
|
||||
-- Copyright (c) 2015-2018 Étienne "Reuh" Fildadut <fildadut@reuh.eu>
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
-- of this software and associated documentation files (the "Software"), to deal
|
||||
-- in the Software without restriction, including without limitation the rights
|
||||
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
-- copies of the Software, and to permit persons to whom the Software is
|
||||
-- furnished to do so, subject to the following conditions:
|
||||
--
|
||||
-- 1. The above copyright notice and this permission notice shall be included in
|
||||
-- all copies or substantial portions of the Software; and
|
||||
-- 2. You must cause any modified source files to carry prominent notices stating
|
||||
-- that you changed the files.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
|
||||
local socket
|
||||
if package.loaded["ctr.socket"] then
|
||||
socket = require("ctr.socket")
|
||||
else
|
||||
socket = require("socket")
|
||||
end
|
||||
|
||||
-- Parsing commands response helpers.
|
||||
local function cast(str)
|
||||
return tonumber(str, 10) or str
|
||||
end
|
||||
local function parseList(lines, starters)
|
||||
local list = {}
|
||||
if lines[1] ~= "OK" then
|
||||
if not starters then
|
||||
starters = { lines[1]:match("^[^:]+") } -- first tag for each song
|
||||
end
|
||||
local cursong -- currently parsing song
|
||||
for _, l in ipairs(lines) do
|
||||
if l == "OK" then
|
||||
break
|
||||
else
|
||||
for _, startType in ipairs(starters) do
|
||||
if l:match(startType..":") then -- next song
|
||||
cursong = {}
|
||||
table.insert(list, cursong)
|
||||
break
|
||||
end
|
||||
end
|
||||
cursong[l:match("^[^:]+")] = cast(l:match("^[^:]+%: (.*)$")) -- add tag
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
local function parseSticker(dict)
|
||||
for k, v in pairs(dict) do
|
||||
if k == "sticker" and v:match(".+=.+") then
|
||||
dict[k] = { [v:match("(.+)=.+")] = cast(v:match(".+=(.+)")) }
|
||||
end
|
||||
if type(v) == "table" then
|
||||
parseSticker(v)
|
||||
end
|
||||
end
|
||||
return dict
|
||||
end
|
||||
|
||||
--- Music Player Client object.
|
||||
-- Used to manipulate a single MPD server.
|
||||
-- @type mpc
|
||||
local mpc = {
|
||||
---## PUBLIC METHODS ##---
|
||||
-- The functions you should use.
|
||||
|
||||
--- Connect to a MPD server.
|
||||
-- @tparam string address server address
|
||||
-- @tparam number port server port
|
||||
-- @see mpcmodule
|
||||
init = function(self, address, port)
|
||||
self._address = address
|
||||
self._port = port
|
||||
|
||||
self:_connect()
|
||||
end,
|
||||
|
||||
--- Execute a command.
|
||||
-- A shortcut (and generally more useful version) is `mpc:commandName([arguments, ...])`, which will also try return a parsed version of the result
|
||||
-- in a nicer Lua representation as a second return value. See COMMANDS OVERWRITE.
|
||||
-- @tparam string command command name
|
||||
-- @tparam[opt] any ... command arguments
|
||||
-- @treturn[1] boolean true if it was a success, false otherwise
|
||||
-- @treturn[1] table List of the lines of the response (strings)
|
||||
-- @treturn[2] nil nil if nothing was received
|
||||
command = function(self, command, ...)
|
||||
self:_send({ command, ... })
|
||||
return self:_receive(true)
|
||||
end,
|
||||
|
||||
--- Called each time something can be logged.
|
||||
-- You will want to and can overwrite this function.
|
||||
-- @tparam string message message string
|
||||
-- @tparam[opt] any ... arguments to pass to message:format(...)
|
||||
log = function(self, message, ...)
|
||||
print(("[mpc.lua@%s:%s] %s"):format(self._address, self._port, message:format(...)))
|
||||
end,
|
||||
|
||||
---## COMMANDS OVERWRITE ##---
|
||||
-- Theses functions overwrite some MPD's command, to add somme pratical features.
|
||||
-- In particular, every MPD command which return something will be wrapped here so the second return value is either (the list of lines is still available as a third return value):
|
||||
-- * A list of dictionnaries, if the command can return a list of similar elements (playlistinfo, search, ...)
|
||||
-- * A dictionnary if the command return something which isn't a list (status, currentsong, ...)
|
||||
-- The key names are the same used by MPD in the responses. Numbers will be automatically casted to a Lua number, every other value will be a string.
|
||||
-- Commands which return nothing will return nil as a second return value.
|
||||
-- Most overwrites are generated at the end of the file using the overwrites lists defined below.
|
||||
-- Signature for every command called through `mpc:commandName`:
|
||||
-- @tparam[opt] any ... command arguments
|
||||
-- @treturn[1] boolean true if it was a success
|
||||
-- @treturn[1] table dictionnary or list of dictionnary containing the response data, or nil if there is no response data to be expected
|
||||
-- @treturn[1] table List of the lines of the response (strings)
|
||||
-- @treturn[2] boolean false if there was an error
|
||||
-- @treturn[2] string error message
|
||||
-- @treturn[3] nil nil if nothing was received
|
||||
|
||||
--- Sends the close command and close the socket.
|
||||
-- This will log any uncomplete message received, if any. Returns nothing.
|
||||
-- Call this when you are done with the mpc object.
|
||||
-- Note however, that the client will automatically reconnect if you reuse it later.
|
||||
close = function(self)
|
||||
self:_send("close")
|
||||
self._socket:close()
|
||||
|
||||
if #self._buffer > 0 then
|
||||
self:log("UNCOMPLETLY RECEIVED MESSAGE:\n\t%s", table.concat(self._buffer, "\n\t"))
|
||||
end
|
||||
self:log("CLOSED")
|
||||
end,
|
||||
|
||||
--- Sends the password command.
|
||||
-- This will store the password in a variable so it can be resent in case of disconnection.
|
||||
-- @tparam string password the password
|
||||
password = function(self, password)
|
||||
self._password = password
|
||||
local success, lines = self:command("password", password)
|
||||
return success, not success and lines[1] or nil, lines
|
||||
end,
|
||||
|
||||
--- Returns a chunk of albumart.
|
||||
-- See the MPD documentation. The raw bytes will be stored in the `chunk` field of the response dictionnary.
|
||||
albumart = function(self, ...)
|
||||
local success, lines = self:command("albumart", ...)
|
||||
return success, success and {
|
||||
size = lines[1]:match("size: (%d+)"),
|
||||
binary = lines[2]:match("binary: (%d+)"),
|
||||
chunk = table.concat(lines, "", 3, #lines-1)
|
||||
} or lines[1], lines
|
||||
end,
|
||||
|
||||
--- Sticker commands.
|
||||
-- Will parse stickers values in dictionnary.sticker.name = value.
|
||||
sticker = function(self, action, ...)
|
||||
if action == "list" or action == "find" then
|
||||
local success, lines = self:command("sticker", action, ...)
|
||||
return success, success and parseSticker(parseList(lines)) or lines[1], lines
|
||||
elseif action == "get" then
|
||||
local success, lines = self:command("sticker", action, ...)
|
||||
return success, success and parseSticker(parseList(lines)[1]) or lines[1], lines
|
||||
else
|
||||
local success, lines = self:command("sticker", action, ...)
|
||||
return success, not success and lines[1] or nil, lines
|
||||
end
|
||||
end,
|
||||
|
||||
--- Commands which will return a list of dictionnaries.
|
||||
_overwriteDictList = {
|
||||
"idle", -- Querying MPD's status
|
||||
"playlistfind", "playlistid", "playlistinfo", "playlistsearch", "plchanges", "plchangesposid", -- The current playlist
|
||||
"listplaylist", "listplaylistinfo", "listplaylists", -- The current playlist
|
||||
"count", "find", "list", "listall", "listallinfo", "search", listfiles = { "file", "directory" }, lsinfo = { "file", "directory" }, -- The music database
|
||||
"listmounts", "listneighbors", -- Mounts and neighbors
|
||||
"tagtypes", -- Connection settings
|
||||
"listpartitions", -- Partition commands
|
||||
"outputs", -- Audio output devices
|
||||
"commands", "notcommands", "urlhandlers", "decoders", -- Reflection
|
||||
"channels", "readmessages" -- Client to client
|
||||
},
|
||||
|
||||
--- Commands which will return a dictionnary.
|
||||
_overwriteDict = {
|
||||
"currentsong", "status", "stats", -- Querying MPD's status
|
||||
"readcomments", "update", "rescan", -- The music database
|
||||
"config" -- Reflection
|
||||
},
|
||||
|
||||
---## PRIVATE FUNCTIONS ##---
|
||||
-- Theses functions are intended to be used internally by mpc.lua.
|
||||
-- You can use them but they weren't meant to be used from the outside.
|
||||
|
||||
_socket = nil, -- socket object
|
||||
|
||||
_address = "", -- server address string
|
||||
_port = 0, -- server port integer
|
||||
|
||||
_password = "", -- server password string
|
||||
|
||||
_buffer = {}, -- received message buffer table
|
||||
|
||||
--- Connects to the server.
|
||||
-- The fuction will auto-login if a password was previously set.
|
||||
-- If the client was already connected, it will disconnect and then reconnect.
|
||||
_connect = function(self)
|
||||
if self._socket then self._socket:close() end
|
||||
|
||||
self._socket = assert(socket.tcp())
|
||||
assert(self._socket:connect(self._address, self._port))
|
||||
if self._socket.settimeout then self._socket:settimeout(0.1) end
|
||||
|
||||
assert(self:_receive(), "something went terribly wrong")
|
||||
self:log("CONNECTED")
|
||||
|
||||
if self._password ~= "" then self:password(self._password) end
|
||||
end,
|
||||
|
||||
--- Send a list of commands to the server.
|
||||
-- @tparam table commands List of commands (strings or tables). If table, the table represent the arguments list.
|
||||
-- ie, this `:_send({"play", 18})` is equivalent to `:_send("play 18")`.
|
||||
_send = function(self, ...)
|
||||
local commands = {...}
|
||||
for i, v in ipairs(commands) do
|
||||
if type(v) == "table" then
|
||||
local cmd = v[1]
|
||||
for j, k in ipairs(v) do
|
||||
if j > 1 then -- bweh
|
||||
if type(k) == "string" then
|
||||
cmd = cmd..(" %q"):format(k)
|
||||
elseif type(k) == "table" then
|
||||
cmd = cmd..(k[1] or "")..":"..(k[2] or "")
|
||||
else
|
||||
cmd = cmd.." "..tostring(k)
|
||||
end
|
||||
end
|
||||
end
|
||||
commands[i] = cmd
|
||||
end
|
||||
end
|
||||
|
||||
local success, err = self._socket:send(table.concat(commands, "\n").."\n")
|
||||
if not success then
|
||||
if err == "closed" then
|
||||
self:log("CONNECTION CLOSED, RECONNECTING")
|
||||
self:_connect()
|
||||
self:_send(...)
|
||||
else
|
||||
error("error while sending data to MPD server: "..err)
|
||||
end
|
||||
end
|
||||
|
||||
self:log("SENT:\n\t%s", table.concat(commands, "\n\t"))
|
||||
end,
|
||||
|
||||
--- Receive a single server response.
|
||||
-- @tparam boolean block true to block until a message is received
|
||||
-- @treturn[1] boolean true if was a success, false otherwise
|
||||
-- @treturn[1] table List of the lines of the response (strings)
|
||||
-- @treturn[2] nil nil if nothing was received
|
||||
_receive = function(self, block)
|
||||
local success
|
||||
local received
|
||||
|
||||
repeat
|
||||
local response, err = self._socket:receive()
|
||||
|
||||
if response and response ~= "" then
|
||||
table.insert(self._buffer, response)
|
||||
|
||||
if response:sub(1, 2) == "OK" or response:sub(1, 3) == "ACK" then
|
||||
success = response:sub(1, 2) == "OK"
|
||||
|
||||
received = self._buffer
|
||||
self._buffer = {}
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
elseif err == "closed" then
|
||||
self:log("CONNECTION CLOSED, RECONNECTING")
|
||||
self:_connect()
|
||||
return self:_receive()
|
||||
elseif err ~= "timeout" then
|
||||
error("error while receiving data from MPD server: "..err)
|
||||
end
|
||||
until not block and (not response or response == "")
|
||||
|
||||
if not received then return nil end
|
||||
|
||||
self:log("RECEIVED:\n\t%s", table.concat(received, "\n\t"))
|
||||
return success, received
|
||||
end
|
||||
}
|
||||
|
||||
-- Overwrites for commands which return lists of dictionnaries
|
||||
for k, v in pairs(mpc._overwriteDictList) do
|
||||
local command, starters
|
||||
if type(k) == "number" then command = v
|
||||
else command, starters = k, v end
|
||||
mpc[command] = function(self, ...)
|
||||
local success, lines = self:command(command, ...)
|
||||
return success, success and parseList(lines, starters) or lines[1], lines
|
||||
end
|
||||
end
|
||||
-- Overwrites for commands which return a single dictionnary
|
||||
for k, v in pairs(mpc._overwriteDict) do
|
||||
local command, starters
|
||||
if type(k) == "number" then command = v
|
||||
else command, starters = k, v end
|
||||
mpc[command] = function(self, ...)
|
||||
local success, lines = self:command(command, ...)
|
||||
return success, success and parseList(lines, starters)[1] or lines[1], lines
|
||||
end
|
||||
end
|
||||
|
||||
--- The module returns a constructor function.
|
||||
-- Calling this function will create a new MPC object.
|
||||
-- The arguments will be passed to the object's `:init` method.
|
||||
-- @within Module
|
||||
-- @function mpcmodule
|
||||
-- @usage local mpc = require("mpc")("localhost", 6600) -- where "localhost", 6600 are :init's arguments
|
||||
return setmetatable(mpc, {
|
||||
__call = function(t, ...)
|
||||
local object = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
if mpc[k] then
|
||||
return mpc[k]
|
||||
elseif k:sub(1, 1) ~= "_" then
|
||||
return function(self, ...)
|
||||
local success, lines = mpc.command(self, k, ...)
|
||||
return success, not success and lines[1] or nil, lines
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
object:init(...)
|
||||
return object
|
||||
end
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue