Skip to content

Instantly share code, notes, and snippets.

@techtycho
Last active October 15, 2023 01:13
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save techtycho/47eb79735a5e5c1cab85d0f6cd869e9f to your computer and use it in GitHub Desktop.
Save techtycho/47eb79735a5e5c1cab85d0f6cd869e9f to your computer and use it in GitHub Desktop.
Adding Fancy Tag Switch Effects In AwesomeWM

Adding Fancy Tag Switch Effects In AwesomeWM

tagswitch

Before We Begin

First of all, you need to have AwesomeWM installed and running. I'm using awesome-git, the development release. I'm not sure whether this would work on the stable release.

We will be creating the effect displayed in the GIF. We will build the widget from scratch, as well as the animations.

Note: My file structure may be different from yours, but I'll make sure this configuration works on all systems. But you also should not blindly follow me.

Note: AwesomeWM doesn't have any built-in standard animation libraries, there are some animation libraries built by the community like Awesome Animation Framework, but we won't use any of these.

Note: You will find the final file links at the bottom.

Steps

  1. Building The Widget
  2. Animating The Widget
  3. Adding Text
  4. Cleaning Up
  5. Adding To Keybindings
  6. Bonus: Encapsulating And Cleaning Up

Building The Widget

Let's start by creating some directories.

effects/
 |- tagswitch.lua
animations/
 |- init.lua
 |- fade.lua

Now let's start by writing:

animations/init.lua

local M = {}
return M

animations/fade.lua

return function (widget, delay)
end

We will modify these two files later when we get to the animating step. But now let's focus on building the widget.

Let's open effects/tagswitch.lua and import some standard libraries:

effects/tagswitch.lua

local awful = require("awful")
local wibox = require("wibox")

But before we continue, you need to write this in your rc.lua:

rc.lua

require("effects.tagswitch")

Then let's create the widget:

effects/tagswitch.lua

local M = wibox {

}

return M

Let's add some properties.

local M = wibox {
  visible = true,
  opacity = 1,
  bg      = "#000",
  fg      = "#fff",
  ontop   = true,
  height  = 90,
  width   = 180,
}

...

Let's display it on the screen.

...

M:setup {
  valign = "center",
  halign = "center",
  layout = wibox.container.place,
}

awful.placement.centered(M, { parent = awful.screen.focused() })

tagswitch.lua should now look like this:

local awful = require("awful")
local wibox = require("wibox")

local M = wibox {
  visible = true,
  opacity = 1,
  bg      = "#000",
  fg      = "#fff",
  ontop   = true,
  height  = 90,
  width   = 180,
}

M:setup {
  valign = "center",
  halign = "center",
  layout = wibox.container.place,
}

awful.placement.centered(M, { parent = awful.screen.focused() })

return M

And a black square in the middle of your screen should appear.

black

Let's quickly add the text:

M:setup {
  {
    id     = "text",
    markup = "<b>dev</b>",
    font   = "JetBrainsMono Nerd Font 37",
    widget = wibox.widget.textbox,
  },
  ...
}

We just used the text dev, that doesn't mean anything. Note: I used the font Jet Brains Mono, change the font to your liking, but make it big.

tagswitch.lua should now look like this:

local awful = require("awful")
local wibox = require("wibox")

local M = wibox {
  visible = true,
  opacity = 1,
  bg      = "#000",
  fg      = "#fff",
  ontop   = true,
  height  = 90,
  width   = 180,
}

M:setup {
  {
    id     = "text",
    markup = "<b>dev</b>",
    font   = "JetBrainsMono Nerd Font 37",
    widget = wibox.widget.textbox,
  },
  valign = "center",
  halign = "center",
  layout = wibox.container.place,
}

awful.placement.centered(M, { parent = awful.screen.focused() })

return M

And your square should like like this:

text

And now we are done building the widget! Next Step: Animating The Widget

Animating The Widget

