Added ags to hyprland

This commit is contained in:
2024-08-15 09:57:47 -03:00
parent 5fc32bd6b9
commit 791beef7c7
352 changed files with 22897 additions and 7 deletions

View File

@@ -0,0 +1,27 @@
const { Gdk } = imports.gi;
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { execAsync, exec } = Utils;
export let monitors;
// Mixes with Gdk monitor size cuz it reports monitor size scaled
async function updateStuff() {
monitors = JSON.parse(exec('hyprctl monitors -j'))
const display = Gdk.Display.get_default();
monitors.forEach((monitor, i) => {
const gdkMonitor = display.get_monitor(i);
monitor.realWidth = monitor.width;
monitor.realHeight = monitor.height;
if (userOptions.monitors.scaleMethod.toLowerCase == "gdk") {
monitor.width = gdkMonitor.get_geometry().width;
monitor.height = gdkMonitor.get_geometry().height;
}
else { // == "division"
monitor.width = Math.ceil(monitor.realWidth / monitor.scale);
monitor.height = Math.ceil(monitor.realHeight / monitor.scale);
}
});
}
updateStuff().catch(print);

View File

@@ -0,0 +1,14 @@
export const quotes = [
{
quote: 'Nvidia, fuck you',
author: 'Linus Torvalds',
},
{
quote: 'reproducible system? cock and vagina?',
author: 'vaxry',
},
{
quote: "haha pointers hee hee i love pointe-\\\nProcess Vaxry exited with signal SIGSEGV",
author: 'vaxry',
}
];

View File

@@ -0,0 +1,94 @@
export const WWO_CODE = {
"113": "Sunny",
"116": "PartlyCloudy",
"119": "Cloudy",
"122": "VeryCloudy",
"143": "Fog",
"176": "LightShowers",
"179": "LightSleetShowers",
"182": "LightSleet",
"185": "LightSleet",
"200": "ThunderyShowers",
"227": "LightSnow",
"230": "HeavySnow",
"248": "Fog",
"260": "Fog",
"263": "LightShowers",
"266": "LightRain",
"281": "LightSleet",
"284": "LightSleet",
"293": "LightRain",
"296": "LightRain",
"299": "HeavyShowers",
"302": "HeavyRain",
"305": "HeavyShowers",
"308": "HeavyRain",
"311": "LightSleet",
"314": "LightSleet",
"317": "LightSleet",
"320": "LightSnow",
"323": "LightSnowShowers",
"326": "LightSnowShowers",
"329": "HeavySnow",
"332": "HeavySnow",
"335": "HeavySnowShowers",
"338": "HeavySnow",
"350": "LightSleet",
"353": "LightShowers",
"356": "HeavyShowers",
"359": "HeavyRain",
"362": "LightSleetShowers",
"365": "LightSleetShowers",
"368": "LightSnowShowers",
"371": "HeavySnowShowers",
"374": "LightSleetShowers",
"377": "LightSleet",
"386": "ThunderyShowers",
"389": "ThunderyHeavyRain",
"392": "ThunderySnowShowers",
"395": "HeavySnowShowers",
}
export const WEATHER_SYMBOL = {
"Unknown": "air",
"Cloudy": "cloud",
"Fog": "foggy",
"HeavyRain": "rainy",
"HeavyShowers": "rainy",
"HeavySnow": "snowing",
"HeavySnowShowers": "snowing",
"LightRain": "rainy",
"LightShowers": "rainy",
"LightSleet": "rainy",
"LightSleetShowers": "rainy",
"LightSnow": "cloudy_snowing",
"LightSnowShowers": "cloudy_snowing",
"PartlyCloudy": "partly_cloudy_day",
"Sunny": "clear_day",
"ThunderyHeavyRain": "thunderstorm",
"ThunderyShowers": "thunderstorm",
"ThunderySnowShowers": "thunderstorm",
"VeryCloudy": "cloud",
}
export const NIGHT_WEATHER_SYMBOL = {
"Unknown": "air",
"Cloudy": "cloud",
"Fog": "foggy",
"HeavyRain": "rainy",
"HeavyShowers": "rainy",
"HeavySnow": "snowing",
"HeavySnowShowers": "snowing",
"LightRain": "rainy",
"LightShowers": "rainy",
"LightSleet": "rainy",
"LightSleetShowers": "rainy",
"LightSnow": "cloudy_snowing",
"LightSnowShowers": "cloudy_snowing",
"PartlyCloudy": "partly_cloudy_night",
"Sunny": "clear_night",
"ThunderyHeavyRain": "thunderstorm",
"ThunderyShowers": "thunderstorm",
"ThunderySnowShowers": "thunderstorm",
"VeryCloudy": "cloud",
}

View File

@@ -0,0 +1,106 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
// -- Styling --
// min-height for diameter
// min-width for trough stroke
// padding for space between trough and progress
// margin for space between widget and parent
// background-color for trough color
// color for progress color
// -- Usage --
// font size for progress value (0-100px) (hacky i know, but i want animations)
export const AnimatedCircProg = ({
initFrom = 0,
initTo = 0,
initAnimTime = 2900,
initAnimPoints = 1,
extraSetup = () => { },
...rest
}) => Widget.DrawingArea({
...rest,
css: `${initFrom != initTo ? 'font-size: ' + initFrom + 'px; transition: ' + initAnimTime + 'ms linear;' : ''}`,
setup: (area) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
const progressValue = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100.0;
const bg_stroke = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const fg_stroke = bg_stroke - padding;
const radius = Math.min(width, height) / 2.0 - Math.max(bg_stroke, fg_stroke) / 2.0;
const center_x = width / 2.0 + marginLeft;
const center_y = height / 2.0 + marginTop;
const start_angle = -Math.PI / 2.0;
const end_angle = start_angle + (2 * Math.PI * progressValue);
const start_x = center_x + Math.cos(start_angle) * radius;
const start_y = center_y + Math.sin(start_angle) * radius;
const end_x = center_x + Math.cos(end_angle) * radius;
const end_y = center_y + Math.sin(end_angle) * radius;
// Draw background
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.arc(center_x, center_y, radius, 0, 2 * Math.PI);
cr.setLineWidth(bg_stroke);
cr.stroke();
if (progressValue == 0) return;
// Draw progress
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
cr.arc(center_x, center_y, radius, start_angle, end_angle);
cr.setLineWidth(fg_stroke);
cr.stroke();
// Draw rounded ends for progress arcs
cr.setLineWidth(0);
cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
}));
// Init animation
if (initFrom != initTo) {
area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`;
Utils.timeout(20, () => {
area.css = `font-size: ${initTo}px;`;
}, area)
const transitionDistance = initTo - initFrom;
const oneStep = initAnimTime / initAnimPoints;
area.css = `
font-size: ${initFrom}px;
transition: ${oneStep}ms linear;
`;
for (let i = 0; i < initAnimPoints; i++) {
Utils.timeout(Math.max(10, i * oneStep), () => {
if(!area) return;
area.css = `${initFrom != initTo ? 'font-size: ' + (initFrom + (transitionDistance / initAnimPoints * (i + 1))) + 'px;' : ''}`;
});
}
}
else area.css = 'font-size: 0px;';
extraSetup(area);
},
})

View File

@@ -0,0 +1,71 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
// min-height/min-width for height/width
// background-color/color for background/indicator color
// padding for pad of indicator
// font-size for selected index (0-based)
export const NavigationIndicator = ({count, vertical, ...props}) => Widget.DrawingArea({
...props,
setup: (area) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
area.set_size_request(width, height);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
// console.log('allocated width/height:', area.get_allocated_width(), '/', area.get_allocated_height())
area.set_size_request(width, height);
const paddingLeft = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const paddingRight = styleContext.get_padding(Gtk.StateFlags.NORMAL).right;
const paddingTop = styleContext.get_padding(Gtk.StateFlags.NORMAL).top;
const paddingBottom = styleContext.get_padding(Gtk.StateFlags.NORMAL).bottom;
const selectedCell = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
let cellWidth = width;
let cellHeight = height;
if (vertical) cellHeight /= count;
else cellWidth /= count;
const indicatorWidth = cellWidth - paddingLeft - paddingRight;
const indicatorHeight = cellHeight - paddingTop - paddingBottom;
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setLineWidth(2);
// Background
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.rectangle(0, 0, width, height);
cr.fill();
// The indicator line
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
if (vertical) {
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.stroke();
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth / 2, Math.PI, 2 * Math.PI);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorHeight - indicatorWidth / 2, indicatorWidth / 2, 0, Math.PI);
cr.fill();
}
else {
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.stroke();
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorWidth - indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
}))
},
})

View File

@@ -0,0 +1,50 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const Lang = imports.lang;
export const RoundedCorner = (place, props) => Widget.DrawingArea({
...props,
hpack: place.includes('left') ? 'start' : 'end',
vpack: place.includes('top') ? 'start' : 'end',
setup: (widget) => Utils.timeout(1, () => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r);
widget.connect('draw', Lang.bind(widget, (widget, cr) => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
// const borderColor = widget.get_style_context().get_property('color', Gtk.StateFlags.NORMAL);
// const borderWidth = widget.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
widget.set_size_request(r, r);
switch (place) {
case 'topleft':
cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
cr.lineTo(0, 0);
break;
case 'topright':
cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
cr.lineTo(r, 0);
break;
case 'bottomleft':
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
cr.lineTo(0, r);
break;
case 'bottomright':
cr.arc(0, 0, r, 0, Math.PI / 2);
cr.lineTo(r, r);
break;
}
cr.closePath();
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
cr.fill();
// cr.setLineWidth(borderWidth);
// cr.setSourceRGBA(borderColor.red, borderColor.green, borderColor.blue, borderColor.alpha);
// cr.stroke();
}));
}),
});

View File

@@ -0,0 +1,49 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const Lang = imports.lang;
export const AnimatedSlider = ({
className,
value,
...rest
}) => {
return Widget.DrawingArea({
className: `${className}`,
setup: (self) => {
self.connect('draw', Lang.bind(self, (self, cr) => {
const styleContext = self.get_style_context();
const allocatedWidth = self.get_allocated_width();
const allocatedHeight = self.get_allocated_height();
console.log(allocatedHeight, allocatedWidth)
const minWidth = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const minHeight = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const radius = styleContext.get_property('border-radius', Gtk.StateFlags.NORMAL);
const bg = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
const value = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100;
self.set_size_request(-1, minHeight);
const width = allocatedHeight;
const height = minHeight;
cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
cr.arc(width - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
cr.arc(width - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
cr.closePath();
cr.fill();
// const valueWidth = width * value;
// cr.arc(radius, radius, radius, -1 * Math.PI, -0.5 * Math.PI); // Top-left
// cr.arc(valueWidth - radius, radius, radius, -0.5 * Math.PI, 0); // Top-right
// cr.arc(valueWidth - radius, height - radius, radius, 0, 0.5 * Math.PI); // Bottom-left
// cr.arc(radius, height - radius, radius, 0.5 * Math.PI, 1 * Math.PI); // Bottom-right
// cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
// cr.closePath();
// cr.fill();
}));
},
...rest,
})
}

View File

@@ -0,0 +1,23 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { monitors } from '../.commondata/hyprlanddata.js';
const { Box, EventBox } = Widget;
export const clickCloseRegion = ({ name, multimonitor = true, monitor = 0, expand = true, fillMonitor = '' }) => {
return EventBox({
child: Box({
expand: expand,
css: `
min-width: ${fillMonitor.includes('h') ? monitors[monitor].width : 0}px;
min-height: ${fillMonitor.includes('v') ? monitors[monitor].height : 0}px;
`,
}),
setup: (self) => self.on('button-press-event', (self, event) => { // Any mouse button
if (multimonitor) closeWindowOnAllMonitors(name);
else App.closeWindow(name);
}),
})
}
export default clickCloseRegion;

View File

@@ -0,0 +1,219 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
const { Box, Button, Label, Revealer, SpinButton } = Widget;
export const ConfigToggle = ({
icon, name, desc = '', initValue,
expandWidget = true,
onChange = () => { }, extraSetup = () => { },
...rest
}) => {
const enabled = Variable(initValue);
const toggleIcon = Label({
className: `icon-material txt-bold ${enabled.value ? '' : 'txt-poof'}`,
label: `${enabled.value ? 'check' : ''}`,
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-fg-toggling-false', false);
if (!enabled.value) {
self.label = '';
self.toggleClassName('txt-poof', true);
}
else Utils.timeout(1, () => {
toggleIcon.label = 'check';
toggleIcon.toggleClassName('txt-poof', false);
})
}),
})
const toggleButtonIndicator = Box({
className: `switch-fg ${enabled.value ? 'switch-fg-true' : ''}`,
vpack: 'center',
hpack: 'start',
homogeneous: true,
children: [toggleIcon,],
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-fg-true', enabled.value);
}),
});
const toggleButton = Box({
hpack: 'end',
className: `switch-bg ${enabled.value ? 'switch-bg-true' : ''}`,
homogeneous: true,
children: [toggleButtonIndicator],
setup: (self) => self.hook(enabled, (self) => {
self.toggleClassName('switch-bg-true', enabled.value);
}),
});
const widgetContent = Box({
tooltipText: desc,
className: 'txt spacing-h-5 configtoggle-box',
children: [
...(icon !== undefined ? [MaterialIcon(icon, 'norm')] : []),
...(name !== undefined ? [Label({
className: 'txt txt-small',
label: name,
})] : []),
...(expandWidget ? [Box({ hexpand: true })] : []),
toggleButton,
]
});
const interactionWrapper = Button({
attribute: {
enabled: enabled,
toggle: (newValue) => {
enabled.value = !enabled.value;
onChange(interactionWrapper, enabled.value);
}
},
child: widgetContent,
onClicked: (self) => self.attribute.toggle(self),
setup: (self) => {
setupCursorHover(self);
self.connect('pressed', () => { // mouse down
toggleIcon.toggleClassName('txt-poof', true);
toggleIcon.toggleClassName('switch-fg-true', false);
if (!enabled.value) toggleIcon.toggleClassName('switch-fg-toggling-false', true);
});
extraSetup(self)
},
...rest,
});
interactionWrapper.enabled = enabled;
return interactionWrapper;
}
export const ConfigSegmentedSelection = ({
icon, name, desc = '',
options = [{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
initIndex = 0,
onChange,
...rest
}) => {
let lastSelected = initIndex;
let value = options[initIndex].value;
const widget = Box({
tooltipText: desc,
className: 'segment-container',
// homogeneous: true,
children: options.map((option, id) => {
const selectedIcon = Revealer({
revealChild: id == initIndex,
transition: 'slide_right',
transitionDuration: userOptions.animations.durationSmall,
child: MaterialIcon('check', 'norm')
});
return Button({
setup: setupCursorHover,
className: `segment-btn ${id == initIndex ? 'segment-btn-enabled' : ''}`,
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
selectedIcon,
Label({
label: option.name,
})
]
}),
onClicked: (self) => {
value = option.value;
const kids = widget.get_children();
kids[lastSelected].toggleClassName('segment-btn-enabled', false);
kids[lastSelected].get_children()[0].get_children()[0].revealChild = false;
lastSelected = id;
self.toggleClassName('segment-btn-enabled', true);
selectedIcon.revealChild = true;
onChange(option.value, option.name);
}
})
}),
...rest,
});
return widget;
}
export const ConfigMulipleSelection = ({
icon, name, desc = '',
optionsArr = [
[{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
[{ name: 'Option 3', value: 0 }, { name: 'Option 4', value: 1 }],
],
initIndex = [0, 0],
onChange,
...rest
}) => {
let lastSelected = initIndex;
const widget = Box({
tooltipText: desc,
className: 'multipleselection-container spacing-v-3',
vertical: true,
children: optionsArr.map((options, grp) => Box({
className: 'spacing-h-5',
hpack: 'center',
children: options.map((option, id) => Button({
setup: setupCursorHover,
className: `multipleselection-btn ${id == initIndex[1] && grp == initIndex[0] ? 'multipleselection-btn-enabled' : ''}`,
label: option.name,
onClicked: (self) => {
const kidsg = widget.get_children();
const kids = kidsg.flatMap(widget => widget.get_children());
kids.forEach(kid => {
kid.toggleClassName('multipleselection-btn-enabled', false);
});
lastSelected = id;
self.toggleClassName('multipleselection-btn-enabled', true);
onChange(option.value, option.name);
}
})),
})),
...rest,
});
return widget;
}
export const ConfigGap = ({ vertical = true, size = 5, ...rest }) => Box({
className: `gap-${vertical ? 'v' : 'h'}-${size}`,
...rest,
})
export const ConfigSpinButton = ({
icon, name, desc = '', initValue,
minValue = 0, maxValue = 100, step = 1,
expandWidget = true,
onChange = () => { }, extraSetup = () => { },
...rest
}) => {
const value = Variable(initValue);
const spinButton = SpinButton({
className: 'spinbutton',
range: [minValue, maxValue],
increments: [step, step],
onValueChanged: ({ value: newValue }) => {
value.value = newValue;
onChange(spinButton, newValue);
},
});
spinButton.value = value.value;
const widgetContent = Box({
tooltipText: desc,
className: 'txt spacing-h-5 configtoggle-box',
children: [
...(icon !== undefined ? [MaterialIcon(icon, 'norm')] : []),
...(name !== undefined ? [Label({
className: 'txt txt-small',
label: name,
})] : []),
...(expandWidget ? [Box({ hexpand: true })] : []),
spinButton,
],
setup: (self) => {
extraSetup(self);
},
...rest,
});
return widgetContent;
}

View File

@@ -0,0 +1,7 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
export const MaterialIcon = (icon, size, props = {}) => Widget.Label({
className: `icon-material txt-${size}`,
label: icon,
...props,
})

View File

@@ -0,0 +1,462 @@
// This file is for the actual widget for each single notification
const { GLib, Gdk, Gtk } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
const { Box, EventBox, Icon, Overlay, Label, Button, Revealer } = Widget;
import { MaterialIcon } from './materialicon.js';
import { setupCursorHover } from "../.widgetutils/cursorhover.js";
import { AnimatedCircProg } from "./cairo_circularprogress.js";
function guessMessageType(summary) {
const str = summary.toLowerCase();
if (str.includes('reboot')) return 'restart_alt';
if (str.includes('recording')) return 'screen_record';
if (str.includes('battery') || summary.includes('power')) return 'power';
if (str.includes('screenshot')) return 'screenshot_monitor';
if (str.includes('welcome')) return 'waving_hand';
if (str.includes('time')) return 'scheduleb';
if (str.includes('installed')) return 'download';
if (str.includes('update')) return 'update';
if (str.startsWith('file')) return 'folder_copy';
return 'chat';
}
function exists(widget) {
return widget !== null;
}
const getFriendlyNotifTimeString = (timeObject) => {
const messageTime = GLib.DateTime.new_from_unix_local(timeObject);
const oneMinuteAgo = GLib.DateTime.new_now_local().add_seconds(-60);
if (messageTime.compare(oneMinuteAgo) > 0)
return 'Now';
else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year())
return messageTime.format(userOptions.time.format);
else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year() - 1)
return 'Yesterday';
else
return messageTime.format(userOptions.time.dateFormat);
}
const NotificationIcon = (notifObject) => {
// { appEntry, appIcon, image }, urgency = 'normal'
if (notifObject.image) {
return Box({
valign: Gtk.Align.CENTER,
hexpand: false,
className: 'notif-icon',
css: `
background-image: url("${notifObject.image}");
background-size: auto 100%;
background-repeat: no-repeat;
background-position: center;
`,
});
}
let icon = 'NO_ICON';
if (Utils.lookUpIcon(notifObject.appIcon))
icon = notifObject.appIcon;
if (Utils.lookUpIcon(notifObject.appEntry))
icon = notifObject.appEntry;
return Box({
vpack: 'center',
hexpand: false,
className: `notif-icon notif-icon-material-${notifObject.urgency}`,
homogeneous: true,
children: [
(icon != 'NO_ICON' ?
Icon({
vpack: 'center',
icon: icon,
})
:
MaterialIcon(`${notifObject.urgency == 'critical' ? 'release_alert' : guessMessageType(notifObject.summary.toLowerCase())}`, 'hugerass', {
hexpand: true,
})
)
],
});
};
export default ({
notifObject,
isPopup = false,
props = {},
} = {}) => {
const popupTimeout = notifObject.timeout || (notifObject.urgency == 'critical' ? 8000 : 3000);
const command = (isPopup ?
() => notifObject.dismiss() :
() => notifObject.close()
)
const destroyWithAnims = () => {
widget.sensitive = false;
notificationBox.setCss(middleClickClose);
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(userOptions.animations.durationSmall * 2, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
const widget = EventBox({
onHover: (self) => {
self.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
if (!wholeThing.attribute.hovered)
wholeThing.attribute.hovered = true;
},
onHoverLost: (self) => {
self.window.set_cursor(null);
if (wholeThing.attribute.hovered)
wholeThing.attribute.hovered = false;
if (isPopup) {
command();
}
},
onMiddleClick: (self) => {
destroyWithAnims();
},
setup: (self) => {
self.on("button-press-event", () => {
wholeThing.attribute.held = true;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, true);
Utils.timeout(800, () => {
if (wholeThing?.attribute.held) {
Utils.execAsync(['wl-copy', `${notifObject.body}`]).catch(print);
notifTextSummary.label = notifObject.summary + " (copied)";
Utils.timeout(3000, () => notifTextSummary.label = notifObject.summary)
}
})
}).on("button-release-event", () => {
wholeThing.attribute.held = false;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, false);
})
}
});
let wholeThing = Revealer({
attribute: {
'close': undefined,
'destroyWithAnims': destroyWithAnims,
'dragging': false,
'held': false,
'hovered': false,
'id': notifObject.id,
},
revealChild: false,
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Box({ // Box to make sure css-based spacing works
homogeneous: true,
}),
});
const display = Gdk.Display.get_default();
const notifTextPreview = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationSmall,
revealChild: true,
child: Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 1,
truncate: 'end',
label: notifObject.body.split("\n")[0],
}),
});
const notifTextExpanded = Revealer({
transition: 'slide_up',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
child: Box({
vertical: true,
className: 'spacing-v-10',
children: [
Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 1,
wrap: true,
label: notifObject.body,
}),
Box({
className: 'notif-actions spacing-h-5',
children: [
Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => destroyWithAnims(),
setup: setupCursorHover,
child: Label({
label: 'Close',
}),
}),
...notifObject.actions.map(action => Widget.Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => notifObject.invoke(action.id),
setup: setupCursorHover,
child: Label({
label: action.label,
}),
}))
],
})
]
}),
});
const notifIcon = Box({
vpack: 'start',
homogeneous: true,
children: [
Overlay({
child: NotificationIcon(notifObject),
overlays: isPopup ? [AnimatedCircProg({
className: `notif-circprog-${notifObject.urgency}`,
vpack: 'center', hpack: 'center',
initFrom: (isPopup ? 100 : 0),
initTo: 0,
initAnimTime: popupTimeout,
})] : [],
}),
]
});
const notifTextSummary = Label({
xalign: 0,
className: 'txt-small txt-semibold titlefont',
justify: Gtk.Justification.LEFT,
hexpand: true,
maxWidthChars: 1,
truncate: 'end',
ellipsize: 3,
useMarkup: notifObject.summary.startsWith('<'),
label: notifObject.summary,
});
const initTimeString = getFriendlyNotifTimeString(notifObject.time);
const notifTextBody = Label({
vpack: 'center',
justification: 'right',
className: 'txt-smaller txt-semibold',
label: initTimeString,
setup: initTimeString == 'Now' ? (self) => {
let id = Utils.timeout(60000, () => {
self.label = getFriendlyNotifTimeString(notifObject.time);
id = null;
});
self.connect('destroy', () => { if (id) GLib.source_remove(id) });
} : () => { },
});
const notifText = Box({
valign: Gtk.Align.CENTER,
vertical: true,
hexpand: true,
children: [
Box({
children: [
notifTextSummary,
notifTextBody,
]
}),
notifTextPreview,
notifTextExpanded,
]
});
const notifExpandButton = Button({
vpack: 'start',
className: 'notif-expand-btn',
onClicked: (self) => {
if (notifTextPreview.revealChild) { // Expanding...
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
self.child.label = 'expand_less';
expanded = true;
}
else {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
self.child.label = 'expand_more';
expanded = false;
}
},
child: MaterialIcon('expand_more', 'norm', {
vpack: 'center',
}),
setup: setupCursorHover,
});
const notificationContent = Box({
...props,
className: `${isPopup ? 'popup-' : ''}notif-${notifObject.urgency} spacing-h-10`,
children: [
notifIcon,
Box({
className: 'spacing-h-5',
children: [
notifText,
notifExpandButton,
]
})
]
})
// Gesture stuff
const gesture = Gtk.GestureDrag.new(widget);
var initDirX = 0;
var initDirVertical = -1; // -1: unset, 0: horizontal, 1: vertical
var expanded = false;
// in px
const startMargin = 0;
const MOVE_THRESHOLD = 10;
const DRAG_CONFIRM_THRESHOLD = 100;
// in rem
const maxOffset = 10.227;
const endMargin = 20.455;
const disappearHeight = 6.818;
const leftAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: -${Number(maxOffset + endMargin)}rem;
margin-right: ${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const rightAnim1 = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const middleClickClose = `transition: ${userOptions.animations.durationSmall}ms cubic-bezier(0.85, 0, 0.15, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const notificationBox = Box({
attribute: {
'leftAnim1': leftAnim1,
'rightAnim1': rightAnim1,
'middleClickClose': middleClickClose,
'ready': false,
},
homogeneous: true,
children: [notificationContent],
setup: (self) => self
.hook(gesture, self => {
var offset_x = gesture.get_offset()[1];
var offset_y = gesture.get_offset()[2];
// Which dir?
if (initDirVertical == -1) {
if (Math.abs(offset_y) > MOVE_THRESHOLD)
initDirVertical = 1;
if (initDirX == 0 && Math.abs(offset_x) > MOVE_THRESHOLD) {
initDirVertical = 0;
initDirX = (offset_x > 0 ? 1 : -1);
}
}
// Horizontal drag
if (initDirVertical == 0 && offset_x > MOVE_THRESHOLD) {
if (initDirX < 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else
self.setCss(`
margin-left: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-right: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
else if (initDirVertical == 0 && offset_x < -MOVE_THRESHOLD) {
if (initDirX > 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else {
offset_x = Math.abs(offset_x);
self.setCss(`
margin-right: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-left: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
}
// Update dragging
wholeThing.attribute.dragging = Math.abs(offset_x) > MOVE_THRESHOLD;
if (Math.abs(offset_x) > MOVE_THRESHOLD ||
Math.abs(offset_y) > MOVE_THRESHOLD) wholeThing.attribute.held = false;
widget.window?.set_cursor(Gdk.Cursor.new_from_name(display, 'grabbing'));
// Vertical drag
if (initDirVertical == 1 && offset_y > MOVE_THRESHOLD && !expanded) {
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
expanded = true;
notifExpandButton.child.label = 'expand_less';
}
else if (initDirVertical == 1 && offset_y < -MOVE_THRESHOLD && expanded) {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
expanded = false;
notifExpandButton.child.label = 'expand_more';
}
}, 'drag-update')
.hook(gesture, self => {
if (!self.attribute.ready) {
wholeThing.revealChild = true;
self.attribute.ready = true;
return;
}
const offset_h = gesture.get_offset()[1];
if (Math.abs(offset_h) > DRAG_CONFIRM_THRESHOLD && offset_h * initDirX > 0) {
if (offset_h > 0) {
self.setCss(rightAnim1);
widget.sensitive = false;
}
else {
self.setCss(leftAnim1);
widget.sensitive = false;
}
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(userOptions.animations.durationSmall * 2, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
else {
self.setCss(`transition: margin 200ms cubic-bezier(0.05, 0.7, 0.1, 1), opacity 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${startMargin}px;
margin-right: ${startMargin}px;
margin-bottom: unset; margin-top: unset;
opacity: 1;`);
if (widget.window)
widget.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
wholeThing.attribute.dragging = false;
}
initDirX = 0;
initDirVertical = -1;
}, 'drag-end')
,
})
widget.add(notificationBox);
wholeThing.child.children = [widget];
if (isPopup) Utils.timeout(popupTimeout, () => {
if (wholeThing) {
wholeThing.revealChild = false;
Utils.timeout(userOptions.animations.durationSmall, () => {
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
command();
}, wholeThing);
}
})
return wholeThing;
}

View File

@@ -0,0 +1,306 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { languages } from './statusicons_languages.js';
// A guessing func to try to support langs not listed in data/languages.js
function isLanguageMatch(abbreviation, word) {
const lowerAbbreviation = abbreviation.toLowerCase();
const lowerWord = word.toLowerCase();
let j = 0;
for (let i = 0; i < lowerWord.length; i++) {
if (lowerWord[i] === lowerAbbreviation[j]) {
j++;
}
if (j === lowerAbbreviation.length) {
return true;
}
}
return false;
}
export const MicMuteIndicator = () => Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
setup: (self) => self.hook(Audio, (self) => {
self.revealChild = Audio.microphone?.stream?.isMuted;
}),
child: MaterialIcon('mic_off', 'norm'),
});
export const NotificationIndicator = (notifCenterName = 'sideright') => {
const widget = Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.revealChild = true;
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.revealChild = false;
}
})
,
child: Widget.Box({
children: [
MaterialIcon('notifications', 'norm'),
Widget.Label({
className: 'txt-small titlefont',
attribute: {
unreadCount: 0,
update: (self) => self.label = `${self.attribute.unreadCount}`,
},
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.attribute.unreadCount++;
self.attribute.update(self);
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.attribute.unreadCount = 0;
self.attribute.update(self);
}
})
,
})
]
})
});
return widget;
}
export const BluetoothIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'false': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_disabled' }),
'true': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth' }),
},
setup: (self) => self
.hook(Bluetooth, stack => {
stack.shown = String(Bluetooth.enabled);
})
,
});
const BluetoothDevices = () => Widget.Box({
className: 'spacing-h-5',
setup: self => self.hook(Bluetooth, self => {
self.children = Bluetooth.connected_devices.map((device) => {
return Widget.Box({
className: 'bar-bluetooth-device spacing-h-5',
vpack: 'center',
tooltipText: device.name,
children: [
Widget.Icon(`${device.iconName}-symbolic`),
...(device.batteryPercentage ? [Widget.Label({
className: 'txt-smallie',
label: `${device.batteryPercentage}`,
setup: (self) => {
self.hook(device, (self) => {
self.label = `${device.batteryPercentage}`;
}, 'notify::batteryPercentage')
}
})] : []),
]
});
});
self.visible = Bluetooth.connected_devices.length > 0;
}, 'notify::connected-devices'),
})
const NetworkWiredIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'fallback': SimpleNetworkIndicator(),
'unknown': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connected': Widget.Label({ className: 'txt-norm icon-material', label: 'lan' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.wired)
return;
const { internet } = Network.wired;
if (['connecting', 'connected'].includes(internet))
stack.shown = internet;
else if (Network.connectivity !== 'full')
stack.shown = 'disconnected';
else
stack.shown = 'fallback';
}),
});
const SimpleNetworkIndicator = () => Widget.Icon({
setup: (self) => self.hook(Network, self => {
const icon = Network[Network.primary || 'wifi']?.iconName;
self.icon = icon || '';
self.visible = icon;
}),
});
const NetworkWifiIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'disabled': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
'0': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_0_bar' }),
'1': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_1_bar' }),
'2': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_2_bar' }),
'3': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_3_bar' }),
'4': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_4_bar' }),
},
setup: (self) => self.hook(Network, (stack) => {
if (!Network.wifi) {
return;
}
if (Network.wifi.internet == 'connected') {
stack.shown = String(Math.ceil(Network.wifi.strength / 25));
}
else if (["disconnected", "connecting"].includes(Network.wifi.internet)) {
stack.shown = Network.wifi.internet;
}
}),
});
export const NetworkIndicator = () => Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'fallback': SimpleNetworkIndicator(),
'wifi': NetworkWifiIndicator(),
'wired': NetworkWiredIndicator(),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.primary) {
stack.shown = 'wifi';
return;
}
const primary = Network.primary || 'fallback';
if (['wifi', 'wired'].includes(primary))
stack.shown = primary;
else
stack.shown = 'fallback';
}),
});
const HyprlandXkbKeyboardLayout = async ({ useFlag } = {}) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
var languageStackArray = [];
const updateCurrentKeyboards = () => {
var initLangs = [];
JSON.parse(Utils.exec('hyprctl -j devices')).keyboards
.forEach(keyboard => {
initLangs.push(...keyboard.layout.split(',').map(lang => lang.trim()));
});
initLangs = [...new Set(initLangs)];
languageStackArray = Array.from({ length: initLangs.length }, (_, i) => {
const lang = languages.find(lang => lang.layout == initLangs[i]);
// if (!lang) return [
// initLangs[i],
// Widget.Label({ label: initLangs[i] })
// ];
// return [
// lang.layout,
// Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
// ];
// Object
if (!lang) return {
[initLangs[i]]: Widget.Label({ label: initLangs[i] })
};
return {
[lang.layout]: Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
};
});
};
updateCurrentKeyboards();
const widgetRevealer = Widget.Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationSmall,
revealChild: languageStackArray.length > 1,
});
const widgetKids = {
...languageStackArray.reduce((obj, lang) => {
return { ...obj, ...lang };
}, {}),
'undef': Widget.Label({ label: '?' }),
}
const widgetContent = Widget.Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: widgetKids,
setup: (self) => self.hook(Hyprland, (stack, kbName, layoutName) => {
if (!kbName) {
return;
}
var lang = languages.find(lang => layoutName.includes(lang.name));
if (lang) {
widgetContent.shown = lang.layout;
}
else { // Attempt to support langs not listed
lang = languageStackArray.find(lang => isLanguageMatch(lang[0], layoutName));
if (!lang) stack.shown = 'undef';
else stack.shown = lang[0];
}
}, 'keyboard-layout'),
});
widgetRevealer.child = widgetContent;
return widgetRevealer;
} catch {
return null;
}
}
const OptionalKeyboardLayout = async () => {
try {
return await HyprlandXkbKeyboardLayout({ useFlag: userOptions.appearance.keyboardUseFlag });
} catch {
return null;
}
};
const createKeyboardLayoutInstances = async () => {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
const monitorsCount = Hyprland.monitors.length
const instances = await Promise.all(
Array.from({ length: monitorsCount }, () => OptionalKeyboardLayout())
);
return instances;
};
const optionalKeyboardLayoutInstances = await createKeyboardLayoutInstances()
export const StatusIcons = (props = {}, monitor = 0) => Widget.Box({
...props,
child: Widget.Box({
className: 'spacing-h-15',
children: [
MicMuteIndicator(),
optionalKeyboardLayoutInstances[monitor],
NotificationIndicator(),
NetworkIndicator(),
Widget.Box({
className: 'spacing-h-5',
children: [BluetoothIndicator(), BluetoothDevices()]
})
]
})
});

