Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: overhaul animation configuration #251

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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