feat(workspace): map client classes to application icons (#368)
* feat(workspace): map client classes to application icons * refactor: extract app icon detection * feat: hide duplicate icons per workspace * feat: use dedicated icons for empty workspace and fallback * provide default icons * feat: title or class matcher can no provided as regex * style: change option description * style: use more descriptive param name * style: fix comment * fix(lint): missing return type * refactor: move type definitions into separate file * feat: defined default app icon set * docs: change option subtitles * style: change icons * fix: add missing default variant --------- Co-authored-by: Jas Singh <jaskiratpal.singh@outlook.com>
This commit is contained in:
committed by
GitHub
parent
25753e5f17
commit
4e2a774c7e
29
lib/constants/workspaces.ts
Normal file
29
lib/constants/workspaces.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export const defaultApplicationIcons = {
|
||||||
|
'[dD]iscord': '',
|
||||||
|
'^thunderbird': '',
|
||||||
|
'class:wezterm$': '',
|
||||||
|
'draw.io': '',
|
||||||
|
'firefox-developer-edition': '',
|
||||||
|
'google-chrome': '',
|
||||||
|
'title:.*youtube': '',
|
||||||
|
Spotify: '',
|
||||||
|
chromium: '',
|
||||||
|
code: '',
|
||||||
|
dbeaver: '',
|
||||||
|
edge: '',
|
||||||
|
evince: '',
|
||||||
|
firefox: '',
|
||||||
|
foot: '',
|
||||||
|
keepassxc: '',
|
||||||
|
keymapp: '',
|
||||||
|
kitty: '',
|
||||||
|
obsidian: '',
|
||||||
|
password$: '',
|
||||||
|
qBittorrent$: '',
|
||||||
|
rofi: '',
|
||||||
|
slack: '',
|
||||||
|
spotube: '',
|
||||||
|
steam: '',
|
||||||
|
telegram: '',
|
||||||
|
vlc: '',
|
||||||
|
};
|
||||||
11
lib/types/workspace.d.ts
vendored
11
lib/types/workspace.d.ts
vendored
@@ -11,10 +11,21 @@ export type MonitorMap = {
|
|||||||
[key: number]: string;
|
[key: number]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApplicationIcons = {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceIcons = {
|
export type WorkspaceIcons = {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppIconOptions = {
|
||||||
|
iconMap: ApplicationIcons;
|
||||||
|
defaultIcon: string;
|
||||||
|
emptyIcon: string;
|
||||||
|
};
|
||||||
|
export type ClientAttributes = [className: string, title: string];
|
||||||
|
|
||||||
export type WorkspaceIconsColored = {
|
export type WorkspaceIconsColored = {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
color: string;
|
color: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { WorkspaceIconMap } from 'lib/types/workspace';
|
import { defaultApplicationIcons } from 'lib/constants/workspaces';
|
||||||
|
import type { ClientAttributes, AppIconOptions, WorkspaceIconMap } from 'lib/types/workspace';
|
||||||
import { isValidGjsColor } from 'lib/utils';
|
import { isValidGjsColor } from 'lib/utils';
|
||||||
import options from 'options';
|
import options from 'options';
|
||||||
import { Monitor } from 'types/service/hyprland';
|
import { Monitor } from 'types/service/hyprland';
|
||||||
@@ -68,6 +69,65 @@ export const getWsColor = (
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAppIcon = (
|
||||||
|
workspaceIndex: number,
|
||||||
|
removeDuplicateIcons: boolean,
|
||||||
|
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
|
||||||
|
): string => {
|
||||||
|
// append the default icons so user defined icons take precedence
|
||||||
|
const iconMap = { ...userDefinedIconMap, ...defaultApplicationIcons };
|
||||||
|
|
||||||
|
// detect the clients attributes on the current workspace
|
||||||
|
const clients: ReadonlyArray<ClientAttributes> = hyprland.clients
|
||||||
|
.filter((c) => c.workspace.id === workspaceIndex)
|
||||||
|
.map((c) => [c.class, c.title]);
|
||||||
|
|
||||||
|
if (!clients.length) {
|
||||||
|
return emptyIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// map the client attributes to icons
|
||||||
|
let icons = clients
|
||||||
|
.map(([clientClass, clientTitle]) => {
|
||||||
|
const maybeIcon = Object.entries(iconMap).find(([matcher]) => {
|
||||||
|
// non-valid Regex construction could result in a syntax error
|
||||||
|
try {
|
||||||
|
if (matcher.startsWith('class:')) {
|
||||||
|
const re = matcher.substring(6);
|
||||||
|
return new RegExp(re, 'i').test(clientClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.startsWith('title:')) {
|
||||||
|
const re = matcher.substring(6);
|
||||||
|
return new RegExp(re, 'i').test(clientTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(matcher, 'i').test(clientClass);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!maybeIcon) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return maybeIcon.at(1);
|
||||||
|
})
|
||||||
|
.filter((x) => x);
|
||||||
|
|
||||||
|
// remove duplicate icons
|
||||||
|
if (removeDuplicateIcons) {
|
||||||
|
icons = [...new Set(icons)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (icons.length) {
|
||||||
|
return icons.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultIcon;
|
||||||
|
};
|
||||||
|
|
||||||
export const renderClassnames = (
|
export const renderClassnames = (
|
||||||
showIcons: boolean,
|
showIcons: boolean,
|
||||||
showNumbered: boolean,
|
showNumbered: boolean,
|
||||||
@@ -104,6 +164,8 @@ export const renderLabel = (
|
|||||||
available: string,
|
available: string,
|
||||||
active: string,
|
active: string,
|
||||||
occupied: string,
|
occupied: string,
|
||||||
|
showAppIcons: boolean,
|
||||||
|
appIcons: string,
|
||||||
workspaceMask: boolean,
|
workspaceMask: boolean,
|
||||||
showWsIcons: boolean,
|
showWsIcons: boolean,
|
||||||
wsIconMap: WorkspaceIconMap,
|
wsIconMap: WorkspaceIconMap,
|
||||||
@@ -112,6 +174,10 @@ export const renderLabel = (
|
|||||||
monitor: number,
|
monitor: number,
|
||||||
monitors: Monitor[],
|
monitors: Monitor[],
|
||||||
): string => {
|
): string => {
|
||||||
|
if (showAppIcons) {
|
||||||
|
return appIcons;
|
||||||
|
}
|
||||||
|
|
||||||
if (showIcons) {
|
if (showIcons) {
|
||||||
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
|
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
|
||||||
return active;
|
return active;
|
||||||
@@ -123,8 +189,10 @@ export const renderLabel = (
|
|||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showWsIcons) {
|
if (showWsIcons) {
|
||||||
return getWsIcon(wsIconMap, i);
|
return getWsIcon(wsIconMap, i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMask ? `${index + 1}` : `${i}`;
|
return workspaceMask ? `${index + 1}` : `${i}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import options from 'options';
|
|||||||
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
|
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
|
||||||
import { range } from 'lib/utils';
|
import { range } from 'lib/utils';
|
||||||
import { BoxWidget } from 'lib/types/widget';
|
import { BoxWidget } from 'lib/types/widget';
|
||||||
import { getWsColor, renderClassnames, renderLabel } from '../utils';
|
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from '../utils';
|
||||||
import { WorkspaceIconMap } from 'lib/types/workspace';
|
import { WorkspaceIconMap } from 'lib/types/workspace';
|
||||||
import { Monitor } from 'types/service/hyprland';
|
import { Monitor } from 'types/service/hyprland';
|
||||||
|
|
||||||
@@ -98,6 +98,11 @@ export const defaultWses = (monitor: number): BoxWidget => {
|
|||||||
options.bar.workspaces.icons.occupied.bind('value'),
|
options.bar.workspaces.icons.occupied.bind('value'),
|
||||||
options.bar.workspaces.workspaceIconMap.bind('value'),
|
options.bar.workspaces.workspaceIconMap.bind('value'),
|
||||||
options.bar.workspaces.showWsIcons.bind('value'),
|
options.bar.workspaces.showWsIcons.bind('value'),
|
||||||
|
options.bar.workspaces.showApplicationIcons.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconOncePerWorkspace.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconMap.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconEmptyWorkspace.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconFallback.bind('value'),
|
||||||
workspaceMask.bind('value'),
|
workspaceMask.bind('value'),
|
||||||
hyprland.bind('monitors'),
|
hyprland.bind('monitors'),
|
||||||
],
|
],
|
||||||
@@ -108,14 +113,29 @@ export const defaultWses = (monitor: number): BoxWidget => {
|
|||||||
occupied: string,
|
occupied: string,
|
||||||
wsIconMap: WorkspaceIconMap,
|
wsIconMap: WorkspaceIconMap,
|
||||||
showWsIcons: boolean,
|
showWsIcons: boolean,
|
||||||
|
showAppIcons,
|
||||||
|
applicationIconOncePerWorkspace,
|
||||||
|
applicationIconMap,
|
||||||
|
applicationIconEmptyWorkspace,
|
||||||
|
applicationIconFallback,
|
||||||
workspaceMask: boolean,
|
workspaceMask: boolean,
|
||||||
monitors: Monitor[],
|
monitors: Monitor[],
|
||||||
) => {
|
) => {
|
||||||
|
const appIcons = showAppIcons
|
||||||
|
? getAppIcon(i, applicationIconOncePerWorkspace, {
|
||||||
|
iconMap: applicationIconMap,
|
||||||
|
defaultIcon: applicationIconFallback,
|
||||||
|
emptyIcon: applicationIconEmptyWorkspace,
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
return renderLabel(
|
return renderLabel(
|
||||||
showIcons,
|
showIcons,
|
||||||
available,
|
available,
|
||||||
active,
|
active,
|
||||||
occupied,
|
occupied,
|
||||||
|
showAppIcons,
|
||||||
|
appIcons,
|
||||||
workspaceMask,
|
workspaceMask,
|
||||||
showWsIcons,
|
showWsIcons,
|
||||||
wsIconMap,
|
wsIconMap,
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ const hyprland = await Service.import('hyprland');
|
|||||||
import options from 'options';
|
import options from 'options';
|
||||||
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
|
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
|
||||||
import { Monitor, Workspace } from 'types/service/hyprland';
|
import { Monitor, Workspace } from 'types/service/hyprland';
|
||||||
import { getWsColor, renderClassnames, renderLabel } from '../utils';
|
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from '../utils';
|
||||||
import { range } from 'lib/utils';
|
import { range } from 'lib/utils';
|
||||||
import { BoxWidget } from 'lib/types/widget';
|
import { BoxWidget } from 'lib/types/widget';
|
||||||
import { WorkspaceIconMap } from 'lib/types/workspace';
|
import { WorkspaceIconMap } from 'lib/types/workspace';
|
||||||
|
|
||||||
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored, showAllActive } = options.bar.workspaces;
|
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored, showAllActive } = options.bar.workspaces;
|
||||||
|
|
||||||
export const occupiedWses = (monitor: number): BoxWidget => {
|
export const occupiedWses = (monitor: number): BoxWidget => {
|
||||||
@@ -26,6 +25,11 @@ export const occupiedWses = (monitor: number): BoxWidget => {
|
|||||||
spacing.bind('value'),
|
spacing.bind('value'),
|
||||||
options.bar.workspaces.workspaceIconMap.bind('value'),
|
options.bar.workspaces.workspaceIconMap.bind('value'),
|
||||||
options.bar.workspaces.showWsIcons.bind('value'),
|
options.bar.workspaces.showWsIcons.bind('value'),
|
||||||
|
options.bar.workspaces.showApplicationIcons.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconOncePerWorkspace.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconMap.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconEmptyWorkspace.bind('value'),
|
||||||
|
options.bar.workspaces.applicationIconFallback.bind('value'),
|
||||||
options.theme.matugen.bind('value'),
|
options.theme.matugen.bind('value'),
|
||||||
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
|
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
|
||||||
hyprland.bind('monitors'),
|
hyprland.bind('monitors'),
|
||||||
@@ -46,6 +50,11 @@ export const occupiedWses = (monitor: number): BoxWidget => {
|
|||||||
spacing: number,
|
spacing: number,
|
||||||
wsIconMap: WorkspaceIconMap,
|
wsIconMap: WorkspaceIconMap,
|
||||||
showWsIcons: boolean,
|
showWsIcons: boolean,
|
||||||
|
showAppIcons,
|
||||||
|
applicationIconOncePerWorkspace,
|
||||||
|
applicationIconMap,
|
||||||
|
applicationIconEmptyWorkspace,
|
||||||
|
applicationIconFallback,
|
||||||
matugen: boolean,
|
matugen: boolean,
|
||||||
smartHighlight: boolean,
|
smartHighlight: boolean,
|
||||||
monitors: Monitor[],
|
monitors: Monitor[],
|
||||||
@@ -98,6 +107,15 @@ export const occupiedWses = (monitor: number): BoxWidget => {
|
|||||||
if (isWorkspaceIgnored(ignored, i)) {
|
if (isWorkspaceIgnored(ignored, i)) {
|
||||||
return Widget.Box();
|
return Widget.Box();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appIcons = showAppIcons
|
||||||
|
? getAppIcon(i, applicationIconOncePerWorkspace, {
|
||||||
|
iconMap: applicationIconMap,
|
||||||
|
defaultIcon: applicationIconFallback,
|
||||||
|
emptyIcon: applicationIconEmptyWorkspace,
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
return Widget.Button({
|
return Widget.Button({
|
||||||
class_name: 'workspace-button',
|
class_name: 'workspace-button',
|
||||||
on_primary_click: () => {
|
on_primary_click: () => {
|
||||||
@@ -124,6 +142,8 @@ export const occupiedWses = (monitor: number): BoxWidget => {
|
|||||||
available,
|
available,
|
||||||
active,
|
active,
|
||||||
occupied,
|
occupied,
|
||||||
|
showAppIcons,
|
||||||
|
appIcons,
|
||||||
workspaceMask,
|
workspaceMask,
|
||||||
showWsIcons,
|
showWsIcons,
|
||||||
wsIconMap,
|
wsIconMap,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { MatugenScheme, MatugenTheme, MatugenVariations } from 'lib/types/options';
|
import { MatugenScheme, MatugenTheme, MatugenVariations } from 'lib/types/options';
|
||||||
import { UnitType } from 'lib/types/weather';
|
import { UnitType } from 'lib/types/weather';
|
||||||
import { Transition } from 'lib/types/widget';
|
import { Transition } from 'lib/types/widget';
|
||||||
import { WorkspaceIcons, WorkspaceIconsColored } from 'lib/types/workspace';
|
import { ApplicationIcons, WorkspaceIcons, WorkspaceIconsColored } from 'lib/types/workspace';
|
||||||
|
|
||||||
// WARN: CHANGING THESE VALUES WILL PREVENT MATUGEN COLOR GENERATION FOR THE CHANGED VALUE
|
// WARN: CHANGING THESE VALUES WILL PREVENT MATUGEN COLOR GENERATION FOR THE CHANGED VALUE
|
||||||
export const colors = {
|
export const colors = {
|
||||||
@@ -868,6 +868,11 @@ const options = mkOptions(OPTIONS, {
|
|||||||
ignored: opt(''),
|
ignored: opt(''),
|
||||||
show_numbered: opt(false),
|
show_numbered: opt(false),
|
||||||
showWsIcons: opt(false),
|
showWsIcons: opt(false),
|
||||||
|
showApplicationIcons: opt(false),
|
||||||
|
applicationIconOncePerWorkspace: opt(true),
|
||||||
|
applicationIconMap: opt<ApplicationIcons>({}),
|
||||||
|
applicationIconFallback: opt(''),
|
||||||
|
applicationIconEmptyWorkspace: opt(''),
|
||||||
numbered_active_indicator: opt<ActiveWsIndicator>('underline'),
|
numbered_active_indicator: opt<ActiveWsIndicator>('underline'),
|
||||||
icons: {
|
icons: {
|
||||||
available: opt(''),
|
available: opt(''),
|
||||||
|
|||||||
@@ -261,6 +261,34 @@ export const BarSettings = (): Scrollable<Gtk.Widget, Gtk.Widget> => {
|
|||||||
title: 'Map Workspaces to Icons',
|
title: 'Map Workspaces to Icons',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
}),
|
}),
|
||||||
|
Option({
|
||||||
|
opt: options.bar.workspaces.showApplicationIcons,
|
||||||
|
title: 'Map Workspaces to Application Icons',
|
||||||
|
subtitle: "Requires 'Map Workspace to Icons' to be enabled",
|
||||||
|
type: 'boolean',
|
||||||
|
}),
|
||||||
|
Option({
|
||||||
|
opt: options.bar.workspaces.applicationIconOncePerWorkspace,
|
||||||
|
title: 'Hide Duplicate App Icons',
|
||||||
|
type: 'boolean',
|
||||||
|
}),
|
||||||
|
Option({
|
||||||
|
opt: options.bar.workspaces.applicationIconMap,
|
||||||
|
title: 'App Icon Mappings',
|
||||||
|
subtitle: "Use the class/title output of 'hyprctl clients' to match against",
|
||||||
|
type: 'object',
|
||||||
|
}),
|
||||||
|
Option({
|
||||||
|
opt: options.bar.workspaces.applicationIconFallback,
|
||||||
|
title: 'Fallback App Icon',
|
||||||
|
subtitle: 'Fallback icon to display if no specific icon is defined for the application',
|
||||||
|
type: 'string',
|
||||||
|
}),
|
||||||
|
Option({
|
||||||
|
opt: options.bar.workspaces.applicationIconEmptyWorkspace,
|
||||||
|
title: 'App Icon for empty workspace',
|
||||||
|
type: 'string',
|
||||||
|
}),
|
||||||
Option({
|
Option({
|
||||||
opt: options.bar.workspaces.workspaceIconMap,
|
opt: options.bar.workspaces.workspaceIconMap,
|
||||||
title: 'Workspace Icon Mappings',
|
title: 'Workspace Icon Mappings',
|
||||||
|
|||||||
Reference in New Issue
Block a user