View File

@@ -0,0 +1,62 @@
// For keyboard layout in statusicons.js
// This list is not exhaustive. It just includes known/possible languages of users of my dotfiles
// Add your language here if you use multi-lang xkb input. Else, ignore
// Note that something like "French (Canada)" should go before "French"
// and "English (US)" should go before "English"
export const languages = [
{
layout: 'us',
name: 'English (US)',
flag: '🇺🇸'
},
{
layout: 'ru',
name: 'Russian',
flag: '🇷🇺',
},
{
layout: 'pl',
name: 'Polish',
flag: '🇷🇵🇵🇱',
},
{
layout: 'ro',
name: 'Romanian',
flag: '🇷🇴',
},
{
layout: 'ca',
name: 'French (Canada)',
flag: '🇫🇷',
},
{
layout: 'fr',
name: 'French',
flag: '🇫🇷',
},
{
layout: 'tr',
name: 'Turkish',
flag: '🇹🇷',
},
{
layout: 'jp',
name: 'Japanese',
flag: '🇯🇵',
},
{
layout: 'cn',
name: 'Chinese',
flag: '🇨🇳',
},
{
layout: 'vn',
name: 'Vietnamese',
flag: '🇻🇳',
},
{
layout: 'undef',
name: 'Undefined',
flag: '🧐',
},
]

View File

