From b9c275b9795ce7a852a4b370d592422000d303c7 Mon Sep 17 00:00:00 2001 From: j-hui Date: Sun, 7 Jul 2024 12:17:28 -0400 Subject: [PATCH] feat!: overhaul animation configuration This is still a work in progress and needs more thought. Goals of this overhaul are: - Make configuration concise, explicit, and extensible - Get rid of ambiguity warts like `{ "dots" }` vs `"dots"` - Introduce a default BAD pattern that is shown when patterns are misconfigured - Make it easier to inspect, debug, and compose animations At the moment, animations are just a mess of types. Maybe it's time to introduce a proper `Anime` class/interface with methods and everything. --- lua/fidget/progress/display.lua | 63 +++++++++++++++++------ lua/fidget/spinner.lua | 89 +++++++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 27 deletions(-) diff --git a/lua/fidget/progress/display.lua b/lua/fidget/progress/display.lua index 25ff0a5..d69059d 100644 --- a/lua/fidget/progress/display.lua +++ b/lua/fidget/progress/display.lua @@ -43,13 +43,15 @@ M.options = { --- Icon shown when all LSP progress tasks are complete --- - --- When a string literal is given (e.g., `"✔"`), it is used as a static icon; - --- when a table (e.g., `{"dots"}` or `{ pattern = "clock", period = 2 }`) is - --- given, it is used to generate an animation function; when a function is - --- specified (e.g., `function(now) return now % 2 < 1 and "+" or "-" end`), - --- it is used as the animation function. + --- When a string literal is given (e.g., `"✔"`), it is used as a static icon. --- - --- See also: |fidget.spinner.Manga| and |fidget.spinner.Anime|. + --- When a table (e.g., `{ pattern = "clock", period = 2 }`) is given, it is + --- used to generate an animation function. See also: |fidget.spinner.Manga|. + --- + --- When a function is given, it is used as the animation function. This + --- function is passed the timestamp at each frame and should return the frame + --- contents, e.g., `function(now) return now % 2 < 1 and ". " or " ." end`. + --- See also: |fidget.spinner.Anime|. --- ---@type string|Manga|Anime done_icon = "✔", @@ -71,14 +73,18 @@ M.options = { --- Icon shown when LSP progress tasks are in progress --- - --- When a string literal is given (e.g., `"✔"`), it is used as a static icon; - --- when a table (e.g., `{"dots"}` or `{ pattern = "clock", period = 2 }`) is - --- given, it is used to generate an animation function; when a function is - --- specified (e.g., `function(now) return now % 2 < 1 and "+" or "-" end`), - --- it is used as the animation function. + --- When a string literal is given (e.g., `"✔"`), it is used as a static icon. + --- + --- When a table (e.g., `{ pattern = "clock", period = 2 }`) is given, it is + --- used to generate an animation function. See also: |fidget.spinner.Manga|. + --- + --- When a function is given, it is used as the animation function. This + --- function is passed the timestamp at each frame and should return the frame + --- contents, e.g., `function(now) return now % 2 < 1 and ". " or " ." end`. + --- See also: |fidget.spinner.Anime|. --- ---@type string|Manga|Anime - progress_icon = { "dots" }, + progress_icon = { pattern = "dots" }, --- Highlight group for in-progress LSP tasks --- @@ -169,12 +175,31 @@ M.options = { --- hls = { --- name = "Haskell Language Server", --- priority = 60, - --- icon = fidget.progress.display.for_icon(fidget.spinner.animate("triangle", 3), "💯"), + --- icon = fidget.progress.display.for_icon( + --- fidget.spinner.animate({ + --- pattern = { + --- " ", + --- "= ", + --- ">= ", + --- ">>= ", + --- " >>=", + --- " >>", + --- " >", + --- }, + --- }), + --- " <> " + --- ), --- skip_history = false, --- }, --- rust_analyzer = { --- name = "Rust Analyzer", - --- icon = fidget.progress.display.for_icon(fidget.spinner.animate("arrow", 2.5), "🦀"), + --- icon = fidget.progress.display.for_icon( + --- fidget.spinner.animate({ + --- pattern = fidget.spinner.patterns.arrow, + --- period = 2.5, + --- }), + --- "🦀" + --- ), --- update_hook = function(item) --- require("fidget.notification").set_content_key(item) --- if item.hidden == nil and string.match(item.annote, "clippy") then @@ -217,12 +242,18 @@ end function M.make_config(group) local progress = M.options.progress_icon if type(progress) == "table" then - progress = spinner.animate(progress[1] or progress.pattern, progress.period) + progress = spinner.animate(progress) + end + if type(progress) ~= "function" and type(progress) ~= "string" then + progress = spinner.bad end local done = M.options.done_icon if type(done) == "table" then - done = spinner.animate(done[1] or done.pattern, done.period) + done = spinner.animate(done) + end + if type(done) ~= "function" and type(done) ~= "string" then + done = spinner.bad end local config = { diff --git a/lua/fidget/spinner.lua b/lua/fidget/spinner.lua index 282dd3f..a6b1443 100644 --- a/lua/fidget/spinner.lua +++ b/lua/fidget/spinner.lua @@ -1,33 +1,100 @@ ---@mod fidget.spinner Spinner animations -local spinner = {} -spinner.patterns = require("fidget.spinner.patterns") +local M = {} +M.patterns = require("fidget.spinner.patterns") +local logger = require("fidget.logger") + +--- The frames of an animation. +--- +--- Either an array of strings, which comprise each frame of the animation, +--- or a string referring to the name of a built-in pattern. Note that this +--- means `{ "dots" }` is different from `"dots"`; the former is a static +--- animation, consisting of a single frame, `dots`, while the latter refers to +--- the built-in pattern named "dots". +--- +--- Specifying built-in patterns by name like this is DEPRECATED and will be +--- removed in a future release; instead of `"dots"`, directly import the +--- pattern using `require("fidget.spinner.patterns").dots`. +--- +--- The array must contain at least one frame. +--- +---@alias Frames string[]|string --- A Manga is a table specifying an Anime to generate. --- +--- When the pattern is omitted, it will be looked up from the first position +--- instead, i.e., from key `[1]`. That means writing `{ the_pattern }` is +--- equivalent to `{ pattern = the_pattern }`. However, this behavior is +--- DEPRECATED and will be removed in a future release; prefer using using the +--- explicit `pattern` key. +--- --- The period is specified in seconds; if omitted, it defaults to 1. --- ----@alias Manga { pattern: string[]|string, period: number|nil } | { [1]: string[]|string } +---@alias Manga { pattern: Frames, period: number|nil, [1]: Frames|nil } --- An Anime is a function that takes a timestamp and renders a frame (string). --- --- Note that Anime is a subtype of Display. ---@alias Anime fun(now: number): string +--- A basic Anime function used to indicate an error. +--- +--- Returned by `spinner.animate()` when there is an error, to ensure that +--- Fidget will work even with a semi-broken config. +--- +--- An Anime original, if you will. +--- +---@type Anime +function M.bad(now) + if math.floor(now) % 2 == 0 then + return " BAD_PATTERN " + else + return " " + end +end + --- Generate an Anime function that can be polled for spinner animation frames. --- --- The period is specified in seconds; if omitted, it defaults to 1. --- ----@param pattern string[]|string Either an array of frames, or the name of a known pattern ----@param period number|nil How long one cycle of the animation should take, in seconds ----@return Anime anime Call this function to compute the frame at some timestamp -function spinner.animate(pattern, period) - period = period or 1 +---@param manga Manga A Manga from which to generate an Anime +---@return Anime|string anime Get the frame at some timestamp, or a single static frame +function M.animate(manga) + local pattern = manga.pattern + + if pattern == nil then + logger.warn("Specifying the pattern like `{ pat }` is DEPRECATED; use `{ patter = pat }` instead.") + pattern = manga[1] + end + + if pattern == nil then + logger.error("No pattern specified") + return M.bad + end + if type(pattern) == "string" then + logger.warn("Specifying a built-in pattern by name is DEPRECATED; import it from `fidget.spinner.patterns`.") + local pattern_name = pattern - pattern = spinner.patterns[pattern_name] - assert(pattern ~= nil, "Unknown pattern: " .. pattern_name) + pattern = M.patterns[pattern_name] + + if pattern == nil then + logger.error("Unknown pattern:", pattern_name) + return M.bad + end + end + + if type(pattern) ~= "table" or #pattern < 1 then + logger.error("Invalid pattern:", pattern) + return M.bad end + if #pattern == 1 then + logger.info("Animating single-frame pattern:", pattern[1]) + return pattern[1] + end + + local period = manga.period or 1 + --- Timestamp of the first frame of the animation. ---@type number? local origin @@ -44,4 +111,4 @@ function spinner.animate(pattern, period) end end -return spinner +return M