Skip to content

Commit

Permalink
feat!: overhaul animation configuration
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
j-hui committed Jul 7, 2024
1 parent a01443a commit b9c275b
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 27 deletions.
63 changes: 47 additions & 16 deletions lua/fidget/progress/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "",
Expand All @@ -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
---
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down
89 changes: 78 additions & 11 deletions lua/fidget/spinner.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -44,4 +111,4 @@ function spinner.animate(pattern, period)
end
end

return spinner
return M

0 comments on commit b9c275b

Please sign in to comment.