@@ -0,0 +1,279 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Button, EventBox, Label, Overlay, Stack } = Widget;
import { MaterialIcon } from './materialicon.js';
import { NavigationIndicator } from './cairo_navigationindicator.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { DoubleRevealer } from '../.widgethacks/advancedrevealers.js';
export const TabContainer = ({ icons, names, children, className = '', setup = () => { }, ...rest }) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(icons.length, names.length, children.length);
const tabs = Box({
homogeneous: true,
children: Array.from({ length: count }, (_, i) => Button({ // Tab button
className: 'tab-btn',
onClicked: () => shownIndex.value = i,
setup: setupCursorHover,
child: Box({
hpack: 'center',
vpack: 'center',
className: 'spacing-h-5 txt-small',
children: [
MaterialIcon(icons[i], 'norm'),
Label({
label: names[i],
})
]
})
})),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleClassName('tab-btn-active', false);
self.children[shownIndex.value].toggleClassName('tab-btn-active', true);
previousShownIndex = shownIndex.value;
}),
});
const tabIndicatorLine = Box({
vertical: true,
homogeneous: true,
setup: (self) => self.hook(shownIndex, (self) => {
self.children[0].css = `font-size: ${shownIndex.value}px;`;
}),
children: [NavigationIndicator({
className: 'tab-indicator',
count: count,
css: `font-size: ${shownIndex.value}px;`,
})],
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
children: [
tabs,
tabIndicatorLine
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
return mainBox;
}
export const IconTabContainer = ({
iconWidgets, names, children, className = '',
setup = () => { }, onChange = () => { },
tabsHpack = 'center', tabSwitcherClassName = '',
...rest
}) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(iconWidgets.length, names.length, children.length);
const tabs = Box({
hpack: tabsHpack,
className: `spacing-h-5 ${tabSwitcherClassName}`,
children: iconWidgets.map((icon, i) => Button({
className: 'tab-icon',
tooltipText: names[i],
child: icon,
setup: setupCursorHover,
onClicked: () => shownIndex.value = i,
})),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleClassName('tab-icon-active', false);
self.children[shownIndex.value].toggleClassName('tab-icon-active', true);
previousShownIndex = shownIndex.value;
}),
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
hexpand: true,
children: [
tabs,
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
mainBox.shown = shownIndex;
return mainBox;
}
export const ExpandingIconTabContainer = ({
icons, names, children, className = '',
setup = () => { }, onChange = () => { },
tabsHpack = 'center', tabSwitcherClassName = '',
transitionDuration = userOptions.animations.durationLarge,
...rest
}) => {
const shownIndex = Variable(0);
let previousShownIndex = 0;
const count = Math.min(icons.length, names.length, children.length);
const tabs = Box({
hpack: tabsHpack,
className: `spacing-h-5 ${tabSwitcherClassName}`,
children: icons.map((icon, i) => {
const tabIcon = MaterialIcon(icon, 'norm', { hexpand: true });
const tabName = DoubleRevealer({
transition1: 'slide_right',
transition2: 'crossfade',
duration1: 0,
duration2: 0,
// duration1: userOptions.animations.durationSmall,
// duration2: userOptions.animations.durationSmall,
child: Label({
className: 'margin-left-5 txt-small',
label: names[i],
}),
revealChild: i === shownIndex.value,
})
const button = Button({
className: 'tab-icon-expandable',
tooltipText: names[i],
child: Box({
homogeneous: true,
children: [Box({
hpack: 'center',
children: [
tabIcon,
tabName,
]
})],
}),
setup: setupCursorHover,
onClicked: () => shownIndex.value = i,
});
button.toggleFocus = (value) => {
tabIcon.hexpand = !value;
button.toggleClassName('tab-icon-expandable-active', value);
tabName.toggleRevealChild(value);
}
return button;
}),
setup: (self) => self.hook(shownIndex, (self) => {
self.children[previousShownIndex].toggleFocus(false);
self.children[shownIndex.value].toggleFocus(true);
previousShownIndex = shownIndex.value;
}),
});
const tabSection = Box({
homogeneous: true,
children: [EventBox({
onScrollUp: () => mainBox.prevTab(),
onScrollDown: () => mainBox.nextTab(),
child: Box({
vertical: true,
hexpand: true,
children: [
tabs,
]
})
})]
});
const contentStack = Stack({
transition: 'slide_left_right',
transitionDuration: transitionDuration,
children: children.reduce((acc, currentValue, index) => {
acc[index] = currentValue;
return acc;
}, {}),
setup: (self) => self.hook(shownIndex, (self) => {
self.shown = `${shownIndex.value}`;
}),
});
const mainBox = Box({
attribute: {
children: children,
shown: shownIndex,
names: names,
},
vertical: true,
className: `spacing-v-5 ${className}`,
setup: (self) => {
self.pack_start(tabSection, false, false, 0);
self.pack_end(contentStack, true, true, 0);
setup(self);
self.hook(shownIndex, (self) => onChange(self, shownIndex.value));
},
...rest,
});
mainBox.nextTab = () => shownIndex.value = Math.min(shownIndex.value + 1, count - 1);
mainBox.prevTab = () => shownIndex.value = Math.max(shownIndex.value - 1, 0);
mainBox.cycleTab = () => shownIndex.value = (shownIndex.value + 1) % count;
mainBox.focusName = (name) => {
const focusIndex = names.indexOf(name);
if (focusIndex !== -1) {
shownIndex.value = focusIndex;
}
}
mainBox.shown = shownIndex;
return mainBox;
}

View File

@@ -0,0 +1,250 @@
import GLib from 'gi://GLib';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
import userOverrides from '../../user_options.js';
// Default options.
// Add overrides in ~/.config/ags/user_options.js
let configOptions = {
// General stuff
'ai': {
'defaultGPTProvider': "openai",
'defaultTemperature': 0.9,
'enhancements': true,
'useHistory': true,
'safety': true,
'writingCursor': " ...", // Warning: Using weird characters can mess up Markdown rendering
'proxyUrl': null, // Can be "socks5://127.0.0.1:9050" or "http://127.0.0.1:8080" for example. Leave it blank if you don't need it.
},
'animations': {
'choreographyDelay': 35,
'durationSmall': 110,
'durationLarge': 180,
},
'appearance': {
'autoDarkMode': { // Turns on dark mode in certain hours. Time in 24h format
'enabled': false,
'from': "18:10",
'to': "6:10",
},
'keyboardUseFlag': false, // Use flag emoji instead of abbreviation letters
'layerSmoke': false,
'layerSmokeStrength': 0.2,
'fakeScreenRounding': 1, // 0: None | 1: Always | 2: When not fullscreen
},
'apps': {
'bluetooth': "blueberry",
'imageViewer': "loupe",
'network': "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center wifi",
'settings': "XDG_CURRENT_DESKTOP=\"gnome\" gnome-control-center",
'taskManager': "gnome-usage",
'terminal': "foot", // This is only for shell actions
},
'battery': {
'low': 20,
'critical': 10,
'warnLevels': [20, 15, 5],
'warnTitles': ["Low battery", "Very low battery", 'Critical Battery'],
'warnMessages': ["Plug in the charger", "You there?", 'PLUG THE CHARGER ALREADY'],
'suspendThreshold': 3,
},
'brightness': {
// Object of controller names for each monitor, either "brightnessctl" or "ddcutil" or "auto"
// 'default' one will be used if unspecified
// Examples
// 'eDP-1': "brightnessctl",
// 'DP-1': "ddcutil",
'controllers': {
'default': "auto",
},
},
'cheatsheet': {
'keybinds': {
'configPath': "" // Path to hyprland keybind config file. Leave empty for default (~/.config/hypr/hyprland/keybinds.conf)
}
},
'gaming': {
'crosshair': {
'size': 20,
'color': 'rgba(113,227,32,0.9)',
},
},
'monitors': {
'scaleMethod': "division", // Either "division" [default] or "gdk"
},
'music': {
'preferredPlayer': "plasma-browser-integration",
},
'onScreenKeyboard': {
'layout': "qwerty_full", // See modules/onscreenkeyboard/onscreenkeyboard.js for available layouts
},
'overview': {
'scale': 0.18, // Relative to screen size
'numOfRows': 2,
'numOfCols': 5,
'wsNumScale': 0.09,
'wsNumMarginScale': 0.07,
},
'sidebar': {
'ai': {
'extraGptModels': {
'oxygen3': {
'name': 'Oxygen (GPT-3.5)',
'logo_name': 'ai-oxygen-symbolic',
'description': 'An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key',
'base_url': 'https://app.oxyapi.uk/v1/chat/completions',
'key_get_url': 'https://discord.com/invite/kM6MaCqGKA',
'key_file': 'oxygen_key.txt',
'model': 'gpt-3.5-turbo',
},
}
},
'image': {
'columns': 2,
'batchCount': 20,
'allowNsfw': false,
},
'pages': {
'order': ["apis", "tools"],
'apis': {
'order': ["gemini", "gpt", "waifu", "booru"],
}
},
},
'search': {
'enableFeatures': {
'actions': true,
'commands': true,
'mathResults': true,
'directorySearch': true,
'aiSearch': true,
'webSearch': true,
},
'engineBaseUrl': "https://www.google.com/search?q=",
'excludedSites': ["quora.com"],
},
'time': {
// See https://docs.gtk.org/glib/method.DateTime.format.html
// Here's the 12h format: "%I:%M%P"
// For seconds, add "%S" and set interval to 1000
'format': "%H:%M",
'interval': 5000,
'dateFormatLong': "%A, %d/%m", // On bar
'dateInterval': 5000,
'dateFormat': "%d/%m", // On notif time
},
'weather': {
'city': "",
'preferredUnit': "C", // Either C or F
},
'workspaces': {
'shown': 10,
},
'dock': {
'enabled': false,
'hiddenThickness': 5,
'pinnedApps': ['firefox', 'org.gnome.Nautilus'],
'layer': 'top',
'monitorExclusivity': true, // Dock will move to other monitor along with focus if enabled
'searchPinnedAppIcons': false, // Try to search for the correct icon if the app class isn't an icon name
'trigger': ['client-added', 'client-removed'], // client_added, client_move, workspace_active, client_active
// Automatically hide dock after `interval` ms since trigger
'autoHide': [
{
'trigger': 'client-added',
'interval': 500,
},
{
'trigger': 'client-removed',
'interval': 500,
},
],
},
// Longer stuff
'icons': {
// Find the window's icon by its class with levenshteinDistance
// The file names are processed at startup, so if there
// are too many files in the search path it'll affect performance
// Example: ['/usr/share/icons/Tela-nord/scalable/apps']
'searchPaths': [''],
'symbolicIconTheme': {
"dark": "Adwaita",
"light": "Adwaita",
},
substitutions: {
'code-url-handler': "visual-studio-code",
'Code': "visual-studio-code",
'GitHub Desktop': "github-desktop",
'Minecraft* 1.20.1': "minecraft",
'gnome-tweaks': "org.gnome.tweaks",
'pavucontrol-qt': "pavucontrol",
'wps': "wps-office2019-kprometheus",
'wpsoffice': "wps-office2019-kprometheus",
'': "image-missing",
},
regexSubstitutions: [
{
regex: /^steam_app_(\d+)$/,
replace: "steam_icon_$1",
}
]
},
'keybinds': {
// Format: Mod1+Mod2+key. CaSe SeNsItIvE!
// Modifiers: Shift Ctrl Alt Hyper Meta
// See https://docs.gtk.org/gdk3/index.html#constants for the other keys (they are listed as KEY_key)
'overview': {
'altMoveLeft': "Ctrl+b",
'altMoveRight': "Ctrl+f",
'deleteToEnd': "Ctrl+k",
},
'sidebar': {
'apis': {
'nextTab': "Page_Down",
'prevTab': "Page_Up",
},
'options': { // Right sidebar
'nextTab': "Page_Down",
'prevTab': "Page_Up",
},
'pin': "Ctrl+p",
'cycleTab': "Ctrl+Tab",
'nextTab': "Ctrl+Page_Down",
'prevTab': "Ctrl+Page_Up",
},
'cheatsheet': {
'keybinds': {
'nextTab': "Page_Down",
'prevTab': "Page_Up",
},
'nextTab': "Ctrl+Page_Down",
'prevTab': "Ctrl+Page_Up",
'cycleTab': "Ctrl+Tab",
}
},
}
// Override defaults with user's options
let optionsOkay = true;
function overrideConfigRecursive(userOverrides, configOptions = {}, check = true) {
for (const [key, value] of Object.entries(userOverrides)) {
if (configOptions[key] === undefined && check) {
optionsOkay = false;
}
else if (typeof value === 'object' && !(value instanceof Array)) {
if (key === "substitutions" || key === "regexSubstitutions" || key === "extraGptModels") {
overrideConfigRecursive(value, configOptions[key], false);
} else overrideConfigRecursive(value, configOptions[key]);
} else {
configOptions[key] = value;
}
}
}
overrideConfigRecursive(userOverrides, configOptions);
if (!optionsOkay) Utils.timeout(2000, () => Utils.execAsync(['notify-send',
'Update your user options',
'One or more config options don\'t exist',
'-a', 'ags',
]).catch(print))
globalThis['userOptions'] = configOptions;
export default configOptions;

View File

@@ -0,0 +1,14 @@
const { Gio, GLib, Gtk } = imports.gi;
export function fileExists(filePath) {
let file = Gio.File.new_for_path(filePath);
return file.query_exists(null);
}
export function expandTilde(path) {
if (path.startsWith('~')) {
return GLib.get_home_dir() + path.slice(1);
} else {
return path;
}
}

View File

@@ -0,0 +1,28 @@
const { Gtk } = imports.gi;
export function iconExists(iconName) {
let iconTheme = Gtk.IconTheme.get_default();
return iconTheme.has_icon(iconName);
}
export function substitute(str) {
// Normal substitutions
if (userOptions.icons.substitutions[str])
return userOptions.icons.substitutions[str];
// Regex substitutions
for (let i = 0; i < userOptions.icons.regexSubstitutions.length; i++) {
const substitution = userOptions.icons.regexSubstitutions[i];
const replacedName = str.replace(
substitution.regex,
substitution.replace,
);
if (replacedName != str) return replacedName;
}
// Guess: convert to kebab case
if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, "-");
// Original string
return str;
}

View File

@@ -0,0 +1,4 @@
export function clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
}

View File

