Download
109 lines 3.7 KiBJavaScriptModified 2026-10-14 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
// herbstluftwm workspace (tag) switcher. Renders one button per tag from the
// `divebar://hlwm` event stream; clicking a tag switches to it.

export default {
  name: "workspaces",
  css: `
    .widget-workspaces {
      /* How far each tag button sits inside the pill. The hit-target
         variables (see style.css) extend each tag's clickable area over
         this inset and halfway across the 2px gap between tags. */
      --tag-inset: 3px;
      --hit-inset-x: var(--tag-inset);
      --hit-inset-y: var(--tag-inset);
      --hit-gap-x: 1px;
      display: flex;
      align-items: center;
      gap: 2px;
      height: var(--chip-height);
      padding: 0 var(--tag-inset);
      border-radius: var(--chip-radius);
      background: rgba(0, 0, 0, 0.26);
      box-shadow:
        inset 0 1px 2px rgba(0, 0, 0, 0.35),
        inset 0 -1px 0 rgba(255, 255, 255, 0.04);
    }
    .widget-workspaces .tag {
      min-width: 1.3em;
      height: calc(var(--chip-height) - 2 * var(--tag-inset));
      padding: 0 0.25em;
      border: none;
      border-radius: calc((var(--chip-height) - 2 * var(--tag-inset)) / 2);
      background: transparent;
      color: var(--bar-fg-dim);
      font: inherit;
      font-variant-numeric: tabular-nums;
      cursor: pointer;
      transition: background 120ms ease, color 120ms ease;
    }
    .widget-workspaces .tag:hover { background: var(--chip-bg-hover); }
    .widget-workspaces .tag.empty { color: rgba(255, 255, 255, 0.25); }
    .widget-workspaces .tag.occupied { color: var(--bar-fg); }
    .widget-workspaces .tag.visible,
    .widget-workspaces .tag.focused-elsewhere {
      color: var(--bar-fg);
      background: var(--chip-bg-hover);
    }
    .widget-workspaces .tag.focused {
      color: #10121a;
      background: var(--accent);
      font-weight: 700;
    }
    .widget-workspaces .tag.urgent {
      color: #10121a;
      background: var(--critical);
      font-weight: 700;
      animation: divebar-urgent-pulse 1.6s ease-in-out infinite;
    }
    @keyframes divebar-urgent-pulse {
      0%, 100% { box-shadow: 0 0 0 0 rgba(247, 118, 142, 0); filter: brightness(1); }
      50%      { box-shadow: 0 0 8px 1px rgba(247, 118, 142, 0.55); filter: brightness(1.18); }
    }
  `,
  settings: [
    {
      key: "labels",
      label: "Tag labels",
      type: "select",
      choices: ["numbers", "names"],
      default: "numbers",
    },
  ],
  create({ options, onEvent, invoke }) {
    const el = document.createElement("div");
    el.className = "widget-workspaces";

    const unsubscribe = onEvent("divebar://hlwm", ({ tags }) => {
      if (!tags) return;
      // Rebuild only when the tag list changes; otherwise just update classes.
      const buttons = el.querySelectorAll(".tag");
      const sameTags =
        buttons.length === tags.length &&
        tags.every((tag, i) => buttons[i].dataset.tag === tag.name);

      if (!sameTags) {
        el.replaceChildren(
          ...tags.map((tag, i) => {
            const btn = document.createElement("button");
            btn.className = "tag hit-target";
            btn.dataset.tag = tag.name;
            // Position number by default; the name is always used for
            // switching and shown as a tooltip.
            btn.textContent =
              options.labels === "names" ? tag.name : String(i + 1);
            btn.title = tag.name;
            btn.addEventListener("click", () =>
              invoke("hlwm_use_tag", { tag: tag.name }),
            );
            return btn;
          }),
        );
      }
      el.querySelectorAll(".tag").forEach((btn, i) => {
        btn.className = `tag hit-target ${tags[i].state}`;
      });
    });

    return { el, destroy: unsubscribe };
  },
};