Now let's continue to the hardest part in creating the effect, it is the animation. AwesomeWM doesn't have any standard animation libraries, there are some community-made libraries, but we will be making the animations from scratch.

First, let's import some standard libraries.

animations/fade.lua

local gears = require("gears")

Let's create some useful functions that we'll use later.

local function hide(w)
  w.visible = false
  w.opacity = 0
end

local function show(w)
  w.visible = true
  w.opacity = 1
end

Now let's write in the returned function:

...

return function (widget, delay)
  show(widget)

  gears.timer.start_new(delay, function ()
    hide(widget)
  end)
end

Now, in order to see our changes, we need to write in rc.lua:

rc.lua

local tagswitch = require("effects.tagswitch")
local fade = require("animations.fade")

And let's run our function:

rc.lua

fade(tagswitch, 1)

Here we give it the delay of 1 second, you can see that the square appears for 1 second and then disappears.

animations/fade.lua should now look like this:

local gears = require("gears")

local function hide(w)
  w.visible = false
  w.opacity = 0
end

local function show(w)
  w.visible = true
  w.opacity = 1
end

return function (widget, delay)
  show(widget)

  gears.timer.start_new(delay, function ()
    hide(widget)
  end)
end

We still didn't finish, but before we continue, let's alias the function gears.timer.start_new to something shorter as we will use it a lot in this tutorial:

local gears = require("gears")

local timeout = gears.timer.start_new

...

We can now change all occurrences of gears.timer.start_new to just timeout.

Let's add the animation:

...

timeout(delay, function ()
  -- The maximum number of frames
  local max = 10

  for f = 1, max do
    timeout((f / 10), function ()
      f = f + 1

      -- Decrease the opacity to make fading effect
      widget.opacity = widget.opacity - 0.1

      -- This runs when the animation is finished
      if f == max then
        hide(widget)
      end
    end)
  end
end)

You can play with the values to understand the code more. The animation should look like this:

slow

And animations/fade.lua should look like this:

local gears = require("gears")

local timeout = gears.timer.start_new

local function hide(w)
  w.visible = false
  w.opacity = 0
end

local function show(w)
  w.visible = true
  w.opacity = 1
end

return function (widget, delay)
  show(widget)

  timeout(delay, function ()
    -- The maximum number of frames
    local max = 10

    for f = 1, max do
      timeout((f / 10), function ()
        f = f + 1

        -- Decrease the opacity to make fading effect
        widget.opacity = widget.opacity - 0.1

        -- This runs when the animation is finished
        if f == max then
          hide(widget)
        end
      end)
    end
  end)
end

Now let's make the animation more smooth by playing with the values:

local max = 50