@@ -0,0 +1,78 @@
// Converts from Markdown to Pango. This does not support code blocks.
// For illogical-impulse, code blocks are treated separately, in their own GtkSourceView widgets.
// Partly inherited from https://github.com/ubunatic/md2pango
const monospaceFonts = 'JetBrains Mono NF, JetBrains Mono Nerd Font, JetBrains Mono NL, SpaceMono NF, SpaceMono Nerd Font, monospace';
const replacements = {
'indents': [
{ name: 'BULLET', re: /^(\s*)([\*\-]\s)(.*)(\s*)$/, sub: ' $1- $3' },
{ name: 'NUMBERING', re: /^(\s*[0-9]+\.\s)(.*)(\s*)$/, sub: ' $1 $2' },
],
'escapes': [
{ name: 'COMMENT', re: /<!--.*-->/, sub: '' },
{ name: 'AMPERSTAND', re: /&/g, sub: '&amp;' },
{ name: 'LESSTHAN', re: /</g, sub: '&lt;' },
{ name: 'GREATERTHAN', re: />/g, sub: '&gt;' },
],
'sections': [
{ name: 'H1', re: /^(#\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="170%">$2</span>' },
{ name: 'H2', re: /^(##\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="150%">$2</span>' },
{ name: 'H3', re: /^(###\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="125%">$2</span>' },
{ name: 'H4', re: /^(####\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="100%">$2</span>' },
{ name: 'H5', re: /^(#####\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="90%">$2</span>' },
],
'styles': [
{ name: 'BOLD', re: /(\*\*)(\S[\s\S]*?\S)(\*\*)/g, sub: "<b>$2</b>" },
{ name: 'UND', re: /(__)(\S[\s\S]*?\S)(__)/g, sub: "<u>$2</u>" },
{ name: 'EMPH', re: /\*(\S.*?\S)\*/g, sub: "<i>$1</i>" },
// { name: 'EMPH', re: /_(\S.*?\S)_/g, sub: "<i>$1</i>" },
{ name: 'HEXCOLOR', re: /#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/g, sub: '<span bgcolor="#$1" fgcolor="#000000" font_family="' + monospaceFonts + '">#$1</span>' },
{ name: 'INLCODE', re: /(`)([^`]*)(`)/g, sub: '<span font_weight="bold" font_family="' + monospaceFonts + '">$2</span>' },
// { name: 'UND', re: /(__|\*\*)(\S[\s\S]*?\S)(__|\*\*)/g, sub: "<u>$2</u>" },
],
}
const replaceCategory = (text, replaces) => {
for (const type of replaces) {
text = text.replace(type.re, type.sub);
}
return text;
}
// Main function
export default (text) => {
let lines = text.split('\n')
let output = [];
// Replace
for (const line of lines) {
let result = line;
result = replaceCategory(result, replacements.indents);
result = replaceCategory(result, replacements.escapes);
result = replaceCategory(result, replacements.sections);
result = replaceCategory(result, replacements.styles);
output.push(result)
}
// Remove trailing whitespaces
output = output.map(line => line.replace(/ +$/, ''))
return output.join('\n');
}
export const markdownTest = `## Inline formatting
- **Bold** *Italics* __Underline__
- \`Monospace text\` 🤓
- Colors
- Nvidia green #7ABB08
- Soundcloud orange #FF5500
## Code block
\`\`\`cpp
#include <bits/stdc++.h>
const std::string GREETING="UwU";
int main() { std::cout << GREETING; }
\`\`\`
## LaTeX
\`\`\`latex
\\frac{d}{dx} \\left( \\frac{x-438}{x^2+23x-7} \\right) = \\frac{-x^2 + 869}{(x^2+23x-7)^2} \\\\\\\\ cos(2x) = 2cos^2(x) - 1 = 1 - 2sin^2(x) = cos^2(x) - sin^2(x)
\`\`\`
`;

View File

@@ -0,0 +1,61 @@
const { GLib } = imports.gi;
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { execAsync, exec } = Utils;
export const distroID = exec(`bash -c 'cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2 | sed "s/\\"//g"'`).trim();
export const isDebianDistro = (distroID == 'linuxmint' || distroID == 'ubuntu' || distroID == 'debian' || distroID == 'zorin' || distroID == 'popos' || distroID == 'raspbian' || distroID == 'kali');
export const isArchDistro = (distroID == 'arch' || distroID == 'endeavouros' || distroID == 'cachyos');
export const hasFlatpak = !!exec(`bash -c 'command -v flatpak'`);
const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
export const darkMode = Variable(!(Utils.readFile(LIGHTDARK_FILE_LOCATION).split('\n')[0].trim() == 'light'));
darkMode.connect('changed', ({ value }) => {
let lightdark = value ? "dark" : "light";
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "1s/.*/${lightdark}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
.then(execAsync(['bash', '-c', `command -v darkman && darkman set ${lightdark}`])) // Optional darkman integration
.catch(print);
});
globalThis['darkMode'] = darkMode;
export const hasPlasmaIntegration = !!Utils.exec('bash -c "command -v plasma-browser-integration-host"');
export const getDistroIcon = () => {
// Arches
if(distroID == 'arch') return 'arch-symbolic';
if(distroID == 'endeavouros') return 'endeavouros-symbolic';
if(distroID == 'cachyos') return 'cachyos-symbolic';
// Funny flake
if(distroID == 'nixos') return 'nixos-symbolic';
// Cool thing
if(distroID == 'fedora') return 'fedora-symbolic';
// Debians
if(distroID == 'linuxmint') return 'ubuntu-symbolic';
if(distroID == 'ubuntu') return 'ubuntu-symbolic';
if(distroID == 'debian') return 'debian-symbolic';
if(distroID == 'zorin') return 'ubuntu-symbolic';
if(distroID == 'popos') return 'ubuntu-symbolic';
if(distroID == 'raspbian') return 'debian-symbolic';
if(distroID == 'kali') return 'debian-symbolic';
return 'linux-symbolic';
}
export const getDistroName = () => {
// Arches
if(distroID == 'arch') return 'Arch Linux';
if(distroID == 'endeavouros') return 'EndeavourOS';
if(distroID == 'cachyos') return 'CachyOS';
// Funny flake
if(distroID == 'nixos') return 'NixOS';
// Cool thing
if(distroID == 'fedora') return 'Fedora';
// Debians
if(distroID == 'linuxmint') return 'Linux Mint';
if(distroID == 'ubuntu') return 'Ubuntu';
if(distroID == 'debian') return 'Debian';
if(distroID == 'zorin') return 'Zorin';
if(distroID == 'popos') return 'Pop!_OS';
if(distroID == 'raspbian') return 'Raspbian';
if(distroID == 'kali') return 'Kali Linux';
return 'Linux';
}

View File

@@ -0,0 +1,86 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Revealer, Scrollable } = Widget;
export const MarginRevealer = ({
transition = 'slide_down',
child,
revealChild,
showClass = 'element-show', // These are for animation curve, they don't really hide
hideClass = 'element-hide', // Don't put margins in these classes!
extraSetup = () => { },
...rest
}) => {
const widget = Scrollable({
...rest,
attribute: {
'revealChild': true, // It'll be set to false after init if it's supposed to hide
'transition': transition,
'show': () => {
if (widget.attribute.revealChild) return;
widget.hscroll = 'never';
widget.vscroll = 'never';
child.toggleClassName(hideClass, false);
child.toggleClassName(showClass, true);
widget.attribute.revealChild = true;
child.css = 'margin: 0px;';
},
'hide': () => {
if (!widget.attribute.revealChild) return;
child.toggleClassName(hideClass, true);
child.toggleClassName(showClass, false);
widget.attribute.revealChild = false;
if (widget.attribute.transition == 'slide_left')
child.css = `margin-right: -${child.get_allocated_width()}px;`;
else if (widget.attribute.transition == 'slide_right')
child.css = `margin-left: -${child.get_allocated_width()}px;`;
else if (widget.attribute.transition == 'slide_up')
child.css = `margin-bottom: -${child.get_allocated_height()}px;`;
else if (widget.attribute.transition == 'slide_down')
child.css = `margin-top: -${child.get_allocated_height()}px;`;
},
'toggle': () => {
if (widget.attribute.revealChild) widget.attribute.hide();
else widget.attribute.show();
},
},
child: child,
hscroll: `${revealChild ? 'never' : 'always'}`,
vscroll: `${revealChild ? 'never' : 'always'}`,
setup: (self) => {
extraSetup(self);
}
});
child.toggleClassName(`${revealChild ? showClass : hideClass}`, true);
return widget;
}
// TODO: Allow reveal update. Currently this just helps at declaration
export const DoubleRevealer = ({
transition1 = 'slide_right',
transition2 = 'slide_left',
duration1 = 150,
duration2 = 150,
child,
revealChild,
...rest
}) => {
const r2 = Revealer({
transition: transition2,
transitionDuration: duration2,
revealChild: revealChild,
child: child,
});
const r1 = Revealer({
transition: transition1,
transitionDuration: duration1,
revealChild: revealChild,
child: r2,
...rest,
})
r1.toggleRevealChild = (value) => {
r1.revealChild = value;
r2.revealChild = value;
}
return r1;
}

View File

@@ -0,0 +1,36 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Window } = Widget;
export default ({
name,
child,
showClassName = "",
hideClassName = "",
...props
}) => {
return Window({
name,
visible: false,
layer: 'overlay',
...props,
child: Box({
setup: (self) => {
self.keybind("Escape", () => closeEverything());
if (showClassName != "" && hideClassName !== "") {
self.hook(App, (self, currentName, visible) => {
if (currentName === name) {
self.toggleClassName(hideClassName, !visible);
}
});
if (showClassName !== "" && hideClassName !== "")
self.className = `${showClassName} ${hideClassName}`;
}
},
child: child,
}),
});
}

View File

@@ -0,0 +1,4 @@
import Cairo from 'gi://cairo?version=1.0';
export const dummyRegion = new Cairo.Region();
export const enableClickthrough = (self) => self.input_shape_combine_region(dummyRegion);

View File

@@ -0,0 +1,57 @@
const { Gdk } = imports.gi;
export function setupCursorHover(button) { // Hand pointing cursor on hover
const display = Gdk.Display.get_default();
button.connect('enter-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverAim(button) { // Crosshair cursor on hover
button.connect('enter-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'crosshair');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverGrab(button) { // Hand ready to grab on hover
button.connect('enter-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'grab');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverInfo(button) { // "?" mark cursor on hover
const display = Gdk.Display.get_default();
button.connect('enter-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'help');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}

View File

@@ -0,0 +1,25 @@
const { Gdk } = imports.gi;
const MODS = {
'Shift': Gdk.ModifierType.SHIFT_MASK,
'Ctrl': Gdk.ModifierType.CONTROL_MASK,
'Alt': Gdk.ModifierType.ALT_MASK,
'Hyper': Gdk.ModifierType.HYPER_MASK,
'Meta': Gdk.ModifierType.META_MASK
}
export const checkKeybind = (event, keybind) => {
const pressedModMask = event.get_state()[1];
const pressedKey = event.get_keyval()[1];
const keys = keybind.split('+');
for (let i = 0; i < keys.length; i++) {
if (keys[i] in MODS) {
if (!(pressedModMask & MODS[keys[i]])) {
return false;
}
} else if (pressedKey !== Gdk[`KEY_${keys[i]}`]) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,213 @@
const { GLib, Gdk, Gtk } = imports.gi;
const Lang = imports.lang;
const Cairo = imports.cairo;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
import App from 'resource:///com/github/Aylur/ags/app.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, DrawingArea, EventBox } = Widget;
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const dummyWs = Box({ className: 'bar-ws-focus' }); // Not shown. Only for getting size props
const dummyActiveWs = Box({ className: 'bar-ws-focus bar-ws-focus-active' }); // Not shown. Only for getting size props
const dummyOccupiedWs = Box({ className: 'bar-ws-focus bar-ws-focus-occupied' }); // Not shown. Only for getting size props
const WS_TAKEN_WIDTH_MULTIPLIER = 1.4;
const floor = Math.floor;
const ceil = Math.ceil;
// Font size = workspace id
const WorkspaceContents = (count = 10) => {
return DrawingArea({
className: 'menu-decel',
attribute: {
lastImmediateActiveWs: 0,
immediateActiveWs: 0,
initialized: false,
workspaceMask: 0,
workspaceGroup: 0,
updateMask: (self) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
// if (self.attribute.initialized) return; // We only need this to run once
const workspaces = Hyprland.workspaces;
let workspaceMask = 0;
for (let i = 0; i < workspaces.length; i++) {
const ws = workspaces[i];
if (ws.id <= offset || ws.id > offset + count) continue; // Out of range, ignore
if (workspaces[i].windows > 0)
workspaceMask |= (1 << (ws.id - offset));
}
// console.log('Mask:', workspaceMask.toString(2));
self.attribute.workspaceMask = workspaceMask;
// self.attribute.initialized = true;
self.queue_draw();
},
toggleMask: (self, occupied, name) => {
if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
else self.attribute.workspaceMask &= ~(1 << parseInt(name));
self.queue_draw();
},
},
setup: (area) => area
.hook(Hyprland.active.workspace, (self) => {
const newActiveWs = (Hyprland.active.workspace.id - 1) % count + 1;
self.setCss(`font-size: ${newActiveWs}px;`);
self.attribute.lastImmediateActiveWs = self.attribute.immediateActiveWs;
self.attribute.immediateActiveWs = newActiveWs;
const previousGroup = self.attribute.workspaceGroup;
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / count);
if (currentGroup !== previousGroup) {
self.attribute.updateMask(self);
self.attribute.workspaceGroup = currentGroup;
}
})
.hook(Hyprland, (self) => self.attribute.updateMask(self), 'notify::workspaces')
.on('draw', Lang.bind(area, (area, cr) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
const allocation = area.get_allocation();
const { width, height } = allocation;
const workspaceStyleContext = dummyWs.get_style_context();
const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const workspaceRadius = workspaceDiameter / 2;
const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
const activeWorkspaceWidth = activeWorkspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
// const activeWorkspaceWidth = 100;
const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const widgetStyleContext = area.get_style_context();
const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
const lastImmediateActiveWs = area.attribute.lastImmediateActiveWs;
const immediateActiveWs = area.attribute.immediateActiveWs;
// Draw
area.set_size_request(workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth, -1);
for (let i = 1; i <= count; i++) {
if (i == immediateActiveWs) continue;
let colors = {};
if (area.attribute.workspaceMask & (1 << i)) colors = occupiedbg;
else colors = wsbg;
// if ((i == immediateActiveWs + 1 && immediateActiveWs < activeWs) ||
// (i == immediateActiveWs + 1 && immediateActiveWs < activeWs)) {
// const widthPercentage = (i == immediateActiveWs - 1) ?
// 1 - (immediateActiveWs - activeWs) :
// activeWs - immediateActiveWs;
// cr.setSourceRGBA(colors.red * widthPercentage + activebg.red * (1 - widthPercentage),
// colors.green * widthPercentage + activebg.green * (1 - widthPercentage),
// colors.blue * widthPercentage + activebg.blue * (1 - widthPercentage),
// colors.alpha);
// }
// else
cr.setSourceRGBA(colors.red, colors.green, colors.blue, colors.alpha)
const centerX = (i <= activeWs) ?
(-workspaceRadius + (workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * i))
: -workspaceRadius + workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth - ((count - i) * workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER);
cr.arc(centerX, height / 2, workspaceRadius, 0, 2 * Math.PI);
cr.fill();
// What if shrinking
if (i == floor(activeWs) && immediateActiveWs > activeWs) { // To right
const widthPercentage = 1 - (ceil(activeWs) - activeWs);
const leftX = centerX;
const wsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * (1 - widthPercentage);
cr.rectangle(leftX, height / 2 - workspaceRadius, wsWidth, workspaceDiameter);
cr.fill();
cr.arc(leftX + wsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
cr.fill();
}
else if (i == ceil(activeWs) && immediateActiveWs < activeWs) { // To left
const widthPercentage = activeWs - floor(activeWs);
const rightX = centerX;
const wsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * widthPercentage;
const leftX = rightX - wsWidth;
cr.rectangle(leftX, height / 2 - workspaceRadius, wsWidth, workspaceDiameter);
cr.fill();
cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2);
cr.fill();
}
}
let widthPercentage, leftX, rightX, activeWsWidth;
cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
if (immediateActiveWs > activeWs) { // To right
const immediateActiveWs = ceil(activeWs);
widthPercentage = immediateActiveWs - activeWs;
rightX = -workspaceRadius + workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * (count - 1) + activeWorkspaceWidth - ((count - immediateActiveWs) * workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER);
activeWsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * (1 - widthPercentage);
leftX = rightX - activeWsWidth;
cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2); // Should be 0.5 * Math.PI, 1.5 * Math.PI in theory but it leaves a weird 1px gap
cr.fill();
cr.rectangle(leftX, height / 2 - workspaceRadius, activeWsWidth, workspaceDiameter);
cr.fill();
cr.arc(leftX + activeWsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
cr.fill();
}
else { // To left
const immediateActiveWs = floor(activeWs);
widthPercentage = 1 - (activeWs - immediateActiveWs);
leftX = -workspaceRadius + (workspaceDiameter * WS_TAKEN_WIDTH_MULTIPLIER * immediateActiveWs);
activeWsWidth = (activeWorkspaceWidth - (workspaceDiameter * 1.5)) * widthPercentage
cr.arc(leftX, height / 2, workspaceRadius, 0, Math.PI * 2); // Should be 0.5 * Math.PI, 1.5 * Math.PI in theory but it leaves a weird 1px gap
cr.fill();
cr.rectangle(leftX, height / 2 - workspaceRadius, activeWsWidth, workspaceDiameter);
cr.fill();
cr.arc(leftX + activeWsWidth, height / 2, workspaceRadius, 0, Math.PI * 2);
cr.fill();
}
}))
,
})
}
export default () => EventBox({
onScrollUp: () => Hyprland.messageAsync(`dispatch workspace -1`).catch(print),
onScrollDown: () => Hyprland.messageAsync(`dispatch workspace +1`).catch(print),
onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
onSecondaryClick: () => App.toggleWindow('overview'),
attribute: {
clicked: false,
ws_group: 0,
},
child: Box({
homogeneous: true,
// className: 'bar-group-margin',
children: [Box({
// className: 'bar-group bar-group-standalone bar-group-pad',
css: 'min-width: 2px;',
children: [WorkspaceContents(userOptions.workspaces.shown)],
})]
}),
setup: (self) => {
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
self.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
.catch(print);
})
self.on('button-press-event', (self, event) => {
if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
self.attribute.clicked = true;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
// const wsId = Math.ceil(cursorX * NUM_OF_WORKSPACES_PER_GROUP / widgetWidth) + self.attribute.ws_group * NUM_OF_WORKSPACES_PER_GROUP;
// Hyprland.messageAsync(`dispatch workspace ${wsId}`).catch(print);
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
.catch(print);
})
self.on('button-release-event', (self) => self.attribute.clicked = false);
}
})

View File

@@ -0,0 +1,183 @@
const { GLib, Gdk, Gtk } = imports.gi;
const Lang = imports.lang;
const Cairo = imports.cairo;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Sway from "../../../services/sway.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
const { execAsync, exec } = Utils;
const { Box, DrawingArea, EventBox } = Widget;
const dummyWs = Box({ className: 'bar-ws' }); // Not shown. Only for getting size props
const dummyActiveWs = Box({ className: 'bar-ws bar-ws-active' }); // Not shown. Only for getting size props
const dummyOccupiedWs = Box({ className: 'bar-ws bar-ws-occupied' }); // Not shown. Only for getting size props
const switchToWorkspace = (arg) => Utils.execAsync(`swaymsg workspace ${arg}`).catch(print);
const switchToRelativeWorkspace = (self, num) =>
execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
const WorkspaceContents = (count = 10) => {
return DrawingArea({
css: `transition: 90ms cubic-bezier(0.1, 1, 0, 1);`,
attribute: {
initialized: false,
workspaceMask: 0,
updateMask: (self) => {
if (self.attribute.initialized) return; // We only need this to run once
const workspaces = Sway.workspaces;
let workspaceMask = 0;
// console.log('----------------')
for (let i = 0; i < workspaces.length; i++) {
const ws = workspaces[i];
// console.log(ws.name, ',', ws.num);
if (!Number(ws.name)) return;
const id = Number(ws.name);
if (id <= 0) continue; // Ignore scratchpads
if (id > count) return; // Not rendered
if (workspaces[i].windows > 0) {
workspaceMask |= (1 << id);
}
}
self.attribute.workspaceMask = workspaceMask;
self.attribute.initialized = true;
},
toggleMask: (self, occupied, name) => {
if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
else self.attribute.workspaceMask &= ~(1 << parseInt(name));
},
},
setup: (area) => area
.hook(Sway.active.workspace, (area) => {
area.setCss(`font-size: ${Sway.active.workspace.name}px;`)
})
.hook(Sway, (self) => self.attribute.updateMask(self), 'notify::workspaces')
// .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, true, name), 'workspace-added')
// .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, false, name), 'workspace-removed')
.on('draw', Lang.bind(area, (area, cr) => {
const allocation = area.get_allocation();
const { width, height } = allocation;
const workspaceStyleContext = dummyWs.get_style_context();
const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const workspaceRadius = workspaceDiameter / 2;
const workspaceFontSize = workspaceStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 4 * 3;
const workspaceFontFamily = workspaceStyleContext.get_property('font-family', Gtk.StateFlags.NORMAL);
const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const wsfg = workspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const occupiedfg = occupiedWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const activefg = activeWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
area.set_size_request(workspaceDiameter * count, -1);
const widgetStyleContext = area.get_style_context();
const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
const activeWsCenterX = -(workspaceDiameter / 2) + (workspaceDiameter * activeWs);
const activeWsCenterY = height / 2;
// Font
const layout = PangoCairo.create_layout(cr);
const fontDesc = Pango.font_description_from_string(`${workspaceFontFamily[0]} ${workspaceFontSize}`);
layout.set_font_description(fontDesc);
cr.setAntialias(Cairo.Antialias.BEST);
// Get kinda min radius for number indicators
layout.set_text("0".repeat(count.toString().length), -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const indicatorRadius = Math.max(layoutWidth, layoutHeight) / 2 * 1.2; // a bit smaller than sqrt(2)*radius
const indicatorGap = workspaceRadius - indicatorRadius;
// Draw workspace numbers
for (let i = 1; i <= count; i++) {
if (area.attribute.workspaceMask & (1 << i)) {
// Draw bg highlight
cr.setSourceRGBA(occupiedbg.red, occupiedbg.green, occupiedbg.blue, occupiedbg.alpha);
const wsCenterX = -(workspaceRadius) + (workspaceDiameter * i);
const wsCenterY = height / 2;
if (!(area.attribute.workspaceMask & (1 << (i - 1)))) { // Left
cr.arc(wsCenterX, wsCenterY, workspaceRadius, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX - workspaceRadius, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
if (!(area.attribute.workspaceMask & (1 << (i + 1)))) { // Right
cr.arc(wsCenterX, wsCenterY, workspaceRadius, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
// Set color for text
cr.setSourceRGBA(occupiedfg.red, occupiedfg.green, occupiedfg.blue, occupiedfg.alpha);
}
else
cr.setSourceRGBA(wsfg.red, wsfg.green, wsfg.blue, wsfg.alpha);
layout.set_text(`${i}`, -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
const y = (height - layoutHeight) / 2;
cr.moveTo(x, y);
// cr.showText(text);
PangoCairo.show_layout(cr, layout);
cr.stroke();
}
// Draw active ws
// base
cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
cr.fill();
// inner decor
cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius * 0.2, 0, 2 * Math.PI);
cr.fill();
}))
,
})
}
export default () => EventBox({
onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
onSecondaryClick: () => App.toggleWindow('overview'),
attribute: { clicked: false },
child: Box({
homogeneous: true,
className: 'bar-group-margin',
children: [Box({
className: 'bar-group bar-group-standalone bar-group-pad',
css: 'min-width: 2px;',
children: [
WorkspaceContents(10),
]
})]
}),
setup: (self) => {
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
self.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
switchToWorkspace(wsId);
})
self.on('button-press-event', (self, event) => {
if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
self.attribute.clicked = true;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
switchToWorkspace(wsId);
})
self.on('button-release-event', (self) => self.attribute.clicked = false);
}
});

View File

@@ -0,0 +1,130 @@
const { Gtk } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
import WindowTitle from "./normal/spaceleft.js";
import Indicators from "./normal/spaceright.js";
import Music from "./normal/music.js";
import System from "./normal/system.js";
import { enableClickthrough } from "../.widgetutils/clickthrough.js";
import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
import { currentShellMode } from '../../variables.js';
const NormalOptionalWorkspaces = async () => {
try {
return (await import('./normal/workspaces_hyprland.js')).default();
} catch {
try {
return (await import('./normal/workspaces_sway.js')).default();
} catch {
return null;
}
}
};
const FocusOptionalWorkspaces = async () => {
try {
return (await import('./focus/workspaces_hyprland.js')).default();
} catch {
try {
return (await import('./focus/workspaces_sway.js')).default();
} catch {
return null;
}
}
};
export const Bar = async (monitor = 0) => {
const SideModule = (children) => Widget.Box({
className: 'bar-sidemodule',
children: children,
});
const normalBarContent = Widget.CenterBox({
className: 'bar-bg',
setup: (self) => {
const styleContext = self.get_style_context();
const minHeight = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
// execAsync(['bash', '-c', `hyprctl keyword monitor ,addreserved,${minHeight},0,0,0`]).catch(print);
},
startWidget: (await WindowTitle(monitor)),
centerWidget: Widget.Box({
className: 'spacing-h-4',
children: [
SideModule([Music()]),
Widget.Box({
homogeneous: true,
children: [await NormalOptionalWorkspaces()],
}),
SideModule([System()]),
]
}),
endWidget: Indicators(monitor),
});
const focusedBarContent = Widget.CenterBox({
className: 'bar-bg-focus',
startWidget: Widget.Box({}),
centerWidget: Widget.Box({
className: 'spacing-h-4',
children: [
SideModule([]),
Widget.Box({
homogeneous: true,
children: [await FocusOptionalWorkspaces()],
}),
SideModule([]),
]
}),
endWidget: Widget.Box({}),
setup: (self) => {
self.hook(Battery, (self) => {
if (!Battery.available) return;
self.toggleClassName('bar-bg-focus-batterylow', Battery.percent <= userOptions.battery.low);
})
}
});
const nothingContent = Widget.Box({
className: 'bar-bg-nothing',
})
return Widget.Window({
monitor,
name: `bar${monitor}`,
anchor: ['top', 'left', 'right'],
exclusivity: 'exclusive',
visible: true,
child: Widget.Stack({
homogeneous: false,
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationLarge,
children: {
'normal': normalBarContent,
'focus': focusedBarContent,
'nothing': nothingContent,
},
setup: (self) => self.hook(currentShellMode, (self) => {
self.shown = currentShellMode.value;
})
}),
});
}
export const BarCornerTopleft = (monitor = 0) => Widget.Window({
monitor,
name: `barcornertl${monitor}`,
layer: 'top',
anchor: ['top', 'left'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('topleft', { className: 'corner', }),
setup: enableClickthrough,
});
export const BarCornerTopright = (monitor = 0) => Widget.Window({
monitor,
name: `barcornertr${monitor}`,
layer: 'top',
anchor: ['top', 'right'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('topright', { className: 'corner', }),
setup: enableClickthrough,
});

View File

@@ -0,0 +1,230 @@
const { GLib } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
const { Box, Button, EventBox, Label, Overlay, Revealer, Scrollable } = Widget;
const { execAsync, exec } = Utils;
import { AnimatedCircProg } from "../../.commonwidgets/cairo_circularprogress.js";
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { showMusicControls } from '../../../variables.js';
const CUSTOM_MODULE_CONTENT_INTERVAL_FILE = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-interval.txt`;
const CUSTOM_MODULE_CONTENT_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-poll.sh`;
const CUSTOM_MODULE_LEFTCLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-leftclick.sh`;
const CUSTOM_MODULE_RIGHTCLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-rightclick.sh`;
const CUSTOM_MODULE_MIDDLECLICK_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-middleclick.sh`;
const CUSTOM_MODULE_SCROLLUP_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-scrollup.sh`;
const CUSTOM_MODULE_SCROLLDOWN_SCRIPT = `${GLib.get_user_cache_dir()}/ags/user/scripts/custom-module-scrolldown.sh`;
function trimTrackTitle(title) {
if (!title) return '';
const cleanPatterns = [
/【[^】]*】/, // Touhou n weeb stuff
" [FREE DOWNLOAD]", // F-777
];
cleanPatterns.forEach((expr) => title = title.replace(expr, ''));
return title;
}
const BarGroup = ({ child }) => Box({
className: 'bar-group-margin bar-sides',
children: [
Box({
className: 'bar-group bar-group-standalone bar-group-pad-system',
children: [child],
}),
]
});
const BarResource = (name, icon, command, circprogClassName = 'bar-batt-circprog', textClassName = 'txt-onSurfaceVariant', iconClassName = 'bar-batt') => {
const resourceCircProg = AnimatedCircProg({
className: `${circprogClassName}`,
vpack: 'center',
hpack: 'center',
});
const resourceProgress = Box({
homogeneous: true,
children: [Overlay({
child: Box({
vpack: 'center',
className: `${iconClassName}`,
homogeneous: true,
children: [
MaterialIcon(icon, 'small'),
],
}),
overlays: [resourceCircProg]
})]
});
const resourceLabel = Label({
className: `txt-smallie ${textClassName}`,
});
const widget = Button({
onClicked: () => Utils.execAsync(['bash', '-c', `${userOptions.apps.taskManager}`]).catch(print),
child: Box({
className: `spacing-h-4 ${textClassName}`,
children: [
resourceProgress,
resourceLabel,
],
setup: (self) => self.poll(5000, () => execAsync(['bash', '-c', command])
.then((output) => {
resourceCircProg.css = `font-size: ${Number(output)}px;`;
resourceLabel.label = `${Math.round(Number(output))}%`;
widget.tooltipText = `${name}: ${Math.round(Number(output))}%`;
}).catch(print))
,
})
});
return widget;
}
const TrackProgress = () => {
const _updateProgress = (circprog) => {
const mpris = Mpris.getPlayer('');
if (!mpris) return;
// Set circular progress value
circprog.css = `font-size: ${Math.max(mpris.position / mpris.length * 100, 0)}px;`
}
return AnimatedCircProg({
className: 'bar-music-circprog',
vpack: 'center', hpack: 'center',
extraSetup: (self) => self
.hook(Mpris, _updateProgress)
.poll(3000, _updateProgress)
,
})
}
const switchToRelativeWorkspace = async (self, num) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
Hyprland.messageAsync(`dispatch workspace ${num > 0 ? '+' : ''}${num}`).catch(print);
} catch {
execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
}
}
export default () => {
// TODO: use cairo to make button bounce smaller on click, if that's possible
const playingState = Box({ // Wrap a box cuz overlay can't have margins itself
homogeneous: true,
children: [Overlay({
child: Box({
vpack: 'center',
className: 'bar-music-playstate',
homogeneous: true,
children: [Label({
vpack: 'center',
className: 'bar-music-playstate-txt',
justification: 'center',
setup: (self) => self.hook(Mpris, label => {
const mpris = Mpris.getPlayer('');
label.label = `${mpris !== null && mpris.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
}),
})],
setup: (self) => self.hook(Mpris, label => {
const mpris = Mpris.getPlayer('');
if (!mpris) return;
label.toggleClassName('bar-music-playstate-playing', mpris !== null && mpris.playBackStatus == 'Playing');
label.toggleClassName('bar-music-playstate', mpris !== null || mpris.playBackStatus == 'Paused');
}),
}),
overlays: [
TrackProgress(),
]
})]
});
const trackTitle = Label({
hexpand: true,
className: 'txt-smallie bar-music-txt',
truncate: 'end',
maxWidthChars: 1, // Doesn't matter, just needs to be non negative
setup: (self) => self.hook(Mpris, label => {
const mpris = Mpris.getPlayer('');
if (mpris)
label.label = `${trimTrackTitle(mpris.trackTitle)}${mpris.trackArtists.join(', ')}`;
else
label.label = 'No media';
}),
})
const musicStuff = Box({
className: 'spacing-h-10',
hexpand: true,
children: [
playingState,
trackTitle,
]
})
const SystemResourcesOrCustomModule = () => {
// Check if $XDG_CACHE_HOME/ags/user/scripts/custom-module-poll.sh exists
if (GLib.file_test(CUSTOM_MODULE_CONTENT_SCRIPT, GLib.FileTest.EXISTS)) {
const interval = Number(Utils.readFile(CUSTOM_MODULE_CONTENT_INTERVAL_FILE)) || 5000;
return BarGroup({
child: Button({
child: Label({
className: 'txt-smallie txt-onSurfaceVariant',
useMarkup: true,
setup: (self) => Utils.timeout(1, () => {
self.label = exec(CUSTOM_MODULE_CONTENT_SCRIPT);
self.poll(interval, (self) => {
const content = exec(CUSTOM_MODULE_CONTENT_SCRIPT);
self.label = content;
})
})
}),
onPrimaryClickRelease: () => execAsync(CUSTOM_MODULE_LEFTCLICK_SCRIPT).catch(print),
onSecondaryClickRelease: () => execAsync(CUSTOM_MODULE_RIGHTCLICK_SCRIPT).catch(print),
onMiddleClickRelease: () => execAsync(CUSTOM_MODULE_MIDDLECLICK_SCRIPT).catch(print),
onScrollUp: () => execAsync(CUSTOM_MODULE_SCROLLUP_SCRIPT).catch(print),
onScrollDown: () => execAsync(CUSTOM_MODULE_SCROLLDOWN_SCRIPT).catch(print),
})
});
} else return BarGroup({
child: Box({
children: [
BarResource('RAM Usage', 'memory', `LANG=C free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
'bar-ram-circprog', 'bar-ram-txt', 'bar-ram-icon'),
Revealer({
revealChild: true,
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
className: 'spacing-h-10 margin-left-10',
children: [
BarResource('Swap Usage', 'swap_horiz', `LANG=C free | awk '/^Swap/ {if ($2 > 0) printf("%.2f\\n", ($3/$2) * 100); else print "0";}'`,
'bar-swap-circprog', 'bar-swap-txt', 'bar-swap-icon'),
BarResource('CPU Usage', 'settings_motion_mode', `LANG=C top -bn1 | grep Cpu | sed 's/\\,/\\./g' | awk '{print $2}'`,
'bar-cpu-circprog', 'bar-cpu-txt', 'bar-cpu-icon'),
]
}),
setup: (self) => self.hook(Mpris, label => {
const mpris = Mpris.getPlayer('');
self.revealChild = (!mpris);
}),
})
],
})
});
}
return EventBox({
onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
child: Box({
className: 'spacing-h-4',
children: [
SystemResourcesOrCustomModule(),
EventBox({
child: BarGroup({ child: musicStuff }),
onPrimaryClick: () => showMusicControls.setValue(!showMusicControls.value),
onSecondaryClick: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']).catch(print),
onMiddleClick: () => execAsync('playerctl play-pause').catch(print),
setup: (self) => self.on('button-press-event', (self, event) => {
if (event.get_button()[1] === 8) // Side button
execAsync('playerctl previous').catch(print)
}),
})
]
})
});
}

View File

@@ -0,0 +1,78 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Brightness from '../../../services/brightness.js';
import Indicator from '../../../services/indicator.js';
const WindowTitle = async () => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
return Widget.Scrollable({
hexpand: true, vexpand: true,
hscroll: 'automatic', vscroll: 'never',
child: Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
truncate: 'end',
maxWidthChars: 1, // Doesn't matter, just needs to be non negative
className: 'txt-smaller bar-wintitle-topdesc txt',
setup: (self) => self.hook(Hyprland.active.client, label => { // Hyprland.active.client
label.label = Hyprland.active.client.class.length === 0 ? 'Desktop' : Hyprland.active.client.class;
}),
}),
Widget.Label({
xalign: 0,
truncate: 'end',
maxWidthChars: 1, // Doesn't matter, just needs to be non negative
className: 'txt-smallie bar-wintitle-txt',
setup: (self) => self.hook(Hyprland.active.client, label => { // Hyprland.active.client
label.label = Hyprland.active.client.title.length === 0 ? `Workspace ${Hyprland.active.workspace.id}` : Hyprland.active.client.title;
}),
})
]
})
});
} catch {
return null;
}
}
export default async (monitor = 0) => {
const optionalWindowTitleInstance = await WindowTitle();
return Widget.EventBox({
onScrollUp: () => {
Indicator.popup(1); // Since the brightness and speaker are both on the same window
Brightness[monitor].screen_value += 0.05;
},
onScrollDown: () => {
Indicator.popup(1); // Since the brightness and speaker are both on the same window
Brightness[monitor].screen_value -= 0.05;
},
onPrimaryClick: () => {
App.toggleWindow('sideleft');
},
child: Widget.Box({
homogeneous: false,
children: [
Widget.Box({ className: 'bar-corner-spacing' }),
Widget.Overlay({
overlays: [
Widget.Box({ hexpand: true }),
Widget.Box({
className: 'bar-sidemodule', hexpand: true,
children: [Widget.Box({
vertical: true,
className: 'bar-space-button',
children: [
optionalWindowTitleInstance,
]
})]
}),
]
})
]
})
});
}

View File

@@ -0,0 +1,91 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
const { execAsync } = Utils;
import Indicator from '../../../services/indicator.js';
import { StatusIcons } from '../../.commonwidgets/statusicons.js';
import { Tray } from "./tray.js";
const SeparatorDot = () => Widget.Revealer({
transition: 'slide_left',
revealChild: false,
attribute: {
'count': SystemTray.items.length,
'update': (self, diff) => {
self.attribute.count += diff;
self.revealChild = (self.attribute.count > 0);
}
},
child: Widget.Box({
vpack: 'center',
className: 'separator-circle',
}),
setup: (self) => self
.hook(SystemTray, (self) => self.attribute.update(self, 1), 'added')
.hook(SystemTray, (self) => self.attribute.update(self, -1), 'removed')
,
});
export default (monitor = 0) => {
const barTray = Tray();
const barStatusIcons = StatusIcons({
className: 'bar-statusicons',
setup: (self) => self.hook(App, (self, currentName, visible) => {
if (currentName === 'sideright') {
self.toggleClassName('bar-statusicons-active', visible);
}
}),
}, monitor);
const SpaceRightDefaultClicks = (child) => Widget.EventBox({
onHover: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', true) },
onHoverLost: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', false) },
onPrimaryClick: () => App.toggleWindow('sideright'),
onSecondaryClick: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']).catch(print),
onMiddleClick: () => execAsync('playerctl play-pause').catch(print),
setup: (self) => self.on('button-press-event', (self, event) => {
if (event.get_button()[1] === 8)
execAsync('playerctl previous').catch(print)
}),
child: child,
});
const emptyArea = SpaceRightDefaultClicks(Widget.Box({ hexpand: true, }));
const indicatorArea = SpaceRightDefaultClicks(Widget.Box({
children: [
SeparatorDot(),
barStatusIcons
],
}));
const actualContent = Widget.Box({
hexpand: true,
className: 'spacing-h-5 bar-spaceright',
children: [
emptyArea,
barTray,
indicatorArea
],
});
return Widget.EventBox({
onScrollUp: () => {
if (!Audio.speaker) return;
if (Audio.speaker.volume <= 0.09) Audio.speaker.volume += 0.01;
else Audio.speaker.volume += 0.03;
Indicator.popup(1);
},
onScrollDown: () => {
if (!Audio.speaker) return;
if (Audio.speaker.volume <= 0.09) Audio.speaker.volume -= 0.01;
else Audio.speaker.volume -= 0.03;
Indicator.popup(1);
},
child: Widget.Box({
children: [
actualContent,
SpaceRightDefaultClicks(Widget.Box({ className: 'bar-corner-spacing' })),
]
})
});
}

View File

@@ -0,0 +1,236 @@
// This is for the right pills of the bar.
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Label, Button, Overlay, Revealer, Scrollable, Stack, EventBox } = Widget;
const { exec, execAsync } = Utils;
const { GLib } = imports.gi;
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { AnimatedCircProg } from "../../.commonwidgets/cairo_circularprogress.js";
import { WWO_CODE, WEATHER_SYMBOL, NIGHT_WEATHER_SYMBOL } from '../../.commondata/weather.js';
const WEATHER_CACHE_FOLDER = `${GLib.get_user_cache_dir()}/ags/weather`;
Utils.exec(`mkdir -p ${WEATHER_CACHE_FOLDER}`);
const BarBatteryProgress = () => {
const _updateProgress = (circprog) => { // Set circular progress value
circprog.css = `font-size: ${Math.abs(Battery.percent)}px;`
circprog.toggleClassName('bar-batt-circprog-low', Battery.percent <= userOptions.battery.low);
circprog.toggleClassName('bar-batt-circprog-full', Battery.charged);
}
return AnimatedCircProg({
className: 'bar-batt-circprog',
vpack: 'center', hpack: 'center',
extraSetup: (self) => self
.hook(Battery, _updateProgress)
,
})
}
const time = Variable('', {
poll: [
userOptions.time.interval,
() => GLib.DateTime.new_now_local().format(userOptions.time.format),
],
})
const date = Variable('', {
poll: [
userOptions.time.dateInterval,
() => GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong),
],
})
const BarClock = () => Widget.Box({
vpack: 'center',
className: 'spacing-h-4 bar-clock-box',
children: [
Widget.Label({
className: 'bar-time',
label: time.bind(),
}),
Widget.Label({
className: 'txt-norm txt-onLayer1',
label: '•',
}),
Widget.Label({
className: 'txt-smallie bar-date',
label: date.bind(),
}),
],
});
const UtilButton = ({ name, icon, onClicked }) => Button({
vpack: 'center',
tooltipText: name,
onClicked: onClicked,
className: 'bar-util-btn icon-material txt-norm',
label: `${icon}`,
})
const Utilities = () => Box({
hpack: 'center',
className: 'spacing-h-4',
children: [
UtilButton({
name: 'Screen snip', icon: 'screenshot_region', onClicked: () => {
Utils.execAsync(`${App.configDir}/scripts/grimblast.sh copy area`)
.catch(print)
}
}),
UtilButton({
name: 'Color picker', icon: 'colorize', onClicked: () => {
Utils.execAsync(['hyprpicker', '-a']).catch(print)
}
}),
UtilButton({
name: 'Toggle on-screen keyboard', icon: 'keyboard', onClicked: () => {
toggleWindowOnAllMonitors('osk');
}
}),
]
})
const BarBattery = () => Box({
className: 'spacing-h-4 bar-batt-txt',
children: [
Revealer({
transitionDuration: userOptions.animations.durationSmall,
revealChild: false,
transition: 'slide_right',
child: MaterialIcon('bolt', 'norm', { tooltipText: "Charging" }),
setup: (self) => self.hook(Battery, revealer => {
self.revealChild = Battery.charging;
}),
}),
Label({
className: 'txt-smallie',
setup: (self) => self.hook(Battery, label => {
label.label = `${Number.parseFloat(Battery.percent.toFixed(1))}%`;
}),
}),
Overlay({
child: Widget.Box({
vpack: 'center',
className: 'bar-batt',
homogeneous: true,
children: [
MaterialIcon('battery_full', 'small'),
],
setup: (self) => self.hook(Battery, box => {
box.toggleClassName('bar-batt-low', Battery.percent <= userOptions.battery.low);
box.toggleClassName('bar-batt-full', Battery.charged);
}),
}),
overlays: [
BarBatteryProgress(),
]
}),
]
});
const BarGroup = ({ child }) => Widget.Box({
className: 'bar-group-margin bar-sides',
children: [
Widget.Box({
className: 'bar-group bar-group-standalone bar-group-pad-system',
children: [child],
}),
]
});
const BatteryModule = () => Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationLarge,
children: {
'laptop': Box({
className: 'spacing-h-4', children: [
BarGroup({ child: Utilities() }),
BarGroup({ child: BarBattery() }),
]
}),
'desktop': BarGroup({
child: Box({
hexpand: true,
hpack: 'center',
className: 'spacing-h-4 txt-onSurfaceVariant',
children: [
MaterialIcon('device_thermostat', 'small'),
Label({
label: 'Weather',
})
],
setup: (self) => self.poll(900000, async (self) => {
const WEATHER_CACHE_PATH = WEATHER_CACHE_FOLDER + '/wttr.in.txt';
const updateWeatherForCity = (city) => execAsync(`curl https://wttr.in/${city.replace(/ /g, '%20')}?format=j1`)
.then(output => {
const weather = JSON.parse(output);
Utils.writeFile(JSON.stringify(weather), WEATHER_CACHE_PATH)
.catch(print);
const weatherCode = weather.current_condition[0].weatherCode;
const weatherDesc = weather.current_condition[0].weatherDesc[0].value;
const temperature = weather.current_condition[0][`temp_${userOptions.weather.preferredUnit}`];
const feelsLike = weather.current_condition[0][`FeelsLike${userOptions.weather.preferredUnit}`];
const weatherSymbol = WEATHER_SYMBOL[WWO_CODE[weatherCode]];
self.children[0].label = weatherSymbol;
self.children[1].label = `${temperature}°${userOptions.weather.preferredUnit} • Feels like ${feelsLike}°${userOptions.weather.preferredUnit}`;
self.tooltipText = weatherDesc;
}).catch((err) => {
try { // Read from cache
const weather = JSON.parse(
Utils.readFile(WEATHER_CACHE_PATH)
);
const weatherCode = weather.current_condition[0].weatherCode;
const weatherDesc = weather.current_condition[0].weatherDesc[0].value;
const temperature = weather.current_condition[0][`temp_${userOptions.weather.preferredUnit}`];
const feelsLike = weather.current_condition[0][`FeelsLike${userOptions.weather.preferredUnit}`];
const weatherSymbol = WEATHER_SYMBOL[WWO_CODE[weatherCode]];
self.children[0].label = weatherSymbol;
self.children[1].label = `${temperature}°${userOptions.weather.preferredUnit} • Feels like ${feelsLike}°${userOptions.weather.preferredUnit}`;
self.tooltipText = weatherDesc;
} catch (err) {
print(err);
}
});
if (userOptions.weather.city != '' && userOptions.weather.city != null) {
updateWeatherForCity(userOptions.weather.city.replace(/ /g, '%20'));
}
else {
Utils.execAsync('curl ipinfo.io')
.then(output => {
return JSON.parse(output)['city'].toLowerCase();
})
.then(updateWeatherForCity)
.catch(print)
}
}),
})
}),
},
setup: (stack) => Utils.timeout(10, () => {
if (!Battery.available) stack.shown = 'desktop';
else stack.shown = 'laptop';
})
})
const switchToRelativeWorkspace = async (self, num) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
Hyprland.messageAsync(`dispatch workspace ${num > 0 ? '+' : ''}${num}`).catch(print);
} catch {
execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
}
}
export default () => Widget.EventBox({
onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
onPrimaryClick: () => App.toggleWindow('sideright'),
child: Widget.Box({
className: 'spacing-h-4',
children: [
BarGroup({ child: BarClock() }),
BatteryModule(),
]
})
});

View File

@@ -0,0 +1,36 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
const { Box, Icon, Button, Revealer } = Widget;
const { Gravity } = imports.gi.Gdk;
const SysTrayItem = (item) => item.id !== null ? Button({
className: 'bar-systray-item',
child: Icon({ hpack: 'center' }).bind('icon', item, 'icon'),
setup: (self) => self
.hook(item, (self) => self.tooltipMarkup = item['tooltip-markup'])
,
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (btn, event) => item.menu.popup_at_widget(btn, Gravity.SOUTH, Gravity.NORTH, null),
}) : null;
export const Tray = (props = {}) => {
const trayContent = Box({
className: 'margin-right-5 spacing-h-15',
setup: (self) => self
.hook(SystemTray, (self) => {
self.children = SystemTray.items.map(SysTrayItem);
self.show_all();
})
,
});
const trayRevealer = Widget.Revealer({
revealChild: true,
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
child: trayContent,
});
return Box({
...props,
children: [trayRevealer],
});
}

View File

@@ -0,0 +1,224 @@
const { GLib, Gdk, Gtk } = imports.gi;
const Lang = imports.lang;
const Cairo = imports.cairo;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
import App from 'resource:///com/github/Aylur/ags/app.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, DrawingArea, EventBox } = Widget;
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const dummyWs = Box({ className: 'bar-ws' }); // Not shown. Only for getting size props
const dummyActiveWs = Box({ className: 'bar-ws bar-ws-active' }); // Not shown. Only for getting size props
const dummyOccupiedWs = Box({ className: 'bar-ws bar-ws-occupied' }); // Not shown. Only for getting size props
const mix = (value1, value2, perc) => {
return value1 * perc + value2 * (1 - perc);
}
const getFontWeightName = (weight) => {
switch (weight) {
case Pango.Weight.ULTRA_LIGHT:
return 'UltraLight';
case Pango.Weight.LIGHT:
return 'Light';
case Pango.Weight.NORMAL:
return 'Normal';
case Pango.Weight.BOLD:
return 'Bold';
case Pango.Weight.ULTRA_BOLD:
return 'UltraBold';
case Pango.Weight.HEAVY:
return 'Heavy';
default:
return 'Normal';
}
}
// Font size = workspace id
const WorkspaceContents = (count = 10) => {
return DrawingArea({
className: 'bar-ws-container',
attribute: {
initialized: false,
workspaceMask: 0,
workspaceGroup: 0,
updateMask: (self) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
// if (self.attribute.initialized) return; // We only need this to run once
const workspaces = Hyprland.workspaces;
let workspaceMask = 0;
for (let i = 0; i < workspaces.length; i++) {
const ws = workspaces[i];
if (ws.id <= offset || ws.id > offset + count) continue; // Out of range, ignore
if (workspaces[i].windows > 0)
workspaceMask |= (1 << (ws.id - offset));
}
// console.log('Mask:', workspaceMask.toString(2));
self.attribute.workspaceMask = workspaceMask;
// self.attribute.initialized = true;
self.queue_draw();
},
toggleMask: (self, occupied, name) => {
if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
else self.attribute.workspaceMask &= ~(1 << parseInt(name));
self.queue_draw();
},
},
setup: (area) => area
.hook(Hyprland.active.workspace, (self) => {
self.setCss(`font-size: ${(Hyprland.active.workspace.id - 1) % count + 1}px;`);
const previousGroup = self.attribute.workspaceGroup;
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / count);
if (currentGroup !== previousGroup) {
self.attribute.updateMask(self);
self.attribute.workspaceGroup = currentGroup;
}
})
.hook(Hyprland, (self) => self.attribute.updateMask(self), 'notify::workspaces')
.on('draw', Lang.bind(area, (area, cr) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / count) * userOptions.workspaces.shown;
const allocation = area.get_allocation();
const { width, height } = allocation;
const workspaceStyleContext = dummyWs.get_style_context();
const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const workspaceRadius = workspaceDiameter / 2;
const workspaceFontSize = workspaceStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 4 * 3;
const workspaceFontFamily = workspaceStyleContext.get_property('font-family', Gtk.StateFlags.NORMAL);
const workspaceFontWeight = workspaceStyleContext.get_property('font-weight', Gtk.StateFlags.NORMAL);
const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const wsfg = workspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const occupiedfg = occupiedWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const activefg = activeWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
area.set_size_request(workspaceDiameter * count, -1);
const widgetStyleContext = area.get_style_context();
const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
const activeWsCenterX = -(workspaceDiameter / 2) + (workspaceDiameter * activeWs);
const activeWsCenterY = height / 2;
// Font
const layout = PangoCairo.create_layout(cr);
const fontDesc = Pango.font_description_from_string(`${workspaceFontFamily[0]} ${getFontWeightName(workspaceFontWeight)} ${workspaceFontSize}`);
layout.set_font_description(fontDesc);
cr.setAntialias(Cairo.Antialias.BEST);
// Get kinda min radius for number indicators
layout.set_text("0".repeat(count.toString().length), -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const indicatorRadius = Math.max(layoutWidth, layoutHeight) / 2 * 1.15; // smaller than sqrt(2)*radius
const indicatorGap = workspaceRadius - indicatorRadius;
for (let i = 1; i <= count; i++) {
if (area.attribute.workspaceMask & (1 << i)) {
// Draw bg highlight
cr.setSourceRGBA(occupiedbg.red, occupiedbg.green, occupiedbg.blue, occupiedbg.alpha);
const wsCenterX = -(workspaceRadius) + (workspaceDiameter * i);
const wsCenterY = height / 2;
if (!(area.attribute.workspaceMask & (1 << (i - 1)))) { // Left
cr.arc(wsCenterX, wsCenterY, workspaceRadius, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX - workspaceRadius, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
if (!(area.attribute.workspaceMask & (1 << (i + 1)))) { // Right
cr.arc(wsCenterX, wsCenterY, workspaceRadius, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
}
}
// Draw active ws
cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
cr.fill();
// Draw workspace numbers
for (let i = 1; i <= count; i++) {
const inactivecolors = area.attribute.workspaceMask & (1 << i) ? occupiedfg : wsfg;
if (i == activeWs) {
cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
}
// Moving to
else if ((i == Math.floor(activeWs) && Hyprland.active.workspace.id < activeWs) || (i == Math.ceil(activeWs) && Hyprland.active.workspace.id > activeWs)) {
cr.setSourceRGBA(mix(activefg.red, inactivecolors.red, 1 - Math.abs(activeWs - i)), mix(activefg.green, inactivecolors.green, 1 - Math.abs(activeWs - i)), mix(activefg.blue, inactivecolors.blue, 1 - Math.abs(activeWs - i)), activefg.alpha);
}
// Moving from
else if ((i == Math.floor(activeWs) && Hyprland.active.workspace.id > activeWs) || (i == Math.ceil(activeWs) && Hyprland.active.workspace.id < activeWs)) {
cr.setSourceRGBA(mix(activefg.red, inactivecolors.red, 1 - Math.abs(activeWs - i)), mix(activefg.green, inactivecolors.green, 1 - Math.abs(activeWs - i)), mix(activefg.blue, inactivecolors.blue, 1 - Math.abs(activeWs - i)), activefg.alpha);
}
// Inactive
else
cr.setSourceRGBA(inactivecolors.red, inactivecolors.green, inactivecolors.blue, inactivecolors.alpha);
layout.set_text(`${i + offset}`, -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
const y = (height - layoutHeight) / 2;
cr.moveTo(x, y);
PangoCairo.show_layout(cr, layout);
cr.stroke();
}
}))
,
})
}
export default () => EventBox({
onScrollUp: () => Hyprland.messageAsync(`dispatch workspace -1`).catch(print),
onScrollDown: () => Hyprland.messageAsync(`dispatch workspace +1`).catch(print),
onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
onSecondaryClick: () => App.toggleWindow('overview'),
attribute: {
clicked: false,
ws_group: 0,
},
child: Box({
homogeneous: true,
className: 'bar-group-margin',
children: [Box({
className: 'bar-group bar-group-standalone bar-group-pad',
css: 'min-width: 2px;',
children: [WorkspaceContents(userOptions.workspaces.shown)],
})]
}),
setup: (self) => {
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
self.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
.catch(print);
})
self.on('button-press-event', (self, event) => {
if (event.get_button()[1] === 1) {
self.attribute.clicked = true;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
Utils.execAsync([`${App.configDir}/scripts/hyprland/workspace_action.sh`, 'workspace', `${wsId}`])
.catch(print);
}
else if (event.get_button()[1] === 8) {
Hyprland.messageAsync(`dispatch togglespecialworkspace`).catch(print);
}
})
self.on('button-release-event', (self) => self.attribute.clicked = false);
}
})

View File

@@ -0,0 +1,183 @@
const { GLib, Gdk, Gtk } = imports.gi;
const Lang = imports.lang;
const Cairo = imports.cairo;
const Pango = imports.gi.Pango;
const PangoCairo = imports.gi.PangoCairo;
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import Sway from "../../../services/sway.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
const { execAsync, exec } = Utils;
const { Box, DrawingArea, EventBox } = Widget;
const dummyWs = Box({ className: 'bar-ws' }); // Not shown. Only for getting size props
const dummyActiveWs = Box({ className: 'bar-ws bar-ws-active' }); // Not shown. Only for getting size props
const dummyOccupiedWs = Box({ className: 'bar-ws bar-ws-occupied' }); // Not shown. Only for getting size props
const switchToWorkspace = (arg) => Utils.execAsync(`swaymsg workspace ${arg}`).catch(print);
const switchToRelativeWorkspace = (self, num) =>
execAsync([`${App.configDir}/scripts/sway/swayToRelativeWs.sh`, `${num}`]).catch(print);
const WorkspaceContents = (count = 10) => {
return DrawingArea({
css: `transition: 90ms cubic-bezier(0.1, 1, 0, 1);`,
attribute: {
initialized: false,
workspaceMask: 0,
updateMask: (self) => {
if (self.attribute.initialized) return; // We only need this to run once
const workspaces = Sway.workspaces;
let workspaceMask = 0;
// console.log('----------------')
for (let i = 0; i < workspaces.length; i++) {
const ws = workspaces[i];
// console.log(ws.name, ',', ws.num);
if (!Number(ws.name)) return;
const id = Number(ws.name);
if (id <= 0) continue; // Ignore scratchpads
if (id > count) return; // Not rendered
if (workspaces[i].windows > 0) {
workspaceMask |= (1 << id);
}
}
self.attribute.workspaceMask = workspaceMask;
self.attribute.initialized = true;
},
toggleMask: (self, occupied, name) => {
if (occupied) self.attribute.workspaceMask |= (1 << parseInt(name));
else self.attribute.workspaceMask &= ~(1 << parseInt(name));
},
},
setup: (area) => area
.hook(Sway.active.workspace, (area) => {
area.setCss(`font-size: ${Sway.active.workspace.name}px;`)
})
.hook(Sway, (self) => self.attribute.updateMask(self), 'notify::workspaces')
// .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, true, name), 'workspace-added')
// .hook(Hyprland, (self, name) => self.attribute.toggleMask(self, false, name), 'workspace-removed')
.on('draw', Lang.bind(area, (area, cr) => {
const allocation = area.get_allocation();
const { width, height } = allocation;
const workspaceStyleContext = dummyWs.get_style_context();
const workspaceDiameter = workspaceStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const workspaceRadius = workspaceDiameter / 2;
const workspaceFontSize = workspaceStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 4 * 3;
const workspaceFontFamily = workspaceStyleContext.get_property('font-family', Gtk.StateFlags.NORMAL);
const wsbg = workspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const wsfg = workspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const occupiedWorkspaceStyleContext = dummyOccupiedWs.get_style_context();
const occupiedbg = occupiedWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const occupiedfg = occupiedWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
const activeWorkspaceStyleContext = dummyActiveWs.get_style_context();
const activebg = activeWorkspaceStyleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const activefg = activeWorkspaceStyleContext.get_property('color', Gtk.StateFlags.NORMAL);
area.set_size_request(workspaceDiameter * count, -1);
const widgetStyleContext = area.get_style_context();
const activeWs = widgetStyleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
const activeWsCenterX = -(workspaceDiameter / 2) + (workspaceDiameter * activeWs);
const activeWsCenterY = height / 2;
// Font
const layout = PangoCairo.create_layout(cr);
const fontDesc = Pango.font_description_from_string(`${workspaceFontFamily[0]} ${workspaceFontSize}`);
layout.set_font_description(fontDesc);
cr.setAntialias(Cairo.Antialias.BEST);
// Get kinda min radius for number indicators
layout.set_text("0".repeat(count.toString().length), -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const indicatorRadius = Math.max(layoutWidth, layoutHeight) / 2 * 1.2; // a bit smaller than sqrt(2)*radius
const indicatorGap = workspaceRadius - indicatorRadius;
// Draw workspace numbers
for (let i = 1; i <= count; i++) {
if (area.attribute.workspaceMask & (1 << i)) {
// Draw bg highlight
cr.setSourceRGBA(occupiedbg.red, occupiedbg.green, occupiedbg.blue, occupiedbg.alpha);
const wsCenterX = -(workspaceRadius) + (workspaceDiameter * i);
const wsCenterY = height / 2;
if (!(area.attribute.workspaceMask & (1 << (i - 1)))) { // Left
cr.arc(wsCenterX, wsCenterY, workspaceRadius, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX - workspaceRadius, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
if (!(area.attribute.workspaceMask & (1 << (i + 1)))) { // Right
cr.arc(wsCenterX, wsCenterY, workspaceRadius, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
else {
cr.rectangle(wsCenterX, wsCenterY - workspaceRadius, workspaceRadius, workspaceRadius * 2)
cr.fill();
}
// Set color for text
cr.setSourceRGBA(occupiedfg.red, occupiedfg.green, occupiedfg.blue, occupiedfg.alpha);
}
else
cr.setSourceRGBA(wsfg.red, wsfg.green, wsfg.blue, wsfg.alpha);
layout.set_text(`${i}`, -1);
const [layoutWidth, layoutHeight] = layout.get_pixel_size();
const x = -workspaceRadius + (workspaceDiameter * i) - (layoutWidth / 2);
const y = (height - layoutHeight) / 2;
cr.moveTo(x, y);
// cr.showText(text);
PangoCairo.show_layout(cr, layout);
cr.stroke();
}
// Draw active ws
// base
cr.setSourceRGBA(activebg.red, activebg.green, activebg.blue, activebg.alpha);
cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius, 0, 2 * Math.PI);
cr.fill();
// inner decor
cr.setSourceRGBA(activefg.red, activefg.green, activefg.blue, activefg.alpha);
cr.arc(activeWsCenterX, activeWsCenterY, indicatorRadius * 0.2, 0, 2 * Math.PI);
cr.fill();
}))
,
})
}
export default () => EventBox({
onScrollUp: (self) => switchToRelativeWorkspace(self, -1),
onScrollDown: (self) => switchToRelativeWorkspace(self, +1),
onMiddleClick: () => toggleWindowOnAllMonitors('osk'),
onSecondaryClick: () => App.toggleWindow('overview'),
attribute: { clicked: false },
child: Box({
homogeneous: true,
className: 'bar-group-margin',
children: [Box({
className: 'bar-group bar-group-standalone bar-group-pad',
css: 'min-width: 2px;',
children: [
WorkspaceContents(10),
]
})]
}),
setup: (self) => {
self.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
self.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
switchToWorkspace(wsId);
})
self.on('button-press-event', (self, event) => {
if (!(event.get_button()[1] === 1)) return; // We're only interested in left-click here
self.attribute.clicked = true;
const [_, cursorX, cursorY] = event.get_coords();
const widgetWidth = self.get_allocation().width;
const wsId = Math.ceil(cursorX * userOptions.workspaces.shown / widgetWidth);
switchToWorkspace(wsId);
})
self.on('button-release-event', (self) => self.attribute.clicked = false);
}
});

View File

@@ -0,0 +1,122 @@
export const keybindList = [[
{
"icon": "pin_drop",
"name": "Workspaces: navigation",
"binds": [
{ "keys": ["󰖳", "+", "#"], "action": "Go to workspace #" },
{ "keys": ["󰖳", "+", "S"], "action": "Toggle special workspace" },
{ "keys": ["󰖳", "+", "(Scroll ↑↓)"], "action": "Go to workspace -1/+1" },
{ "keys": ["Ctrl", "󰖳", "+", "←"], "action": "Go to workspace on the left" },
{ "keys": ["Ctrl", "󰖳", "+", "→"], "action": "Go to workspace on the right" },
{ "keys": ["󰖳", "+", "PageUp"], "action": "Go to workspace on the left" },
{ "keys": ["󰖳", "+", "PageDown"], "action": "Go to workspace on the right" }
],
"id": 1
},
{
"icon": "overview_key",
"name": "Workspaces: management",
"binds": [
{ "keys": ["󰖳", "Alt", "+", "#"], "action": "Move window to workspace #" },
{ "keys": ["󰖳", "Alt", "+", "S"], "action": "Move window to special workspace" },
{ "keys": ["󰖳", "Alt", "+", "PageUp"], "action": "Move window to workspace on the left" },
{ "keys": ["󰖳", "Alt", "+", "PageDown"], "action": "Move window to workspace on the right" }
],
"id": 2
},
{
"icon": "move_group",
"name": "Windows",
"binds": [
{ "keys": ["󰖳", "+", "←↑→↓"], "action": "Focus window in direction" },
{ "keys": ["󰖳", "Shift", "+", "←↑→↓"], "action": "Swap window in direction" },
{ "keys": ["󰖳", "+", ";"], "action": "Split ratio -" },
{ "keys": ["󰖳", "+", "'"], "action": "Split ratio +" },
{ "keys": ["󰖳", "+", "Lmb"], "action": "Move window" },
{ "keys": ["󰖳", "+", "Rmb"], "action": "Resize window" },
{ "keys": ["󰖳", "Alt", "+", "Space"], "action": "Float window" },
{ "keys": ["󰖳", "+", "F"], "action": "Fullscreen" },
{ "keys": ["󰖳", "Alt", "+", "F"], "action": "Fake fullscreen" }
],
"id": 3
}
],
[
{
"icon": "widgets",
"name": "Widgets (AGS)",
"binds": [
{ "keys": ["󰖳", "OR", "󰖳", "+", "Tab"], "action": "Toggle overview/launcher" },
{ "keys": ["Ctrl", "󰖳", "+", "R"], "action": "Restart AGS" },
{ "keys": ["󰖳", "+", "/"], "action": "Toggle this cheatsheet" },
{ "keys": ["󰖳", "+", "N"], "action": "Toggle system sidebar" },
{ "keys": ["󰖳", "+", "B", "OR", "󰖳", "+", "O"], "action": "Toggle utilities sidebar" },
{ "keys": ["󰖳", "+", "K"], "action": "Toggle virtual keyboard" },
{ "keys": ["Ctrl", "Alt", "+", "Del"], "action": "Power/Session menu" },
{ "keys": ["Esc"], "action": "Exit a window" },
{ "keys": ["rightCtrl"], "action": "Dismiss/close sidebar" },
{ "keys": ["Ctrl", "󰖳", "+", "T"], "action": "Change wallpaper+colorscheme" },
// { "keys": ["󰖳", "+", "B"], "action": "Toggle left sidebar" },
// { "keys": ["󰖳", "+", "N"], "action": "Toggle right sidebar" },
// { "keys": ["󰖳", "+", "G"], "action": "Toggle volume mixer" },
// { "keys": ["󰖳", "+", "M"], "action": "Toggle useless audio visualizer" },
// { "keys": ["(right)Ctrl"], "action": "Dismiss notification & close menus" }
],
"id": 4
},
{
"icon": "construction",
"name": "Utilities",
"binds": [
{ "keys": ["PrtSc"], "action": "Screenshot >> clipboard" },
{ "keys": ["Ctrl", "PrtSc"], "action": "Screenshot >> file + clipboard" },
{ "keys": ["󰖳", "Shift", "+", "S"], "action": "Screen snip >> clipboard" },
{ "keys": ["󰖳", "Shift", "+", "T"], "action": "Image to text >> clipboard" },
{ "keys": ["󰖳", "Shift", "+", "C"], "action": "Color picker" },
{ "keys": ["󰖳", "Alt", "+", "R"], "action": "Record region" },
{ "keys": ["Ctrl", "Alt", "+", "R"], "action": "Record region with sound" },
{ "keys": ["󰖳", "Shift", "Alt", "+", "R"], "action": "Record screen with sound" }
],
"id": 5
},
],
[
{
"icon": "apps",
"name": "Apps",
"binds": [
{ "keys": ["󰖳", "+", "T"], "action": "Launch terminal: foot" },
{ "keys": ["󰖳", "+", "W"], "action": "Launch browser: Firefox" },
{ "keys": ["󰖳", "+", "C"], "action": "Launch editor: vscode" },
{ "keys": ["󰖳", "+", "X"], "action": "Launch editor: GNOME Text Editor" },
{ "keys": ["󰖳", "+", "I"], "action": "Launch settings: GNOME Control center" }
],
"id": 6
},
{
"icon": "keyboard",
"name": "Typing",
"binds": [
{ "keys": ["󰖳", "+", "V"], "action": "Clipboard history >> clipboard" },
{ "keys": ["󰖳", "+", "."], "action": "Emoji picker >> clipboard" },
],
"id": 7
},
{
"icon": "terminal",
"name": "Launcher actions",
"binds": [
{ "keys": [">raw"], "action": "Toggle mouse acceleration" },
{ "keys": [">img"], "action": "Select wallpaper and generate colorscheme" },
{ "keys": [">light"], "action": "Switch to light theme" },
{ "keys": [">dark"], "action": "Switch to dark theme" },
{ "keys": [">badapple"], "action": "Apply black n' white colorscheme" },
{ "keys": [">color"], "action": "Pick acccent color" },
{ "keys": [">todo"], "action": "Type something after that to add a To-do item" },
],
"id": 8
}
]];

View File

@@ -0,0 +1,195 @@
export const periodicTable = [
[
{ name: 'Hydrogen', symbol: 'H', number: 1, weight: 1.01, type: 'nonmetal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Helium', symbol: 'He', number: 2, weight: 4.00, type: 'noblegas' },
],
[
{ name: 'Lithium', symbol: 'Li', number: 3, weight: 6.94, type: 'metal' },
{ name: 'Beryllium', symbol: 'Be', number: 4, weight: 9.01, type: 'metal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Boron', symbol: 'B', number: 5, weight: 10.81, type: 'nonmetal' },
{ name: 'Carbon', symbol: 'C', number: 6, weight: 12.01, type: 'nonmetal' },
{ name: 'Nitrogen', symbol: 'N', number: 7, weight: 14.01, type: 'nonmetal' },
{ name: 'Oxygen', symbol: 'O', number: 8, weight: 16, type: 'nonmetal' },
{ name: 'Fluorine', symbol: 'F', number: 9, weight: 19, type: 'nonmetal' },
{ name: 'Neon', symbol: 'Ne', number: 10, weight: 20.18, type: 'noblegas' },
],
[
{ name: 'Sodium', symbol: 'Na', number: 11, weight: 22.99, type: 'metal' },
{ name: 'Magnesium', symbol: 'Mg', number: 12, weight: 24.31, type: 'metal' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Aluminum', symbol: 'Al', number: 13, weight: 26.98, type: 'metal' },
{ name: 'Silicon', symbol: 'Si', number: 14, weight: 28.09, type: 'nonmetal' },
{ name: 'Phosphorus', symbol: 'P', number: 15, weight: 30.97, type: 'nonmetal' },
{ name: 'Sulfur', symbol: 'S', number: 16, weight: 32.07, type: 'nonmetal' },
{ name: 'Chlorine', symbol: 'Cl', number: 17, weight: 35.45, type: 'nonmetal' },
{ name: 'Argon', symbol: 'Ar', number: 18, weight: 39.95, type: 'noblegas' },
],
[
{ name: 'Kalium', symbol: 'K', number: 19, weight: 39.098, type: 'metal' },
{ name: 'Calcium', symbol: 'Ca', number: 20, weight: 40.078, type: 'metal' },
{ name: 'Scandium', symbol: 'Sc', number: 21, weight: 44.956, type: 'metal' },
{ name: 'Titanium', symbol: 'Ti', number: 22, weight: 47.87, type: 'metal' },
{ name: 'Vanadium', symbol: 'V', number: 23, weight: 50.94, type: 'metal' },
{ name: 'Chromium', symbol: 'Cr', number: 24, weight: 52, type: 'metal', icon: 'chromium-browser' },
{ name: 'Manganese', symbol: 'Mn', number: 25, weight: 54.94, type: 'metal' },
{ name: 'Iron', symbol: 'Fe', number: 26, weight: 55.85, type: 'metal' },
{ name: 'Cobalt', symbol: 'Co', number: 27, weight: 58.93, type: 'metal' },
{ name: 'Nickel', symbol: 'Ni', number: 28, weight: 58.69, type: 'metal' },
{ name: 'Copper', symbol: 'Cu', number: 29, weight: 63.55, type: 'metal' },
{ name: 'Zinc', symbol: 'Zn', number: 30, weight: 65.38, type: 'metal' },
{ name: 'Gallium', symbol: 'Ga', number: 31, weight: 69.72, type: 'metal' },
{ name: 'Germanium', symbol: 'Ge', number: 32, weight: 72.63, type: 'metal' },
{ name: 'Arsenic', symbol: 'As', number: 33, weight: 74.92, type: 'nonmetal' },
{ name: 'Selenium', symbol: 'Se', number: 34, weight: 78.96, type: 'nonmetal' },
{ name: 'Bromine', symbol: 'Br', number: 35, weight: 79.904, type: 'nonmetal' },
{ name: 'Krypton', symbol: 'Kr', number: 36, weight: 83.8, type: 'noblegas' },
],
[
{ name: 'Rubidium', symbol: 'Rb', number: 37, weight: 85.47, type: 'metal' },
{ name: 'Strontium', symbol: 'Sr', number: 38, weight: 87.62, type: 'metal' },
{ name: 'Yttrium', symbol: 'Y', number: 39, weight: 88.91, type: 'metal' },
{ name: 'Zirconium', symbol: 'Zr', number: 40, weight: 91.22, type: 'metal' },
{ name: 'Niobium', symbol: 'Nb', number: 41, weight: 92.91, type: 'metal' },
{ name: 'Molybdenum', symbol: 'Mo', number: 42, weight: 95.94, type: 'metal' },
{ name: 'Technetium', symbol: 'Tc', number: 43, weight: 98, type: 'metal' },
{ name: 'Ruthenium', symbol: 'Ru', number: 44, weight: 101.07, type: 'metal' },
{ name: 'Rhodium', symbol: 'Rh', number: 45, weight: 102.91, type: 'metal' },
{ name: 'Palladium', symbol: 'Pd', number: 46, weight: 106.42, type: 'metal' },
{ name: 'Silver', symbol: 'Ag', number: 47, weight: 107.87, type: 'metal' },
{ name: 'Cadmium', symbol: 'Cd', number: 48, weight: 112.41, type: 'metal' },
{ name: 'Indium', symbol: 'In', number: 49, weight: 114.82, type: 'metal' },
{ name: 'Tin', symbol: 'Sn', number: 50, weight: 118.71, type: 'metal' },
{ name: 'Antimony', symbol: 'Sb', number: 51, weight: 121.76, type: 'metal' },
{ name: 'Tellurium', symbol: 'Te', number: 52, weight: 127.6, type: 'nonmetal' },
{ name: 'Iodine', symbol: 'I', number: 53, weight: 126.9, type: 'nonmetal' },
{ name: 'Xenon', symbol: 'Xe', number: 54, weight: 131.29, type: 'noblegas' },
],
[
{ name: 'Cesium', symbol: 'Cs', number: 55, weight: 132.91, type: 'metal' },
{ name: 'Barium', symbol: 'Ba', number: 56, weight: 137.33, type: 'metal' },
{ name: 'Lanthanum', symbol: 'La', number: 57, weight: 138.91, type: 'lanthanum' },
{ name: 'Hafnium', symbol: 'Hf', number: 72, weight: 178.49, type: 'metal' },
{ name: 'Tantalum', symbol: 'Ta', number: 73, weight: 180.95, type: 'metal' },
{ name: 'Tungsten', symbol: 'W', number: 74, weight: 183.84, type: 'metal' },
{ name: 'Rhenium', symbol: 'Re', number: 75, weight: 186.21, type: 'metal' },
{ name: 'Osmium', symbol: 'Os', number: 76, weight: 190.23, type: 'metal' },
{ name: 'Iridium', symbol: 'Ir', number: 77, weight: 192.22, type: 'metal' },
{ name: 'Platinum', symbol: 'Pt', number: 78, weight: 195.09, type: 'metal' },
{ name: 'Gold', symbol: 'Au', number: 79, weight: 196.97, type: 'metal' },
{ name: 'Mercury', symbol: 'Hg', number: 80, weight: 200.59, type: 'metal' },
{ name: 'Thallium', symbol: 'Tl', number: 81, weight: 204.38, type: 'metal' },
{ name: 'Lead', symbol: 'Pb', number: 82, weight: 207.2, type: 'metal' },
{ name: 'Bismuth', symbol: 'Bi', number: 83, weight: 208.98, type: 'metal' },
{ name: 'Polonium', symbol: 'Po', number: 84, weight: 209, type: 'metal' },
{ name: 'Astatine', symbol: 'At', number: 85, weight: 210, type: 'nonmetal' },
{ name: 'Radon', symbol: 'Rn', number: 86, weight: 222, type: 'noblegas' },
],
[
{ name: 'Francium', symbol: 'Fr', number: 87, weight: 223, type: 'metal' },
{ name: 'Radium', symbol: 'Ra', number: 88, weight: 226, type: 'metal' },
{ name: 'Actinium', symbol: 'Ac', number: 89, weight: 227, type: 'actinium' },
{ name: 'Rutherfordium', symbol: 'Rf', number: 104, weight: 267, type: 'metal' },
{ name: 'Dubnium', symbol: 'Db', number: 105, weight: 268, type: 'metal' },
{ name: 'Seaborgium', symbol: 'Sg', number: 106, weight: 271, type: 'metal' },
{ name: 'Bohrium', symbol: 'Bh', number: 107, weight: 272, type: 'metal' },
{ name: 'Hassium', symbol: 'Hs', number: 108, weight: 277, type: 'metal' },
{ name: 'Meitnerium', symbol: 'Mt', number: 109, weight: 278, type: 'metal' },
{ name: 'Darmstadtium', symbol: 'Ds', number: 110, weight: 281, type: 'metal' },
{ name: 'Roentgenium', symbol: 'Rg', number: 111, weight: 280, type: 'metal' },
{ name: 'Copernicium', symbol: 'Cn', number: 112, weight: 285, type: 'metal' },
{ name: 'Nihonium', symbol: 'Nh', number: 113, weight: 286, type: 'metal' },
{ name: 'Flerovium', symbol: 'Fl', number: 114, weight: 289, type: 'metal' },
{ name: 'Moscovium', symbol: 'Mc', number: 115, weight: 290, type: 'metal' },
{ name: 'Livermorium', symbol: 'Lv', number: 116, weight: 293, type: 'metal' },
{ name: 'Tennessine', symbol: 'Ts', number: 117, weight: 294, type: 'metal' },
{ name: 'Oganesson', symbol: 'Og', number: 118, weight: 294, type: 'noblegas' },
],
]
export const series = [
[
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Cerium', symbol: 'Ce', number: 58, weight: 140.12, type: 'lanthanum' },
{ name: 'Praseodymium', symbol: 'Pr', number: 59, weight: 140.91, type: 'lanthanum' },
{ name: 'Neodymium', symbol: 'Nd', number: 60, weight: 144.24, type: 'lanthanum' },
{ name: 'Promethium', symbol: 'Pm', number: 61, weight: 145, type: 'lanthanum' },
{ name: 'Samarium', symbol: 'Sm', number: 62, weight: 150.36, type: 'lanthanum' },
{ name: 'Europium', symbol: 'Eu', number: 63, weight: 151.96, type: 'lanthanum' },
{ name: 'Gadolinium', symbol: 'Gd', number: 64, weight: 157.25, type: 'lanthanum' },
{ name: 'Terbium', symbol: 'Tb', number: 65, weight: 158.93, type: 'lanthanum' },
{ name: 'Dysprosium', symbol: 'Dy', number: 66, weight: 162.5, type: 'lanthanum' },
{ name: 'Holmium', symbol: 'Ho', number: 67, weight: 164.93, type: 'lanthanum' },
{ name: 'Erbium', symbol: 'Er', number: 68, weight: 167.26, type: 'lanthanum' },
{ name: 'Thulium', symbol: 'Tm', number: 69, weight: 168.93, type: 'lanthanum' },
{ name: 'Ytterbium', symbol: 'Yb', number: 70, weight: 173.04, type: 'lanthanum' },
{ name: 'Lutetium', symbol: 'Lu', number: 71, weight: 174.97, type: 'lanthanum' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
],
[
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
{ name: 'Thorium', symbol: 'Th', number: 90, weight: 232.04, type: 'actinium' },
{ name: 'Protactinium', symbol: 'Pa', number: 91, weight: 231.04, type: 'actinium' },
{ name: 'Uranium', symbol: 'U', number: 92, weight: 238.03, type: 'actinium' },
{ name: 'Neptunium', symbol: 'Np', number: 93, weight: 237, type: 'actinium' },
{ name: 'Plutonium', symbol: 'Pu', number: 94, weight: 244, type: 'actinium' },
{ name: 'Americium', symbol: 'Am', number: 95, weight: 243, type: 'actinium' },
{ name: 'Curium', symbol: 'Cm', number: 96, weight: 247, type: 'actinium' },
{ name: 'Berkelium', symbol: 'Bk', number: 97, weight: 247, type: 'actinium' },
{ name: 'Californium', symbol: 'Cf', number: 98, weight: 251, type: 'actinium' },
{ name: 'Einsteinium', symbol: 'Es', number: 99, weight: 252, type: 'actinium' },
{ name: 'Fermium', symbol: 'Fm', number: 100, weight: 257, type: 'actinium' },
{ name: 'Mendelevium', symbol: 'Md', number: 101, weight: 258, type: 'actinium' },
{ name: 'Nobelium', symbol: 'No', number: 102, weight: 259, type: 'actinium' },
{ name: 'Lawrencium', symbol: 'Lr', number: 103, weight: 262, type: 'actinium' },
{ name: '', symbol: '', number: -1, weight: 0, type: 'empty' },
],
];
export const niceTypes = {
'metal': "Metal",
'nonmetal': "Nonmetal",
'noblegas': "Noble gas",
'lanthanum': "Lanthanum",
'actinium': "Actinium"
}

View File

@@ -0,0 +1,126 @@
const { GLib, Gtk } = imports.gi;
import App from "resource:///com/github/Aylur/ags/app.js";
import * as Utils from "resource:///com/github/Aylur/ags/utils.js";
import Widget from "resource:///com/github/Aylur/ags/widget.js";
import { IconTabContainer } from "../.commonwidgets/tabcontainer.js";
const { Box, Label, Scrollable } = Widget;
const HYPRLAND_KEYBIND_CONFIG_FILE = userOptions.cheatsheet.keybinds.configPath ?
userOptions.cheatsheet.keybinds.configPath : `${GLib.get_user_config_dir()}/hypr/hyprland/keybinds.conf`;
const KEYBIND_SECTIONS_PER_PAGE = 3;
const getKeybindList = () => {
let data = Utils.exec(`${App.configDir}/scripts/hyprland/get_keybinds.py --path ${HYPRLAND_KEYBIND_CONFIG_FILE}`);
if (data == "\"error\"") {
Utils.timeout(2000, () => Utils.execAsync(['notify-send',
'Update path to keybinds',
'Keybinds hyprland config file not found. Check your user options.',
'-a', 'ags',
]).catch(print))
return { children: [] };
}
return JSON.parse(data);
};
const keybindList = getKeybindList();
const keySubstitutions = {
"Super": "󰖳",
"mouse_up": "Scroll ↓", // ikr, weird
"mouse_down": "Scroll ↑", // trust me bro
"mouse:272": "LMB",
"mouse:273": "RMB",
"mouse:275": "MouseBack",
"Slash": "/",
"Hash": "#"
}
const substituteKey = (key) => {
return keySubstitutions[key] || key;
}
const Keybind = (keybindData, type) => { // type: either "keys" or "actions"
const Key = (key) => Label({ // Specific keys
vpack: 'center',
className: `${['OR', '+'].includes(key) ? 'cheatsheet-key-notkey' : 'cheatsheet-key'} txt-small`,
label: substituteKey(key),
});
const Action = (text) => Label({ // Binds
xalign: 0,
label: text,
className: "txt txt-small cheatsheet-action",
})
return Widget.Box({
className: "spacing-h-10 cheatsheet-bind-lineheight",
children: type == "keys" ? [
...(keybindData.mods.length > 0 ? [
...keybindData.mods.map(Key),
Key("+"),
] : []),
Key(keybindData.key),
] : [Action(keybindData.comment)],
})
}
const Section = (sectionData, scope) => {
const keys = Box({
vertical: true,
className: 'spacing-v-5',
children: sectionData.keybinds.map((data) => Keybind(data, "keys"))
})
const actions = Box({
vertical: true,
className: 'spacing-v-5',
children: sectionData.keybinds.map((data) => Keybind(data, "actions"))
})
const name = Label({
xalign: 0,
className: "cheatsheet-category-title txt margin-bottom-10",
label: sectionData.name,
})
const binds = Box({
className: 'spacing-h-10',
children: [
keys,
actions,
]
})
const childrenSections = Box({
vertical: true,
className: 'spacing-v-15',
children: sectionData.children.map((data) => Section(data, scope + 1))
})
return Box({
vertical: true,
children: [
...((sectionData.name && sectionData.name.length > 0) ? [name] : []),
Box({
className: 'spacing-v-10',
children: [
binds,
childrenSections,
]
})
]
})
};
export default () => {
const numOfTabs = Math.ceil(keybindList.children.length / KEYBIND_SECTIONS_PER_PAGE);
const keybindPages = Array.from({ length: numOfTabs }, (_, i) => ({
iconWidget: Label({
className: "txt txt-small",
label: `${i + 1}`,
}),
name: `${i + 1}`,
child: Box({
className: 'spacing-h-30',
children: keybindList.children.slice(
KEYBIND_SECTIONS_PER_PAGE * i, 0 + KEYBIND_SECTIONS_PER_PAGE * (i + 1),
).map(data => Section(data, 1)),
}),
}));
return IconTabContainer({
iconWidgets: keybindPages.map((kbp) => kbp.iconWidget),
names: keybindPages.map((kbp) => kbp.name),
children: keybindPages.map((kbp) => kbp.child),
});
};

View File

@@ -0,0 +1,146 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { setupCursorHover } from "../.widgetutils/cursorhover.js";
import PopupWindow from '../.widgethacks/popupwindow.js';
import Keybinds from "./keybinds.js";
import PeriodicTable from "./periodictable.js";
import { ExpandingIconTabContainer } from '../.commonwidgets/tabcontainer.js';
import { checkKeybind } from '../.widgetutils/keybind.js';
import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
const cheatsheets = [
{
name: 'Keybinds',
materialIcon: 'keyboard',
contentWidget: Keybinds,
},
{
name: 'Periodic table',
materialIcon: 'experiment',
contentWidget: PeriodicTable,
},
];
const CheatsheetHeader = () => Widget.CenterBox({
vertical: false,
startWidget: Widget.Box({}),
centerWidget: Widget.Box({
vertical: true,
className: "spacing-h-15",
children: [
Widget.Box({
hpack: 'center',
className: 'spacing-h-5 cheatsheet-title',
children: [
Widget.Label({
hpack: 'center',
css: 'margin-right: 0.682rem;',
className: 'txt-title',
label: 'Cheat sheet',
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key txt-small",
label: "󰖳",
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key-notkey txt-small",
label: "+",
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key txt-small",
label: "/",
})
]
}),
]
}),
endWidget: Widget.Button({
vpack: 'start',
hpack: 'end',
className: "cheatsheet-closebtn icon-material txt txt-hugeass",
onClicked: () => {
closeWindowOnAllMonitors('cheatsheet');
},
child: Widget.Label({
className: 'icon-material txt txt-hugeass',
label: 'close'
}),
setup: setupCursorHover,
}),
});
const sheetContents = [];
const SheetContent = (id) => {
sheetContents[id] = ExpandingIconTabContainer({
tabsHpack: 'center',
tabSwitcherClassName: 'sidebar-icontabswitcher',
transitionDuration: userOptions.animations.durationLarge * 1.4,
icons: cheatsheets.map((api) => api.materialIcon),
names: cheatsheets.map((api) => api.name),
children: cheatsheets.map((api) => api.contentWidget()),
onChange: (self, id) => {
self.shown = cheatsheets[id].name;
}
});
return sheetContents[id];
}
export default (id) => {
const sheets = SheetContent(id);
const widgetContent = Widget.Box({
vertical: true,
className: "cheatsheet-bg spacing-v-5",
children: [
CheatsheetHeader(),
sheets,
]
});
return PopupWindow({
monitor: id,
name: `cheatsheet${id}`,
layer: 'overlay',
keymode: 'on-demand',
visible: false,
anchor: ['top', 'bottom', 'left', 'right'],
child: Widget.Box({
vertical: true,
children: [
clickCloseRegion({ name: 'cheatsheet' }),
Widget.Box({
children: [
clickCloseRegion({ name: 'cheatsheet' }),
widgetContent,
clickCloseRegion({ name: 'cheatsheet' }),
]
}),
clickCloseRegion({ name: 'cheatsheet' }),
],
setup: (self) => self.on('key-press-event', (widget, event) => { // Typing
// Whole sheet
if (checkKeybind(event, userOptions.keybinds.cheatsheet.nextTab))
sheetContents.forEach(tab => tab.nextTab())
else if (checkKeybind(event, userOptions.keybinds.cheatsheet.prevTab))
sheetContents.forEach(tab => tab.prevTab())
else if (checkKeybind(event, userOptions.keybinds.cheatsheet.cycleTab))
sheetContents.forEach(tab => tab.cycleTab())
// Keybinds
if (sheets.attribute.names[sheets.attribute.shown.value] == 'Keybinds') { // If Keybinds tab is focused
if (checkKeybind(event, userOptions.keybinds.cheatsheet.keybinds.nextTab)) {
sheetContents.forEach((sheet) => {
const toSwitchTab = sheet.attribute.children[sheet.attribute.shown.value];
toSwitchTab.nextTab();
})
}
else if (checkKeybind(event, userOptions.keybinds.cheatsheet.keybinds.prevTab)) {
sheetContents.forEach((sheet) => {
const toSwitchTab = sheet.attribute.children[sheet.attribute.shown.value];
toSwitchTab.prevTab();
})
}
}
})
})
});
}

View File

@@ -0,0 +1,94 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { niceTypes, periodicTable, series } from "./data_periodictable.js";
const { Box, Button, Icon, Label, Revealer } = Widget;
export default () => {
const ElementTile = (element) => {
return Box({
vertical: true,
tooltipText: element.electronConfig ? `${element.electronConfig}` : null,
className: `cheatsheet-periodictable-${element.type}`,
children: element.name == '' ? null : [
Box({
className: 'padding-left-8 padding-right-8 padding-top-8',
children: [
Label({
label: `${element.number}`,
className: "cheatsheet-periodictable-elementnum txt-tiny txt-bold",
}),
Box({ hexpand: true }),
Label({
label: `${element.weight}`,
className: "txt-smaller",
})
]
}),
element.icon ? Icon({
icon: element.icon,
className: "txt-hugerass txt-bold",
}) : Label({
label: `${element.symbol}`,
className: "cheatsheet-periodictable-elementsymbol",
}),
Label({
label: `${element.name}`,
className: "txt-tiny",
})
]
})
}
const BoardColor = (type) => Box({
className: 'spacing-h-5',
children: [
Box({
homogeneous: true,
className: `cheatsheet-periodictable-legend-color-wrapper`,
children: [Box({
className: `cheatsheet-periodictable-legend-color-${type}`,
})]
}),
Label({
label: `${niceTypes[type]}`,
className: "txt txt-small",
})
]
})
const mainBoard = Box({
hpack: 'center',
vertical: true,
className: "spacing-v-3",
children: periodicTable.map((row, _) => Box({ // Rows
className: "spacing-h-5",
children: row.map((element, _) => ElementTile(element))
})),
});
const seriesBoard = Box({
hpack: 'center',
vertical: true,
className: "spacing-v-3",
children: series.map((row, _) => Box({ // Rows
className: "spacing-h-5",
children: row.map((element, _) => ElementTile(element))
})),
});
const legend = Box({
hpack: 'center',
className: 'spacing-h-20',
children: [
BoardColor('metal'),
BoardColor('nonmetal'),
BoardColor('noblegas'),
BoardColor('lanthanum'),
BoardColor('actinium'),
]
})
return Box({
vertical: true,
className: 'spacing-v-20',
children: [
mainBoard,
seriesBoard,
legend
]
})
}

View File

@@ -0,0 +1,22 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { enableClickthrough } from "../.widgetutils/clickthrough.js";
import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
export default (monitor = 0, ) => {
return Widget.Window({
monitor,
name: `crosshair${monitor}`,
layer: 'overlay',
exclusivity: 'ignore',
visible: false,
child: Widget.Icon({
icon: 'crosshair-symbolic',
css: `
font-size: ${userOptions.gaming.crosshair.size}px;
color: ${userOptions.gaming.crosshair.color};
`,
}),
setup: enableClickthrough,
});
}

View File

@@ -0,0 +1,14 @@
export const quickLaunchItems = [
{
"name": "GitHub + Files×2",
"command": "github-desktop & nautilus --new-window & nautilus --new-window &"
},
{
"name": "Terminal×2",
"command": "foot & foot &"
},
{
"name": "Discord + Youtube + Github",
"command": "xdg-open 'https://discord.com/app' && xdg-open 'https://youtube.com/' && xdg-open 'https://github.com/' &"
},
]

View File

@@ -0,0 +1,24 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import WallpaperImage from './wallpaper.js';
import TimeAndLaunchesWidget from './timeandlaunches.js'
import SystemWidget from './system.js'
export default (monitor) => Widget.Window({
name: `desktopbackground${monitor}`,
// anchor: ['top', 'bottom', 'left', 'right'],
layer: 'background',
exclusivity: 'ignore',
visible: true,
child: Widget.Overlay({
child: WallpaperImage(monitor),
// child: Widget.Box({}),
overlays: [
TimeAndLaunchesWidget(),
SystemWidget(),
],
setup: (self) => {
self.set_overlay_pass_through(self.get_children()[1], true);
},
}),
});

View File

@@ -0,0 +1,161 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { execAsync, exec } = Utils;
const { Box, EventBox, Label, Revealer, Overlay } = Widget;
import { AnimatedCircProg } from "../.commonwidgets/cairo_circularprogress.js";
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
const ResourceValue = (name, icon, interval, valueUpdateCmd, displayFunc, props = {}) => Box({
...props,
className: 'bg-system-bg txt',
children: [
Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
vpack: 'center',
vertical: true,
className: 'margin-right-15',
children: [
Label({
xalign: 1,
className: 'txt-small txt',
label: `${name}`,
}),
Label({
xalign: 1,
className: 'titlefont txt-norm txt-onSecondaryContainer',
setup: (self) => self
.poll(interval, (label) => displayFunc(label))
,
})
]
})
}),
Overlay({
child: AnimatedCircProg({
className: 'bg-system-circprog',
extraSetup: (self) => self
.poll(interval, (self) => {
execAsync(['bash', '-c', `${valueUpdateCmd}`]).then((newValue) => {
self.css = `font-size: ${Math.round(newValue)}px;`
}).catch(print);
})
,
}),
overlays: [
MaterialIcon(`${icon}`, 'hugeass'),
],
setup: self => self.set_overlay_pass_through(self.get_children()[1], true),
}),
]
})
const resources = Box({
vpack: 'fill',
vertical: true,
className: 'spacing-v-15',
children: [
ResourceValue('Memory', 'memory', 10000, `free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
(label) => {
execAsync(['bash', '-c', `free -h | awk '/^Mem/ {print $3 " / " $2}' | sed 's/Gi/Gib/g'`])
.then((output) => {
label.label = `${output}`
}).catch(print);
}, { hpack: 'end' }),
ResourceValue('Swap', 'swap_horiz', 10000, `free | awk '/^Swap/ {if ($2 > 0) printf("%.2f\\n", ($3/$2) * 100); else print "0";}'`,
(label) => {
execAsync(['bash', '-c', `free -h | awk '/^Swap/ {if ($2 != "0") print $3 " / " $2; else print "No swap"}' | sed 's/Gi/Gib/g'`])
.then((output) => {
label.label = `${output}`
}).catch(print);
}, { hpack: 'end' }),
ResourceValue('Disk space', 'hard_drive_2', 3600000, `echo $(df --output=pcent / | tr -dc '0-9')`,
(label) => {
execAsync(['bash', '-c', `df -h --output=avail / | awk 'NR==2{print $1}'`])
.then((output) => {
label.label = `${output} available`
}).catch(print);
}, { hpack: 'end' }),
]
});
const distroAndVersion = Box({
vertical: true,
children: [
Box({
hpack: 'end',
children: [
Label({
className: 'bg-distro-txt',
xalign: 0,
label: 'Hyping on ',
}),
Label({
className: 'bg-distro-name',
xalign: 0,
label: '<distro>',
setup: (label) => {
execAsync([`grep`, `-oP`, `PRETTY_NAME="\\K[^"]+`, `/etc/os-release`]).then(distro => {
label.label = distro;
}).catch(print);
},
}),
]
}),
Box({
hpack: 'end',
children: [
Label({
className: 'bg-distro-txt',
xalign: 0,
label: 'with ',
}),
Label({
className: 'bg-distro-name',
xalign: 0,
label: 'An environment idk',
setup: (label) => {
// hyprctl will return unsuccessfully if Hyprland isn't running
execAsync([`bash`, `-c`, `hyprctl version | grep -oP "Tag: v\\K\\d+\\.\\d+\\.\\d+"`]).then(version => {
label.label = `Hyprland ${version}`;
}).catch(() => execAsync([`bash`, `-c`, `sway -v | cut -d'-' -f1 | sed 's/sway version /v/'`]).then(version => {
label.label = `Sway ${version}`;
}).catch(print));
},
}),
]
})
]
})
export default () => Box({
hpack: 'end',
vpack: 'end',
children: [
EventBox({
child: Box({
hpack: 'end',
vpack: 'end',
className: 'bg-distro-box spacing-v-20',
vertical: true,
children: [
resources,
distroAndVersion,
]
}),
onPrimaryClickRelease: () => {
const kids = resources.get_children();
for (let i = 0; i < kids.length; i++) {
const child = kids[i];
const firstChild = child.get_children()[0];
firstChild.revealChild = !firstChild.revealChild;
}
},
})
],
})

View File

@@ -0,0 +1,74 @@
const { GLib } = imports.gi;
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
const { execAsync, exec } = Utils;
const { Box, Label, Button, Revealer, EventBox } = Widget;
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { quickLaunchItems } from './data_quicklaunches.js'
const TimeAndDate = () => Box({
vertical: true,
className: 'spacing-v--5',
children: [
Label({
className: 'bg-time-clock',
xalign: 0,
label: GLib.DateTime.new_now_local().format(userOptions.time.format),
setup: (self) => self.poll(userOptions.time.interval, label => {
label.label = GLib.DateTime.new_now_local().format(userOptions.time.format);
}),
}),
Label({
className: 'bg-time-date',
xalign: 0,
label: GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong),
setup: (self) => self.poll(userOptions.time.dateInterval, (label) => {
label.label = GLib.DateTime.new_now_local().format(userOptions.time.dateFormatLong);
}),
}),
]
})
const QuickLaunches = () => Box({
vertical: true,
className: 'spacing-v-10',
children: [
Label({
xalign: 0,
className: 'bg-quicklaunch-title',
label: 'Quick Launches',
}),
Box({
hpack: 'start',
className: 'spacing-h-5',
children: quickLaunchItems.map((item, i) => Button({
onClicked: () => {
execAsync(['bash', '-c', `${item["command"]}`]).catch(print);
},
className: 'bg-quicklaunch-btn',
child: Label({
label: `${item["name"]}`,
}),
setup: (self) => {
setupCursorHover(self);
}
})),
})
]
})
export default () => Box({
hpack: 'start',
vpack: 'end',
vertical: true,
className: 'bg-time-box spacing-h--10',
children: [
TimeAndDate(),
// QuickLaunches(),
],
})

View File

@@ -0,0 +1,119 @@
const { Gdk, GdkPixbuf, Gio, GLib, Gtk } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { exec, execAsync } = Utils;
const { Box, Button, Label, Stack } = Widget;
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Wallpaper from '../../services/wallpaper.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { clamp } from '../.miscutils/mathfuncs.js';
import { monitors } from '../.commondata/hyprlanddata.js';
const DISABLE_AGS_WALLPAPER = true;
const SWITCHWALL_SCRIPT_PATH = `${App.configDir}/scripts/color_generation/switchwall.sh`;
const WALLPAPER_ZOOM_SCALE = 1.25; // For scrolling when we switch workspace
const MAX_WORKSPACES = 10;
export default (monitor = 0) => {
const WALLPAPER_OFFSCREEN_X = (WALLPAPER_ZOOM_SCALE - 1) * monitors[monitor].width;
const WALLPAPER_OFFSCREEN_Y = (WALLPAPER_ZOOM_SCALE - 1) * monitors[monitor].height;
const wallpaperImage = Widget.DrawingArea({
attribute: {
pixbuf: undefined,
workspace: 1,
sideleft: 0,
sideright: 0,
updatePos: (self) => {
self.setCss(`font-size: ${self.attribute.workspace - self.attribute.sideleft + self.attribute.sideright}px;`)
},
},
className: 'bg-wallpaper-transition',
setup: (self) => {
self.set_size_request(monitors[monitor].width, monitors[monitor].height);
self
// TODO: reduced updates using timeouts to reduce lag
// .hook(Hyprland.active.workspace, (self) => {
// self.attribute.workspace = Hyprland.active.workspace.id
// self.attribute.updatePos(self);
// })
// .hook(App, (box, name, visible) => { // Update on open
// if (self.attribute[name] === undefined) return;
// self.attribute[name] = (visible ? 1 : 0);
// self.attribute.updatePos(self);
// })
.on('draw', (self, cr) => {
if (!self.attribute.pixbuf) return;
const styleContext = self.get_style_context();
const workspace = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
// Draw
Gdk.cairo_set_source_pixbuf(cr, self.attribute.pixbuf,
-(WALLPAPER_OFFSCREEN_X / (MAX_WORKSPACES + 1) * (clamp(workspace, 0, MAX_WORKSPACES + 1))),
-WALLPAPER_OFFSCREEN_Y / 2);
cr.paint();
})
.hook(Wallpaper, (self) => {
if (DISABLE_AGS_WALLPAPER) return;
const wallPath = Wallpaper.get(monitor);
if (!wallPath || wallPath === "") return;
self.attribute.pixbuf = GdkPixbuf.Pixbuf.new_from_file(wallPath);
const scale_x = monitors[monitor].width * WALLPAPER_ZOOM_SCALE / self.attribute.pixbuf.get_width();
const scale_y = monitors[monitor].height * WALLPAPER_ZOOM_SCALE / self.attribute.pixbuf.get_height();
const scale_factor = Math.max(scale_x, scale_y);
self.attribute.pixbuf = self.attribute.pixbuf.scale_simple(
Math.round(self.attribute.pixbuf.get_width() * scale_factor),
Math.round(self.attribute.pixbuf.get_height() * scale_factor),
GdkPixbuf.InterpType.BILINEAR
);
self.queue_draw();
}, 'updated');
;
}
,
});
const wallpaperPrompt = Box({
hpack: 'center',
vpack: 'center',
vertical: true,
className: 'spacing-v-10',
children: [
Label({
hpack: 'center',
justification: 'center',
className: 'txt-large',
label: `No wallpaper loaded.\nAn image ≥ ${monitors[monitor].width * WALLPAPER_ZOOM_SCALE} × ${monitors[monitor].height * WALLPAPER_ZOOM_SCALE} is recommended.`,
}),
Button({
hpack: 'center',
className: 'btn-primary',
label: `Select one`,
setup: setupCursorHover,
onClicked: (self) => Utils.execAsync([SWITCHWALL_SCRIPT_PATH]).catch(print),
}),
]
});
const stack = Stack({
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
children: {
'disabled': Box({}),
'image': wallpaperImage,
'prompt': wallpaperPrompt,
},
setup: (self) => self
.hook(Wallpaper, (self) => {
if (DISABLE_AGS_WALLPAPER) {
self.shown = 'disabled';
return;
}
const wallPath = Wallpaper.get(monitor);
self.shown = ((wallPath && wallPath != "") ? 'image' : 'prompt');
}, 'updated')
,
})
return stack;
// return wallpaperImage;
}

View File

@@ -0,0 +1,300 @@
const { Gtk, GLib } = imports.gi;
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { EventBox, Button } = Widget;
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
const { execAsync, exec } = Utils;
const { Box, Revealer } = Widget;
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { getAllFiles, searchIcons } from './icons.js'
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { substitute } from '../.miscutils/icons.js';
const icon_files = userOptions.icons.searchPaths.map(e => getAllFiles(e)).flat(1)
let isPinned = false
let cachePath = new Map()
let timers = []
function clearTimes() {
timers.forEach(e => GLib.source_remove(e))
timers = []
}
function ExclusiveWindow(client) {
const fn = [
(client) => !(client !== null && client !== undefined),
// Jetbrains
(client) => client.title.includes("win"),
// Vscode
(client) => client.title === '' && client.class === ''
]
for (const item of fn) { if (item(client)) { return true } }
return false
}
const focus = ({ address }) => Utils.execAsync(`hyprctl dispatch focuswindow address:${address}`).catch(print);
const DockSeparator = (props = {}) => Box({
...props,
className: 'dock-separator',
})
const PinButton = () => Widget.Button({
className: 'dock-app-btn dock-app-btn-animate',
tooltipText: 'Pin Dock',
child: Widget.Box({
homogeneous: true,
className: 'dock-app-icon txt',
child: MaterialIcon('push_pin', 'hugeass')
}),
onClicked: (self) => {
isPinned = !isPinned
self.className = `${isPinned ? "pinned-dock-app-btn" : "dock-app-btn animate"} dock-app-btn-animate`
},
setup: setupCursorHover,
})
const LauncherButton = () => Widget.Button({
className: 'dock-app-btn dock-app-btn-animate',
tooltipText: 'Open launcher',
child: Widget.Box({
homogeneous: true,
className: 'dock-app-icon txt',
child: MaterialIcon('apps', 'hugerass')
}),
onClicked: (self) => {
App.toggleWindow('overview');
},
setup: setupCursorHover,
})
const AppButton = ({ icon, ...rest }) => Widget.Revealer({
attribute: {
'workspace': 0
},
revealChild: false,
transition: 'slide_right',
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Button({
...rest,
className: 'dock-app-btn dock-app-btn-animate',
child: Widget.Box({
child: Widget.Overlay({
child: Widget.Box({
homogeneous: true,
className: 'dock-app-icon',
child: Widget.Icon({
icon: icon,
}),
}),
overlays: [Widget.Box({
class_name: 'indicator',
vpack: 'end',
hpack: 'center',
})],
}),
}),
setup: (button) => {
setupCursorHover(button);
}
})
});
const Taskbar = (monitor) => Widget.Box({
className: 'dock-apps',
attribute: {
monitor: monitor,
'map': new Map(),
'clientSortFunc': (a, b) => {
return a.attribute.workspace > b.attribute.workspace;
},
'update': (box, monitor) => {
for (let i = 0; i < Hyprland.clients.length; i++) {
const client = Hyprland.clients[i];
if (client["pid"] == -1) return;
const appClass = substitute(client.class);
// for (const appName of userOptions.dock.pinnedApps) {
// if (appClass.includes(appName.toLowerCase()))
// return null;
// }
let appClassLower = appClass.toLowerCase()
let path = ''
if (cachePath[appClassLower]) { path = cachePath[appClassLower] }
else {
path = searchIcons(appClass.toLowerCase(), icon_files)
cachePath[appClassLower] = path
}
if (path === '') { path = substitute(appClass) }
const newButton = AppButton({
icon: path,
tooltipText: `${client.title} (${appClass})`,
onClicked: () => focus(client),
});
newButton.attribute.workspace = client.workspace.id;
newButton.revealChild = true;
box.attribute.map.set(client.address, newButton);
}
box.children = Array.from(box.attribute.map.values());
},
'add': (box, address, monitor) => {
if (!address) { // First active emit is undefined
box.attribute.update(box);
return;
}
const newClient = Hyprland.clients.find(client => {
return client.address == address;
});
if (ExclusiveWindow(newClient)) { return }
let appClass = newClient.class
let appClassLower = appClass.toLowerCase()
let path = ''
if (cachePath[appClassLower]) { path = cachePath[appClassLower] }
else {
path = searchIcons(appClassLower, icon_files)
cachePath[appClassLower] = path
}
if (path === '') { path = substitute(appClass) }
const newButton = AppButton({
icon: path,
tooltipText: `${newClient.title} (${appClass})`,
onClicked: () => focus(newClient),
})
newButton.attribute.workspace = newClient.workspace.id;
box.attribute.map.set(address, newButton);
box.children = Array.from(box.attribute.map.values());
newButton.revealChild = true;
},
'remove': (box, address) => {
if (!address) return;
const removedButton = box.attribute.map.get(address);
if (!removedButton) return;
removedButton.revealChild = false;
Utils.timeout(userOptions.animations.durationLarge, () => {
removedButton.destroy();
box.attribute.map.delete(address);
box.children = Array.from(box.attribute.map.values());
})
},
},
setup: (self) => {
self.hook(Hyprland, (box, address) => box.attribute.add(box, address, self.monitor), 'client-added')
.hook(Hyprland, (box, address) => box.attribute.remove(box, address, self.monitor), 'client-removed')
Utils.timeout(100, () => self.attribute.update(self));
},
});
const PinnedApps = () => Widget.Box({
class_name: 'dock-apps',
homogeneous: true,
children: userOptions.dock.pinnedApps
.map(term => ({ app: Applications.query(term)?.[0], term }))
.filter(({ app }) => app)
.map(({ app, term = true }) => {
const newButton = AppButton({
// different icon, emm...
icon: userOptions.dock.searchPinnedAppIcons ?
searchIcons(app.name, icon_files) :
app.icon_name,
onClicked: () => {
for (const client of Hyprland.clients) {
if (client.class.toLowerCase().includes(term))
return focus(client);
}
app.launch();
},
onMiddleClick: () => app.launch(),
tooltipText: app.name,
setup: (self) => {
self.revealChild = true;
self.hook(Hyprland, button => {
const running = Hyprland.clients
.find(client => client.class.toLowerCase().includes(term)) || false;
button.toggleClassName('notrunning', !running);
button.toggleClassName('focused', Hyprland.active.client.address == running.address);
button.set_tooltip_text(running ? running.title : app.name);
}, 'notify::clients')
},
})
newButton.revealChild = true;
return newButton;
}),
});
export default (monitor = 0) => {
const dockContent = Box({
className: 'dock-bg spacing-h-5',
children: [
PinButton(),
PinnedApps(),
DockSeparator(),
Taskbar(),
LauncherButton(),
]
})
const dockRevealer = Revealer({
attribute: {
'updateShow': self => { // I only use mouse to resize. I don't care about keyboard resize if that's a thing
if (userOptions.dock.monitorExclusivity)
self.revealChild = Hyprland.active.monitor.id === monitor;
else
self.revealChild = true;
return self.revealChild
}
},
revealChild: false,
transition: 'slide_up',
transitionDuration: userOptions.animations.durationLarge,
child: dockContent,
setup: (self) => {
const callback = (self, trigger) => {
if (!userOptions.dock.trigger.includes(trigger)) return
const flag = self.attribute.updateShow(self)
if (flag) clearTimes();
const hidden = userOptions.dock.autoHide.find(e => e["trigger"] === trigger)
if (hidden) {
let id = Utils.timeout(hidden.interval, () => {
if (!isPinned) { self.revealChild = false }
timers = timers.filter(e => e !== id)
})
timers.push(id)
}
}
self
// .hook(Hyprland, (self) => self.attribute.updateShow(self))
.hook(Hyprland.active.workspace, self => callback(self, "workspace-active"))
.hook(Hyprland.active.client, self => callback(self, "client-active"))
.hook(Hyprland, self => callback(self, "client-added"), "client-added")
.hook(Hyprland, self => callback(self, "client-removed"), "client-removed")
},
})
return EventBox({
onHover: () => {
dockRevealer.revealChild = true;
clearTimes()
},
child: Box({
homogeneous: true,
css: `min-height: ${userOptions.dock.hiddenThickness}px;`,
children: [dockRevealer],
}),
setup: self => self.on("leave-notify-event", () => {
if (!isPinned) dockRevealer.revealChild = false;
clearTimes()
})
})
}

View File

@@ -0,0 +1,63 @@
const { Gio, GLib } = imports.gi
const exists = (path) => Gio.File.new_for_path(path).query_exists(null);
export const levenshteinDistance = (a, b) => {
if (!a.length) { return b.length }
if (!b.length) { return a.length }
let f = Array.from(new Array(a.length + 1),
() => new Array(b.length + 1).fill(0))
for (let i = 0; i <= b.length; i++) { f[0][i] = i; }
for (let i = 0; i <= a.length; i++) { f[i][0] = i; }
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
if (a.charAt(i - 1) === b.charAt(j - 1)) {
f[i][j] = f[i-1][j-1]
} else {
f[i][j] = Math.min(f[i-1][j-1], Math.min(f[i][j-1], f[i-1][j])) + 1
}
}
}
return f[a.length][b.length]
}
export const getAllFiles = (dir, files = []) => {
if (!exists(dir)) { return [] }
const file = Gio.File.new_for_path(dir);
const enumerator = file.enumerate_children('standard::name,standard::type',
Gio.FileQueryInfoFlags.NONE, null);
for (const info of enumerator) {
if (info.get_file_type() === Gio.FileType.DIRECTORY) {
files.push(getAllFiles(`${dir}/${info.get_name()}`))
} else {
files.push(`${dir}/${info.get_name()}`)
}
}
return files.flat(1);
}
export const searchIcons = (appClass, files) => {
appClass = appClass.toLowerCase()
if (!files.length) { return "" }
let appro = 0x3f3f3f3f
let path = ""
for (const item of files) {
let score = levenshteinDistance(item.split("/").pop().toLowerCase().split(".")[0], appClass)
if (score < appro) {
appro = score
path = item
}
}
return path
}

View File

@@ -0,0 +1,12 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Dock from './dock.js';
export default (monitor = 0) => Widget.Window({
monitor,
name: `dock${monitor}`,
layer: userOptions.dock.layer,
anchor: ['bottom'],
exclusivity: 'normal',
visible: true,
child: Dock(monitor),
});

View File

@@ -0,0 +1,264 @@
const { Gio, GLib } = imports.gi;
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { ConfigToggle, ConfigMulipleSelection } from '../.commonwidgets/configwidgets.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { execAsync } = Utils;
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { showColorScheme } from '../../variables.js';
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { darkMode } from '../.miscutils/system.js';
const ColorBox = ({
name = 'Color',
...rest
}) => Widget.Box({
...rest,
homogeneous: true,
children: [
Widget.Label({
label: `${name}`,
})
]
})
const ColorSchemeSettingsRevealer = () => {
const headerButtonIcon = MaterialIcon('expand_more', 'norm');
const header = Widget.Button({
className: 'osd-settings-btn-arrow',
onClicked: () => {
content.revealChild = !content.revealChild;
headerButtonIcon.label = content.revealChild ? 'expand_less' : 'expand_more';
},
setup: setupCursorHover,
hpack: 'end',
child: headerButtonIcon,
});
const content = Widget.Revealer({
revealChild: false,
transition: 'slide_down',
transitionDuration: 200,
child: ColorSchemeSettings(),
setup: (self) => self.hook(isHoveredColorschemeSettings, (revealer) => {
if (isHoveredColorschemeSettings.value == false) {
setTimeout(() => {
if (isHoveredColorschemeSettings.value == false)
revealer.revealChild = false;
headerButtonIcon.label = 'expand_more';
}, 1500);
}
}),
});
return Widget.EventBox({
onHover: (self) => {
isHoveredColorschemeSettings.setValue(true);
},
onHoverLost: (self) => {
isHoveredColorschemeSettings.setValue(false);
},
child: Widget.Box({
vertical: true,
children: [
header,
content,
]
}),
});
}
function calculateSchemeInitIndex(optionsArr, searchValue = 'vibrant') {
if (searchValue == '')
searchValue = 'vibrant';
const flatArray = optionsArr.flatMap(subArray => subArray);
const result = flatArray.findIndex(element => element.value === searchValue);
const rowIndex = Math.floor(result / optionsArr[0].length);
const columnIndex = result % optionsArr[0].length;
return [rowIndex, columnIndex];
}
const schemeOptionsArr = [
[
{ name: 'Tonal Spot', value: 'tonalspot' },
{ name: 'Fruit Salad', value: 'fruitsalad' },
{ name: 'Fidelity', value: 'fidelity' },
{ name: 'Rainbow', value: 'rainbow' },
],
[
{ name: 'Neutral', value: 'neutral' },
{ name: 'Monochrome', value: 'monochrome' },
{ name: 'Expressive', value: 'expressive' },
{ name: 'Vibrant', value: 'vibrant' },
],
[
{ name: 'Vibrant+', value: 'morevibrant' },
],
//[
// { name: 'Content', value: 'content' },
//]
];
const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
const initTransparency = Utils.exec(`bash -c "sed -n \'2p\' ${LIGHTDARK_FILE_LOCATION}"`);
const initTransparencyVal = (initTransparency == "transparent") ? 1 : 0;
const initScheme = Utils.exec(`bash -c "sed -n \'3p\' ${LIGHTDARK_FILE_LOCATION}"`);
const initSchemeIndex = calculateSchemeInitIndex(schemeOptionsArr, initScheme);
const ColorSchemeSettings = () => Widget.Box({
className: 'osd-colorscheme-settings spacing-v-5 margin-20',
vertical: true,
vpack: 'center',
children: [
Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
className: 'txt-norm titlefont txt',
label: 'Options',
hpack: 'center',
}),
//////////////////
ConfigToggle({
icon: 'dark_mode',
name: 'Dark Mode',
desc: 'Ya should go to sleep!',
initValue: darkMode.value,
onChange: (_, newValue) => {
darkMode.value = !!newValue;
},
extraSetup: (self) => self.hook(darkMode, (self) => {
self.enabled.value = darkMode.value;
}),
}),
ConfigToggle({
icon: 'border_clear',
name: 'Transparency',
desc: 'Make shell elements transparent',
initValue: initTransparencyVal,
onChange: (self, newValue) => {
let transparency = newValue == 0 ? "opaque" : "transparent";
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "2s/.*/${transparency}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
.catch(print);
},
}),
Widget.Box({
tooltipText: 'Theme GTK apps using accent color\n(drawback: dark/light mode switching requires restart)',
className: 'txt spacing-h-5 configtoggle-box',
children: [
MaterialIcon('imagesearch_roller', 'norm'),
Widget.Label({
className: 'txt txt-small',
label: 'Use Gradience',
}),
Widget.Box({ hexpand: true }),
ConfigMulipleSelection({
hpack: 'center',
vpack: 'center',
optionsArr: [
[{ name: 'Off', value: 0 }, { name: 'On', value: 1 }],
],
initIndex: [-1, -1],
onChange: (value, name) => {
const ADWAITA_BLUE = "#3584E4";
if (value) execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --yes-gradience`, `&`])
.catch(print);
else execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${ADWAITA_BLUE}" --no-gradience`, `&`])
.catch(print);
},
}),
]
}),
]
}),
Widget.Box({
vertical: true,
className: 'spacing-v-5',
children: [
Widget.Label({
xalign: 0,
className: 'txt-norm titlefont txt margin-top-5',
label: 'Scheme styles',
hpack: 'center',
}),
//////////////////
ConfigMulipleSelection({
hpack: 'center',
vpack: 'center',
optionsArr: schemeOptionsArr,
initIndex: initSchemeIndex,
onChange: (value, name) => {
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "3s/.*/${value}/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
.catch(print);
},
}),
]
})
]
});
const ColorschemeContent = () => Widget.Box({
className: 'osd-colorscheme spacing-v-5',
vertical: true,
hpack: 'center',
children: [
Widget.Label({
xalign: 0,
className: 'txt-norm titlefont txt',
label: 'Color scheme',
hpack: 'center',
}),
Widget.Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
ColorBox({ name: 'P', className: 'osd-color osd-color-primary' }),
ColorBox({ name: 'S', className: 'osd-color osd-color-secondary' }),
ColorBox({ name: 'T', className: 'osd-color osd-color-tertiary' }),
ColorBox({ name: 'Sf', className: 'osd-color osd-color-surface' }),
ColorBox({ name: 'Sf-i', className: 'osd-color osd-color-inverseSurface' }),
ColorBox({ name: 'E', className: 'osd-color osd-color-error' }),
]
}),
Widget.Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
ColorBox({ name: 'P-c', className: 'osd-color osd-color-primaryContainer' }),
ColorBox({ name: 'S-c', className: 'osd-color osd-color-secondaryContainer' }),
ColorBox({ name: 'T-c', className: 'osd-color osd-color-tertiaryContainer' }),
ColorBox({ name: 'Sf-c', className: 'osd-color osd-color-surfaceContainer' }),
ColorBox({ name: 'Sf-v', className: 'osd-color osd-color-surfaceVariant' }),
ColorBox({ name: 'E-c', className: 'osd-color osd-color-errorContainer' }),
]
}),
ColorSchemeSettingsRevealer(),
]
});
const isHoveredColorschemeSettings = Variable(false);
export default () => Widget.Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: ColorschemeContent(),
setup: (self) => {
self
.hook(showColorScheme, (revealer) => {
if (showColorScheme.value == true)
revealer.revealChild = true;
else
revealer.revealChild = isHoveredColorschemeSettings.value;
})
.hook(isHoveredColorschemeSettings, (revealer) => {
if (isHoveredColorschemeSettings.value == false) {
setTimeout(() => {
if (isHoveredColorschemeSettings.value == false)
revealer.revealChild = showColorScheme.value;
}, 2000);
}
})
},
})

Some files were not shown because too many files have changed in this diff Show More