for f = 1, max do
  timeout((f / 200), function ()
    f = f + 1

    -- Decrease the opacity to make fading effect
    widget.opacity = widget.opacity - 0.02

    ...

Now we get a much smoother (and faster) animation:

smooth

Before we continue, let's make the widget hidden by default:

effects/tagswitch.lua

local M = wibox {
  visible = false,
  opacity = 0,
  ...
}

Now, what if we want to bind this effect to a keybinding:

rc.lua

awful.keyboard.append_global_keybindings({
	awful.key({ "Mod4" }, "o", function ()
		fade(tagswitch, 0.25)
	end)
})

Here we binded it to Super + o. Restart your window manager and try it. At first, you will think that it works perfectly, but when you start to spam it, you will find that the effect is kinda broken.

Let's fix that! Open animations/init.lua and add this (before the return statement):

animations/init.lua

M.busy = false

Now let's write this in animations/fade.lua:

animations/fade.lua

local gears = require("gears")
local animations = require("animations")

...

return function (widget, delay)
  if animations.busy then
    return
  end

  timeout(delay, function ()
    animations.busy = true

    ...
  end)
end

And write this in the finish if statement:

...

if f == max then
  animations.busy = false
  hide(widget)
end

We can see that we've partially fixed the bug, and when you spam Super + o, the effect doesn't spam. But that's not what we want.

Go to animations/init.lua and write:

animations/init.lua

M.busy   = false
M.timers = {}

Basically what we want to do, is that if the animation is busy, we want to stop all gears.timers and start all over again. We can do that by registering all the timers by appending them to animations.timers, and then we can write a basic for loop to loop over animations.timers (list) and stop all the timers, one by one.

We can append to the list by using Lua's builtin function table.insert. So replace all occurences of:

animations/fade.lua

timeout(delay, function()

end)

to

table.insert(animations.timer, timeout(delay, function ()

end))

And add this:

return function (widget, delay)
  if animations.busy then
    for _, v in ipairs(animations.timers) do
      v:stop()
    end
  end

  ...
end

animations/init.lua should look like this:

local M = {}

M.busy   = false
M.timers = {}

return M

and animations/fade.lua should look like this:

local gears = require("gears")
local animations = require("animations")

local timeout = gears.timer.start_new

local function hide(w)
  w.visible = false
  w.opacity = 0
end

local function show(w)
  w.visible = true
  w.opacity = 1
end

return function (widget, delay)
  if animations.busy then
    for _, v in ipairs(animations.timers) do
      v:stop()
    end
  end

  show(widget)

  table.insert(animations.timers, timeout(delay, function ()
    animations.busy = true
    local max = 50

    for f = 1, max do
      table.insert(animations.timers, timeout((f / 200), function ()
        f = f + 1
        widget.opacity = widget.opacity - 0.02

        -- This runs when the animation is finished
        if f == max then
          animations.busy = false
          hide(widget)
        end
      end))
    end
  end))
end

You can now see a much better behavior:

perfect

Now we are done with animating! Next Step: Adding Text

Adding Text

This is a fairly easy step, we will be adding the ability to change the text in the widget. Let's start by opening effects/tagswitch.lua and adding this function:

effects/tagswitch.lua

M.changeText = function (text)
  M:get_children_by_id("text")[1]:set_markup("<b>" .. text .. "</b>")
end

What are we doing is that we are calling the function get_children_by_id on the widget (M), the function recieves a string, which is the id for the child. Since we are using nested widgets, the declarative style, we can access child widgets (children) by using get_children_by_id. Note that the id of the child widget is determined by the id property (see effects/tagswitch.lua).

For some reason, we have to select the first element of the returned list (as seen in the docs), and then we run the function set_markup. Note that we used markup instead of text to make the text bold by using the <b> HTML tags. So we are passing the concatenated text with the bold (<b>) tags to set_markup.

Let's test it by going to the rc.lua and modifying the keybinding:

awful.keyboard.append_global_keybindings({
	awful.key({ "Mod4" }, "o", function ()
		tagswitch.changeText("this")
    ...
	end)
})

We changed the text to "this", which again, doesn't mean anything, here's how the widget should look like:

this

Now we are done with adding the text! Next Step: Cleaning Up

Cleaning Up

This is an easy, but very important step. Let's start by opening effects/tagswitch.lua. Add this function:

effects/tagswitch.lua

local fade  = require("animations.fade")

...

M.animate = function (text)
  M.changeText(text)
  fade(M, 0.25)
end

But now arises a problem, we of course need to require the effects.tagswitch module with require, this will run the code in effects/tagswitch.lua, which creates a widget.

Now if we have multiple files (say you split the keybindings into two files), you may have to require it two times, once on each file, that creates two widgets at the same time, and we don't want this.

We can fix this by creating a new file in effects/ directory. Name it init.lua. And write:

effects/init.lua

local M = {}

local effects = {
  { require("effects.tagswitch"), "tagswitch" }
}

M.request_effect = function (name)
  for _, e in ipairs(effects) do
    if name == e[2] then
      return e[1]
    end
  end

  return false
end

return M

This is just a function that we call when we want to access a certain effect. If you have multiple effects, you can add them here, and the function will take care of all of that.

And to make sure the effects run only once, you will need to write this in your rc.lua (write this before your keybindings code, not at the end! Order is very important!):

rc.lua

Effects = require("effects")

Effects is a global variable, Now everytime we need to access an effect, we will write:

Effects.request_effect("effect")

Now let's update our rc.lua:

rc.lua

local tagswitch = Effects.request_effect("tagswitch")

awful.keyboard.append_global_keybindings({
  awful.key({ "Mod4" }, "o", function ()
    if tagswitch then
      tagswitch.animate("yes")
    end
  end)
})

Make sure to 'null-check' when using the effect returned by Effects.request_widget by wrapping the code with if statements, as we did in the example, to prevent errors if by somehow the effect cannot be found (if Effects.request_widget returned false):

if tagswitch then
  -- your code <---
end

We are done with cleaning up! Next Step: Adding To Keybindings

Adding To Keybindings

Let's start off by deleting the keybinding code in rc.lua:

Note: Leave the statement: Effects = require("effects") and don't delete it.

rc.lua

-- local tagswitch = Effects.request_effect("tagswitch")
--
-- awful.keyboard.append_global_keybindings({
--   awful.key({ "Mod4" }, "o", function ()
--     if tagswitch then
--       tagswitch.animate("yes")
--     end
--   end)
-- })

And add this to your keybindings section:

local tagswitch = Effects.request_effect("tagswitch")

...

awful.key {
  ...
  on_press = function (index)
    ...

    if tagswitch then
      local t = awful.screen.focused().selected_tag
      tagswitch.animate(tostring(t.index))
    end
  end,
},

Add this for all tag related keybindings. Unfortunately, I can't show you exact code examples as we have different file structures and code.

Notice that we used t.index instead of t.name, I prefer t.index because some people use icons like Font Awesome and Nerd Fonts. And some people may even have all their tags named 'o' or 'O', so it is convenient to use tag index instead.

You can change the dimensions of the widget as it can be too wide for the numbers:

effects/taglist.lua

local M = wibox {
  ...
  height  = 90,
  width   = 100,
}

And that concludes our tutorial, hope you learned something!

Bonus: Encapsulating And Cleaning Up

If you are still here, let me show you one more trick. Let's make the fade function more pure and encapsulated.

animations/fade.lua

return function (widget, speed, delay)
  ...

  table.insert(animations.timers, timeout((f / speed), function ()
    ...
  end))
end

We just added a speed argument.

effects/taglist.lua

local beautiful = require("beautiful")

-- DPI
local xresources = require("beautiful.xresources")
local dpi = xresources.apply_dpi

...

local M = wibox {
  ...
  bg      = beautiful.bg_tagswitch or beautiful.bg_normal,
  fg      = beautiful.fg_tagswitch or beautiful.fg_normal,
  height  = beautiful.tagswitch_height or dpi(90),
  width   = beautiful.tagswitch_width or dpi(180),
}

M:setup {
  {
    id     = "text",
    font   = beautiful.tagswitch_font or beautiful.font,
    ...
  },
  ...
}

M.animate = function (text)
  M.changeText(text)
  fade(M, beautiful.tagswitch_speed or 200, beautiful.tagswitch_delay or 0.25)
end

We just added theming options to the widget. I can add a section for that in my theme.lua:

theme.lua

-- ## EFFECTS ## --
-- Tagswitch
theme.bg_tagswitch    = "#000"
theme.fg_tagswitch    = "#fff"
theme.tagswitch_width = dpi(100)
theme.tagswitch_font  = "JetBrainsMono Nerd Font 37"
theme.tagswitch_speed = 200
theme.tagswitch_delay = 0.25

Isn't that cool?!

Enjoy Your New Effect!

tagswitch

Code Links

Final effects/init.lua: link

Final effects/tagswitch.lua: link

Final animations/init.lua: link

Final animations/fade.lua: link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment