simple ags launcher

This commit is contained in:
Emile Clark-Boman 2024-11-11 12:29:10 +10:00
parent e2bd5335d8
commit 1d4189b040
186 changed files with 24097 additions and 335 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 getString('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 getString('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: getString('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,262 @@
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,
'barRoundCorners': 1, // 0: No, 1: Yes
'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)',
},
},
'i18n': {
'langCode': "",//Customize the locale, such as zh_CN,Optional value references "~/.config/ags/i18n/locales/"
'extraLogs': false
},
'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,
'saveInFolderByTags': 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",
}
},
'bar': {
// Array of bar modes for each monitor. Hit Ctrl+Alt+Slash to cycle.
// Modes: "normal", "focus" (workspace indicator only), "nothing"
// Example for four monitors: ["normal", "focus", "normal", "nothing"]
'modes': ["normal"]
},
}
// 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: /<!--[\s\S]*?-->/, 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: 'top',
...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,129 @@
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[monitor];
})
}),
});
}
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 = getString('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(getString('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(getString('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(getString('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: getString('Screen snip'), icon: 'screenshot_region', onClicked: () => {
Utils.execAsync(`${App.configDir}/scripts/grimblast.sh copy area`)
.catch(print)
}
}),
UtilButton({
name: getString('Color picker'), icon: 'colorize', onClicked: () => {
Utils.execAsync(['hyprpicker', '-a']).catch(print)
}
}),
UtilButton({
name: getString('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: getString(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: getString(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: getString('Keybinds'),
materialIcon: 'keyboard',
contentWidget: Keybinds,
},
{
name: getString('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: getString('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: 'top',
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,21 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { enableClickthrough } from "../.widgetutils/clickthrough.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: getString('Tonal Spot'), value: 'tonalspot' },
{ name: getString('Fruit Salad'), value: 'fruitsalad' },
{ name: getString('Fidelity'), value: 'fidelity' },
{ name: getString('Rainbow'), value: 'rainbow' },
],
[
{ name: getString('Neutral'), value: 'neutral' },
{ name: getString('Monochrome'), value: 'monochrome' },
{ name: getString('Expressive'), value: 'expressive' },
{ name: getString('Vibrant'), value: 'vibrant' },
],
[
{ name: getString('Vibrant+'), value: 'morevibrant' },
],
//[
// { name: getString('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: getString('Options'),
hpack: 'center',
}),
//////////////////
ConfigToggle({
icon: 'dark_mode',
name: getString('Dark Mode'),
desc: getString('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: getString('Transparency'),
desc: getString('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: getString('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: getString('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: getString('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: getString('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);
}
})
},
})

View file

@ -0,0 +1,124 @@
// This file is for brightness/volume indicators
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
const { Box, Label, ProgressBar } = Widget;
import { MarginRevealer } from '../.widgethacks/advancedrevealers.js';
import Brightness from '../../services/brightness.js';
import Indicator from '../../services/indicator.js';
const OsdValue = ({
name, nameSetup = undefined, labelSetup, progressSetup,
extraClassName = '', extraProgressClassName = '',
...rest
}) => {
const valueName = Label({
xalign: 0, yalign: 0, hexpand: true,
className: 'osd-label',
label: `${name}`,
setup: nameSetup,
});
const valueNumber = Label({
hexpand: false, className: 'osd-value-txt',
setup: labelSetup,
});
return Box({ // Volume
vertical: true,
hexpand: true,
className: `osd-bg osd-value ${extraClassName}`,
attribute: {
'disable': () => {
valueNumber.label = '󰖭';
}
},
children: [
Box({
vexpand: true,
children: [
valueName,
valueNumber,
]
}),
ProgressBar({
className: `osd-progress ${extraProgressClassName}`,
hexpand: true,
vertical: false,
setup: progressSetup,
})
],
...rest,
});
}
export default (monitor = 0) => {
const brightnessIndicator = OsdValue({
name: 'Brightness',
extraClassName: 'osd-brightness',
extraProgressClassName: 'osd-brightness-progress',
labelSetup: (self) => self.hook(Brightness[monitor], self => {
self.label = `${Math.round(Brightness[monitor].screen_value * 100)}`;
}, 'notify::screen-value'),
progressSetup: (self) => self.hook(Brightness[monitor], (progress) => {
const updateValue = Brightness[monitor].screen_value;
if (updateValue !== progress.value) Indicator.popup(1);
progress.value = updateValue;
}, 'notify::screen-value'),
});
const volumeIndicator = OsdValue({
name: 'Volume',
extraClassName: 'osd-volume',
extraProgressClassName: 'osd-volume-progress',
attribute: { headphones: undefined , device: undefined},
nameSetup: (self) => Utils.timeout(1, () => {
const updateAudioDevice = (self) => {
const usingHeadphones = (Audio.speaker?.stream?.port)?.toLowerCase().includes('headphone');
if (volumeIndicator.attribute.headphones === undefined ||
volumeIndicator.attribute.headphones !== usingHeadphones) {
volumeIndicator.attribute.headphones = usingHeadphones;
self.label = usingHeadphones ? 'Headphones' : 'Speakers';
// Indicator.popup(1);
}
}
self.hook(Audio, updateAudioDevice);
Utils.timeout(1000, updateAudioDevice);
}),
labelSetup: (self) => self.hook(Audio, (label) => {
const newDevice = (Audio.speaker?.name);
const updateValue = Math.round(Audio.speaker?.volume * 100);
if (!isNaN(updateValue)) {
if (newDevice === volumeIndicator.attribute.device && updateValue != label.label) {
Indicator.popup(1);
}
}
volumeIndicator.attribute.device = newDevice;
label.label = `${updateValue}`;
}),
progressSetup: (self) => self.hook(Audio, (progress) => {
const updateValue = Audio.speaker?.volume;
if (!isNaN(updateValue)) {
if (updateValue > 1) progress.value = 1;
else progress.value = updateValue;
}
}),
});
return MarginRevealer({
transition: 'slide_down',
showClass: 'osd-show',
hideClass: 'osd-hide',
extraSetup: (self) => self
.hook(Indicator, (revealer, value) => {
if (value > -1) revealer.attribute.show();
else revealer.attribute.hide();
}, 'popup')
,
child: Box({
hpack: 'center',
vertical: false,
className: 'spacing-h--10',
children: [
brightnessIndicator,
volumeIndicator,
]
})
});
}

View file

@ -0,0 +1,32 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Indicator from '../../services/indicator.js';
import IndicatorValues from './indicatorvalues.js';
import MusicControls from './musiccontrols.js';
import ColorScheme from './colorscheme.js';
import NotificationPopups from './notificationpopups.js';
export default (monitor = 0) => Widget.Window({
name: `indicator${monitor}`,
monitor,
className: 'indicator',
layer: 'overlay',
// exclusivity: 'ignore',
visible: true,
anchor: ['top'],
child: Widget.EventBox({
onHover: () => { //make the widget hide when hovering
Indicator.popup(-1);
},
child: Widget.Box({
vertical: true,
className: 'osd-window',
css: 'min-height: 2px;',
children: [
IndicatorValues(monitor),
MusicControls(),
NotificationPopups(),
ColorScheme(),
]
})
}),
});

View file

@ -0,0 +1,408 @@
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 * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
const { exec, execAsync } = Utils;
const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
import { fileExists } from '../.miscutils/files.js';
import { AnimatedCircProg } from "../.commonwidgets/cairo_circularprogress.js";
import { showMusicControls } from '../../variables.js';
import { darkMode, hasPlasmaIntegration } from '../.miscutils/system.js';
const COMPILED_STYLE_DIR = `${GLib.get_user_cache_dir()}/ags/user/generated`
const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/colormode.txt`;
const colorMode = Utils.exec(`bash -c "sed -n \'1p\' '${LIGHTDARK_FILE_LOCATION}'"`);
const lightDark = (colorMode == "light") ? '-l' : '';
const COVER_COLORSCHEME_SUFFIX = '_colorscheme.css';
var lastCoverPath = '';
function isRealPlayer(player) {
return (
// Remove unecessary native buses from browsers if there's plasma integration
!(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
!(hasPlasmaIntegration && player.busName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
// playerctld just copies other buses and we don't need duplicates
!player.busName.startsWith('org.mpris.MediaPlayer2.playerctld') &&
// Non-instance mpd bus
!(player.busName.endsWith('.mpd') && !player.busName.endsWith('MediaPlayer2.mpd'))
);
}
export const getPlayer = (name = userOptions.music.preferredPlayer) => Mpris.getPlayer(name) || Mpris.players[0] || null;
function lengthStr(length) {
const min = Math.floor(length / 60);
const sec = Math.floor(length % 60);
const sec0 = sec < 10 ? '0' : '';
return `${min}:${sec0}${sec}`;
}
function detectMediaSource(link) {
if (link.startsWith("file://")) {
if (link.includes('firefox-mpris'))
return '󰈹 Firefox'
return "󰈣 File";
}
let url = link.replace(/(^\w+:|^)\/\//, '');
let domain = url.match(/(?:[a-z]+\.)?([a-z]+\.[a-z]+)/i)[1];
if (domain == 'ytimg.com') return '󰗃 Youtube';
if (domain == 'discordapp.net') return '󰙯 Discord';
if (domain == 'sndcdn.com') return '󰓀 SoundCloud';
return domain;
}
const DEFAULT_MUSIC_FONT = 'Gabarito, sans-serif';
function getTrackfont(player) {
const title = player.trackTitle;
const artists = player.trackArtists.join(' ');
if (artists.includes('TANO*C') || artists.includes('USAO') || artists.includes('Kobaryo'))
return 'Chakra Petch'; // Rigid square replacement
if (title.includes('東方'))
return 'Crimson Text, serif'; // Serif for Touhou stuff
return DEFAULT_MUSIC_FONT;
}
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 TrackProgress = ({ player, ...rest }) => {
const _updateProgress = (circprog) => {
// const player = Mpris.getPlayer();
if (!player) return;
// Set circular progress (see definition of AnimatedCircProg for explanation)
circprog.css = `font-size: ${Math.max(player.position / player.length * 100, 0)}px;`
}
return AnimatedCircProg({
...rest,
className: 'osd-music-circprog',
vpack: 'center',
extraSetup: (self) => self
.hook(Mpris, _updateProgress)
.poll(3000, _updateProgress)
,
})
}
const TrackTitle = ({ player, ...rest }) => Label({
...rest,
label: 'No music playing',
xalign: 0,
truncate: 'end',
// wrap: true,
className: 'osd-music-title',
setup: (self) => self.hook(player, (self) => {
// Player name
self.label = player.trackTitle.length > 0 ? trimTrackTitle(player.trackTitle) : 'No media';
// Font based on track/artist
const fontForThisTrack = getTrackfont(player);
self.css = `font-family: ${fontForThisTrack}, ${DEFAULT_MUSIC_FONT};`;
}, 'notify::track-title'),
});
const TrackArtists = ({ player, ...rest }) => Label({
...rest,
xalign: 0,
className: 'osd-music-artists',
truncate: 'end',
setup: (self) => self.hook(player, (self) => {
self.label = player.trackArtists.length > 0 ? player.trackArtists.join(', ') : '';
}, 'notify::track-artists'),
})
const CoverArt = ({ player, ...rest }) => {
const fallbackCoverArt = Box({ // Fallback
className: 'osd-music-cover-fallback',
homogeneous: true,
children: [Label({
className: 'icon-material txt-gigantic txt-thin',
label: 'music_note',
})]
});
// const coverArtDrawingArea = Widget.DrawingArea({ className: 'osd-music-cover-art' });
// const coverArtDrawingAreaStyleContext = coverArtDrawingArea.get_style_context();
const realCoverArt = Box({
className: 'osd-music-cover-art',
homogeneous: true,
// children: [coverArtDrawingArea],
attribute: {
'pixbuf': null,
// 'showImage': (self, imagePath) => {
// const borderRadius = coverArtDrawingAreaStyleContext.get_property('border-radius', Gtk.StateFlags.NORMAL);
// const frameHeight = coverArtDrawingAreaStyleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
// const frameWidth = coverArtDrawingAreaStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
// let imageHeight = frameHeight;
// let imageWidth = frameWidth;
// // Get image dimensions
// execAsync(['identify', '-format', '{"w":%w,"h":%h}', imagePath])
// .then((output) => {
// const imageDimensions = JSON.parse(output);
// const imageAspectRatio = imageDimensions.w / imageDimensions.h;
// const displayedAspectRatio = imageWidth / imageHeight;
// if (imageAspectRatio >= displayedAspectRatio) {
// imageWidth = imageHeight * imageAspectRatio;
// } else {
// imageHeight = imageWidth / imageAspectRatio;
// }
// // Real stuff
// // TODO: fix memory leak(?)
// // if (self.attribute.pixbuf) {
// // self.attribute.pixbuf.unref();
// // self.attribute.pixbuf = null;
// // }
// self.attribute.pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imagePath, imageWidth, imageHeight);
// coverArtDrawingArea.set_size_request(frameWidth, frameHeight);
// coverArtDrawingArea.connect("draw", (widget, cr) => {
// // Clip a rounded rectangle area
// cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
// cr.arc(frameWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
// cr.arc(frameWidth - borderRadius, frameHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
// cr.arc(borderRadius, frameHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
// cr.closePath();
// cr.clip();
// // Paint image as bg, centered
// Gdk.cairo_set_source_pixbuf(cr, self.attribute.pixbuf,
// frameWidth / 2 - imageWidth / 2,
// frameHeight / 2 - imageHeight / 2
// );
// cr.paint();
// });
// }).catch(print)
// },
'updateCover': (self) => {
// const player = Mpris.getPlayer(); // Maybe no need to re-get player.. can't remember why I had this
// Player closed
// Note that cover path still remains, so we're checking title
if (!player || player.trackTitle == "" || !player.coverPath) {
self.css = `background-image: none;`; // CSS image
App.applyCss(`${COMPILED_STYLE_DIR}/style.css`);
return;
}
const coverPath = player.coverPath;
const stylePath = `${player.coverPath}${darkMode.value ? '' : '-l'}${COVER_COLORSCHEME_SUFFIX}`;
if (player.coverPath == lastCoverPath) { // Since 'notify::cover-path' emits on cover download complete
Utils.timeout(200, () => {
// self.attribute.showImage(self, coverPath);
self.css = `background-image: url('${coverPath}');`; // CSS image
});
}
lastCoverPath = player.coverPath;
// If a colorscheme has already been generated, skip generation
if (fileExists(stylePath)) {
// self.attribute.showImage(self, coverPath)
self.css = `background-image: url('${coverPath}');`; // CSS image
App.applyCss(stylePath);
return;
}
// Generate colors
execAsync(['bash', '-c',
`${App.configDir}/scripts/color_generation/generate_colors_material.py --path '${coverPath}' --mode ${darkMode.value ? 'dark' : 'light'} > ${GLib.get_user_state_dir()}/ags/scss/_musicmaterial.scss`])
.then(() => {
exec(`wal -i "${player.coverPath}" -n -t -s -e -q ${darkMode.value ? '' : '-l'}`)
exec(`cp ${GLib.get_user_cache_dir()}/wal/colors.scss ${GLib.get_user_state_dir()}/ags/scss/_musicwal.scss`);
exec(`sass -I "${GLib.get_user_state_dir()}/ags/scss" -I "${App.configDir}/scss/fallback" "${App.configDir}/scss/_music.scss" "${stylePath}"`);
Utils.timeout(200, () => {
// self.attribute.showImage(self, coverPath)
self.css = `background-image: url('${coverPath}');`; // CSS image
});
App.applyCss(`${stylePath}`);
})
.catch(print);
},
},
setup: (self) => self
.hook(player, (self) => {
self.attribute.updateCover(self);
}, 'notify::cover-path')
,
});
return Box({
...rest,
className: 'osd-music-cover',
children: [
Widget.Overlay({
child: fallbackCoverArt,
overlays: [realCoverArt],
})
],
})
}
const TrackControls = ({ player, ...rest }) => Widget.Revealer({
revealChild: false,
transition: 'slide_right',
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Box({
...rest,
vpack: 'center',
className: 'osd-music-controls spacing-h-3',
children: [
Button({
className: 'osd-music-controlbtn',
onClicked: () => player.previous(),
child: Label({
className: 'icon-material osd-music-controlbtn-txt',
label: 'skip_previous',
})
}),
Button({
className: 'osd-music-controlbtn',
onClicked: () => player.next(),
child: Label({
className: 'icon-material osd-music-controlbtn-txt',
label: 'skip_next',
})
}),
],
}),
setup: (self) => self.hook(Mpris, (self) => {
// const player = Mpris.getPlayer();
if (!player)
self.revealChild = false;
else
self.revealChild = true;
}, 'notify::play-back-status'),
});
const TrackSource = ({ player, ...rest }) => Widget.Revealer({
revealChild: false,
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Box({
...rest,
className: 'osd-music-pill spacing-h-5',
homogeneous: true,
children: [
Label({
hpack: 'fill',
justification: 'center',
className: 'icon-nerd',
setup: (self) => self.hook(player, (self) => {
self.label = detectMediaSource(player.trackCoverUrl);
}, 'notify::cover-path'),
}),
],
}),
setup: (self) => self.hook(Mpris, (self) => {
const mpris = Mpris.getPlayer('');
if (!mpris)
self.revealChild = false;
else
self.revealChild = true;
}),
});
const TrackTime = ({ player, ...rest }) => {
return Widget.Revealer({
revealChild: false,
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Box({
...rest,
vpack: 'center',
className: 'osd-music-pill spacing-h-5',
children: [
Label({
setup: (self) => self.poll(1000, (self) => {
// const player = Mpris.getPlayer();
if (!player) return;
self.label = lengthStr(player.position);
}),
}),
Label({ label: '/' }),
Label({
setup: (self) => self.hook(Mpris, (self) => {
// const player = Mpris.getPlayer();
if (!player) return;
self.label = lengthStr(player.length);
}),
}),
],
}),
setup: (self) => self.hook(Mpris, (self) => {
if (!player) self.revealChild = false;
else self.revealChild = true;
}),
})
}
const PlayState = ({ player }) => {
var position = 0;
const trackCircProg = TrackProgress({ player: player });
return Widget.Button({
className: 'osd-music-playstate',
child: Widget.Overlay({
child: trackCircProg,
overlays: [
Widget.Button({
className: 'osd-music-playstate-btn',
onClicked: () => player.playPause(),
child: Widget.Label({
justification: 'center',
hpack: 'fill',
vpack: 'center',
setup: (self) => self.hook(player, (label) => {
label.label = `${player.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
}, 'notify::play-back-status'),
}),
}),
],
passThrough: true,
})
});
}
const MusicControlsWidget = (player) => Box({
className: 'osd-music spacing-h-20 test',
children: [
CoverArt({ player: player, vpack: 'center' }),
Box({
vertical: true,
className: 'spacing-v-5 osd-music-info',
children: [
Box({
vertical: true,
vpack: 'center',
hexpand: true,
children: [
TrackTitle({ player: player }),
TrackArtists({ player: player }),
]
}),
Box({ vexpand: true }),
Box({
className: 'spacing-h-10',
setup: (box) => {
box.pack_start(TrackControls({ player: player }), false, false, 0);
box.pack_end(PlayState({ player: player }), false, false, 0);
if(hasPlasmaIntegration || player.busName.startsWith('org.mpris.MediaPlayer2.chromium')) box.pack_end(TrackTime({ player: player }), false, false, 0)
// box.pack_end(TrackSource({ vpack: 'center', player: player }), false, false, 0);
}
})
]
})
]
})
export default () => Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: Box({
children: Mpris.bind("players")
.as(players => players.map((player) => (isRealPlayer(player) ? MusicControlsWidget(player) : null)))
}),
setup: (self) => self.hook(showMusicControls, (revealer) => {
revealer.revealChild = showMusicControls.value;
}),
})

View file

@ -0,0 +1,45 @@
// This file is for popup notifications
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
const { Box } = Widget;
import Notification from '../.commonwidgets/notification.js';
export default () => Box({
vertical: true,
hpack: 'center',
className: 'osd-notifs spacing-v-5-revealer',
attribute: {
'map': new Map(),
'dismiss': (box, id, force = false) => {
if (!id || !box.attribute.map.has(id))
return;
const notifWidget = box.attribute.map.get(id);
if (notifWidget == null || notifWidget.attribute.hovered && !force)
return; // cuz already destroyed
notifWidget.revealChild = false;
notifWidget.attribute.destroyWithAnims();
box.attribute.map.delete(id);
},
'notify': (box, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
box.attribute.map.delete(id);
const notif = Notifications.getNotification(id);
const newNotif = Notification({
notifObject: notif,
isPopup: true,
});
box.attribute.map.set(id, newNotif);
box.pack_end(box.attribute.map.get(id), false, false, 0);
box.show_all();
},
},
setup: (self) => self
.hook(Notifications, (box, id) => box.attribute.notify(box, id), 'notified')
.hook(Notifications, (box, id) => box.attribute.dismiss(box, id), 'dismissed')
.hook(Notifications, (box, id) => box.attribute.dismiss(box, id, true), 'closed')
,
});

View file

@ -0,0 +1,218 @@
// We're going to use ydotool
// See /usr/include/linux/input-event-codes.h for keycodes
export const DEFAULT_OSK_LAYOUT = "qwerty_full"
export const oskLayouts = {
qwerty_full: {
name: "QWERTY - Full",
name_short: "US",
comment: "Like physical keyboard",
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
// keys: [
// [{ k: "Esc", t: "fn" }, { k: "F1", t: "fn" }, { k: "F2", t: "fn" }, { k: "F3", t: "fn" }, { k: "F4", t: "fn" }, { k: "F5", t: "fn" }, { k: "F6", t: "fn" }, { k: "F7", t: "fn" }, { k: "F8", t: "fn" }, { k: "F9", t: "fn" }, { k: "F10", t: "fn" }, { k: "F11", t: "fn" }, { k: "F12", t: "fn" }, { k: "PrtSc", t: "fn" }, { k: "Del", t: "fn" }],
// [{ k: "`", ks: "~", t: "normal" }, { k: "1", ks: "!", t: "normal" }, { k: "2", ks: "@", t: "normal" }, { k: "3", ks: "#", t: "normal" }, { k: "4", ks: "$", t: "normal" }, { k: "5", ks: "%", t: "normal" }, { k: "6", ks: "^", t: "normal" }, { k: "7", ks: "&", t: "normal" }, { k: "8", ks: "*", t: "normal" }, { k: "9", ks: "(", t: "normal" }, { k: "0", ks: ")", t: "normal" }, { k: "-", ks: "_", t: "normal" }, { k: "=", ks: "+", t: "normal" }, { k: "Backspace", t: "shift" }],
// [{ k: "Tab", t: "tab" }, { k: "q", ks: "Q", t: "normal" }, { k: "w", ks: "W", t: "normal" }, { k: "e", ks: "E", t: "normal" }, { k: "r", ks: "R", t: "normal" }, { k: "t", ks: "T", t: "normal" }, { k: "y", ks: "Y", t: "normal" }, { k: "u", ks: "U", t: "normal" }, { k: "i", ks: "I", t: "normal" }, { k: "o", ks: "O", t: "normal" }, { k: "p", ks: "P", t: "normal" }, { k: "[", ks: "{", t: "normal" }, { k: "]", ks: "}", t: "normal" }, { k: "\\", ks: "|", t: "expand" }],
// [{ k: "Caps", t: "caps" }, { k: "a", ks: "A", t: "normal" }, { k: "s", ks: "S", t: "normal" }, { k: "d", ks: "D", t: "normal" }, { k: "f", ks: "F", t: "normal" }, { k: "g", ks: "G", t: "normal" }, { k: "h", ks: "H", t: "normal" }, { k: "j", ks: "J", t: "normal" }, { k: "k", ks: "K", t: "normal" }, { k: "l", ks: "L", t: "normal" }, { k: ";", ks: ":", t: "normal" }, { k: "'", ks: '"', t: "normal" }, { k: "Enter", t: "expand" }],
// [{ k: "Shift", t: "shift" }, { k: "z", ks: "Z", t: "normal" }, { k: "x", ks: "X", t: "normal" }, { k: "c", ks: "C", t: "normal" }, { k: "v", ks: "V", t: "normal" }, { k: "b", ks: "B", t: "normal" }, { k: "n", ks: "N", t: "normal" }, { k: "m", ks: "M", t: "normal" }, { k: ",", ks: "<", t: "normal" }, { k: ".", ks: ">", t: "normal" }, { k: "/", ks: "?", t: "normal" }, { k: "Shift", t: "expand" }],
// [{ k: "Ctrl", t: "control" }, { k: "Fn", t: "normal" }, { k: "Win", t: "normal" }, { k: "Alt", t: "normal" }, { k: "Space", t: "space" }, { k: "Alt", t: "normal" }, { k: "Menu", t: "normal" }, { k: "Ctrl", t: "control" }]
// ]
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "`", labelShift: "~", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "@", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "#", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "^", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "&", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "q", labelShift: "Q", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "w", labelShift: "W", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "e", labelShift: "E", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "r", labelShift: "R", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "t", labelShift: "T", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "y", labelShift: "Y", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "u", labelShift: "U", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "i", labelShift: "I", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "o", labelShift: "O", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "p", labelShift: "P", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "[", labelShift: "{", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "]", labelShift: "}", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "|", shape: "expand", keycode: 43 }
],
[
//{ keytype: "normal", label: "Caps", shape: "caps", keycode: 58 }, // not needed as double-pressing shift does that
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "a", labelShift: "A", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "s", labelShift: "S", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "d", labelShift: "D", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "f", labelShift: "F", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "g", labelShift: "G", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "h", labelShift: "H", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "j", labelShift: "J", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "k", labelShift: "K", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "l", labelShift: "L", shape: "normal", keycode: 38 },
{ keytype: "normal", label: ";", labelShift: ":", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "'", labelShift: '"', shape: "normal", keycode: 40 },
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "z", labelShift: "Z", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "x", labelShift: "X", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "c", labelShift: "C", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "v", labelShift: "V", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "b", labelShift: "B", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "n", labelShift: "N", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "m", labelShift: "M", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: "<", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ">", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "/", labelShift: "?", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 } // optional
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
// { label: "Super", shape: "normal", keycode: 125 }, // dangerous
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
},
qwertz_full: {
name: "QWERTZ - Full",
name_short: "DE",
comment: "Keyboard layout commonly used in German-speaking countries",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "Druck", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Entf", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "^", labelShift: "°", labelAlt: "", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", labelAlt: "¹", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "\"", labelAlt: "²", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "§", labelAlt: "³", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: "$", labelAlt: "¼", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", labelAlt: "½", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: "&", labelAlt: "¬", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "/", labelAlt: "{", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "(", labelAlt: "[", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: ")", labelAlt: "]", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: "=", labelAlt: "}", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "ß", labelShift: "?", labelAlt: "\\", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "´", labelShift: "`", labelAlt: "¸", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "⟵", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab ⇆", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "q", labelShift: "Q", labelAlt: "@", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "w", labelShift: "W", labelAlt: "ſ", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "e", labelShift: "E", labelAlt: "€", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "r", labelShift: "R", labelAlt: "¶", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "t", labelShift: "T", labelAlt: "ŧ", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "z", labelShift: "Z", labelAlt: "←", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "u", labelShift: "U", labelAlt: "↓", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "i", labelShift: "I", labelAlt: "→", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "o", labelShift: "O", labelAlt: "ø", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "p", labelShift: "P", labelAlt: "þ", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "ü", labelShift: "Ü", labelAlt: "¨", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "+", labelShift: "*", labelAlt: "~", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
],
[
//{ keytype: "normal", label: "Umschalt ⇩", shape: "caps", keycode: 58 },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "a", labelShift: "A", labelAlt: "æ", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "s", labelShift: "S", labelAlt: "ſ", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "d", labelShift: "D", labelAlt: "ð", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "f", labelShift: "F", labelAlt: "đ", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "g", labelShift: "G", labelAlt: "ŋ", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "h", labelShift: "H", labelAlt: "ħ", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "j", labelShift: "J", labelAlt: "", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "k", labelShift: "K", labelAlt: "ĸ", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "l", labelShift: "L", labelAlt: "ł", shape: "normal", keycode: 38 },
{ keytype: "normal", label: "ö", labelShift: "Ö", labelAlt: "˝", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "ä", labelShift: 'Ä', labelAlt: "^", shape: "normal", keycode: 40 },
{ keytype: "normal", label: "#", labelShift: '\'', labelAlt: "", shape: "normal", keycode: 43 },
{ keytype: "spacer", label: "", shape: "empty" },
//{ keytype: "normal", label: "↵", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "<", labelShift: ">", labelAlt: "|", shape: "normal", keycode: 86 },
{ keytype: "normal", label: "y", labelShift: "Y", labelAlt: "»", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "x", labelShift: "X", labelAlt: "«", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "c", labelShift: "C", labelAlt: "¢", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "v", labelShift: "V", labelAlt: "„", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "b", labelShift: "B", labelAlt: "“", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "n", labelShift: "N", labelAlt: "”", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "m", labelShift: "M", labelAlt: "µ", shape: "normal", keycode: 50 },
{ keytype: "normal", label: ",", labelShift: ";", labelAlt: "·", shape: "normal", keycode: 51 },
{ keytype: "normal", label: ".", labelShift: ":", labelAlt: "…", shape: "normal", keycode: 52 },
{ keytype: "normal", label: "-", labelShift: "_", labelAlt: "", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", labelShift: "Shift ⇧", labelCaps: "Locked ⇩", shape: "expand", keycode: 54 }, // optional
],
[
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 29 },
//{ keytype: "normal", label: "", shape: "normal", keycode: 125 }, // dangerous
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Leertaste", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "AltGr", shape: "normal", keycode: 100 },
// { label: "Super", shape: "normal", keycode: 126 }, // dangerous
//{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 }, // doesn't work?
{ keytype: "modkey", label: "Strg", shape: "control", keycode: 97 },
{ keytype: "normal", label: "⇦", shape: "normal", keycode: 105 },
{ keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
]
]
}
}

View file

@ -0,0 +1,11 @@
import PopupWindow from '../.widgethacks/popupwindow.js';
import OnScreenKeyboard from "./onscreenkeyboard.js";
export default (id) => PopupWindow({
monitor: id,
anchor: ['bottom'],
name: `osk${id}`,
showClassName: 'osk-show',
hideClassName: 'osk-hide',
child: OnScreenKeyboard({ id: id }),
});

View file

@ -0,0 +1,267 @@
const { Gtk } = 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 { Box, EventBox, Button, Revealer } = Widget;
const { execAsync } = Utils;
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { DEFAULT_OSK_LAYOUT, oskLayouts } from './data_keyboardlayouts.js';
import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js';
const keyboardLayout = oskLayouts[userOptions.onScreenKeyboard.layout] ? userOptions.onScreenKeyboard.layout : DEFAULT_OSK_LAYOUT;
const keyboardJson = oskLayouts[keyboardLayout];
async function startYdotoolIfNeeded() {
const running = exec('pidof ydotool')
if (!running) execAsync(['ydotoold']).catch(print);
}
function releaseAllKeys() {
const keycodes = Array.from(Array(249).keys());
execAsync([`ydotool`, `key`, ...keycodes.map(keycode => `${keycode}:0`)])
.then(console.log('[OSK] Released all keys'))
.catch(print);
}
class ShiftMode {
static Off = new ShiftMode('Off');
static Normal = new ShiftMode('Normal');
static Locked = new ShiftMode('Locked');
constructor(name) {
this.name = name;
}
toString() {
return `ShiftMode.${this.name}`;
}
}
var modsPressed = false;
const TopDecor = () => Box({
vertical: true,
children: [
Box({
hpack: 'center',
className: 'osk-dragline',
homogeneous: true,
children: [EventBox({
setup: setupCursorHoverGrab,
})]
})
]
});
const KeyboardControlButton = (icon, text, runFunction) => Button({
className: 'osk-control-button spacing-h-10',
onClicked: () => runFunction(),
child: Widget.Box({
children: [
MaterialIcon(icon, 'norm'),
Widget.Label({
label: `${text}`,
}),
]
})
})
const KeyboardControls = () => Box({
vertical: true,
className: 'spacing-v-5',
children: [
Button({
className: 'osk-control-button txt-norm icon-material',
onClicked: () => {
releaseAllKeys();
toggleWindowOnAllMonitors('osk');
},
label: 'keyboard_hide',
}),
Button({
className: 'osk-control-button txt-norm',
label: `${keyboardJson['name_short']}`,
}),
Button({
className: 'osk-control-button txt-norm icon-material',
onClicked: () => { // TODO: Proper clipboard widget, since fuzzel doesn't receive mouse inputs
execAsync([`bash`, `-c`, "pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy"]).catch(print);
},
label: 'assignment',
}),
]
})
var shiftMode = ShiftMode.Off;
var shiftButton;
var rightShiftButton;
var allButtons = [];
const KeyboardItself = (kbJson) => {
return Box({
vertical: true,
className: 'spacing-v-5',
children: kbJson.keys.map(row => Box({
vertical: false,
className: 'spacing-h-5',
children: row.map(key => {
return Button({
className: `osk-key osk-key-${key.shape}`,
hexpand: ["space", "expand"].includes(key.shape),
label: key.label,
attribute:
{ key: key },
setup: (button) => {
let pressed = false;
allButtons = allButtons.concat(button);
if (key.keytype == "normal") {
button.connect('pressed', () => { // mouse down
execAsync(`ydotool key ${key.keycode}:1`).catch(print);
});
button.connect('clicked', () => { // release
execAsync(`ydotool key ${key.keycode}:0`).catch(print);
if (shiftMode == ShiftMode.Normal) {
shiftMode = ShiftMode.Off;
if (typeof shiftButton !== 'undefined') {
execAsync(`ydotool key 42:0`).catch(print);
shiftButton.toggleClassName('osk-key-active', false);
}
if (typeof rightShiftButton !== 'undefined') {
execAsync(`ydotool key 54:0`).catch(print);
rightShiftButton.toggleClassName('osk-key-active', false);
}
allButtons.forEach(button => {
if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label;
})
}
});
}
else if (key.keytype == "modkey") {
button.connect('pressed', () => { // release
if (pressed) {
execAsync(`ydotool key ${key.keycode}:0`).catch(print);
button.toggleClassName('osk-key-active', false);
pressed = false;
if (key.keycode == 100) { // Alt Gr button
allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.label; });
}
}
else {
execAsync(`ydotool key ${key.keycode}:1`).catch(print);
button.toggleClassName('osk-key-active', true);
if (!(key.keycode == 42 || key.keycode == 54)) pressed = true;
else switch (shiftMode.name) { // This toggles the shift button state
case "Off": {
shiftMode = ShiftMode.Normal;
allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.labelShift; })
if (typeof shiftButton !== 'undefined') {
shiftButton.toggleClassName('osk-key-active', true);
}
if (typeof rightShiftButton !== 'undefined') {
rightShiftButton.toggleClassName('osk-key-active', true);
}
} break;
case "Normal": {
shiftMode = ShiftMode.Locked;
if (typeof shiftButton !== 'undefined') shiftButton.label = key.labelCaps;
if (typeof rightShiftButton !== 'undefined') rightShiftButton.label = key.labelCaps;
} break;
case "Locked": {
shiftMode = ShiftMode.Off;
if (typeof shiftButton !== 'undefined') {
shiftButton.label = key.label;
shiftButton.toggleClassName('osk-key-active', false);
}
if (typeof rightShiftButton !== 'undefined') {
rightShiftButton.label = key.label;
rightShiftButton.toggleClassName('osk-key-active', false);
}
execAsync(`ydotool key ${key.keycode}:0`).catch(print);
allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label; }
)
};
}
if (key.keycode == 100) { // Alt Gr button
allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.labelAlt; });
}
modsPressed = true;
}
});
if (key.keycode == 42) shiftButton = button;
else if (key.keycode == 54) rightShiftButton = button;
}
}
})
})
}))
})
}
const KeyboardWindow = () => Box({
vexpand: true,
hexpand: true,
vertical: true,
className: 'osk-window spacing-v-5',
children: [
TopDecor(),
Box({
className: 'osk-body spacing-h-10',
children: [
KeyboardControls(),
Widget.Box({ className: 'separator-line' }),
KeyboardItself(keyboardJson),
],
})
],
setup: (self) => self.hook(App, (self, name, visible) => { // Update on open
if (!name) return;
if (name.startsWith('osk') && visible) {
self.setCss(`margin-bottom: -0px;`);
}
}),
});
export default ({ id }) => {
const kbWindow = KeyboardWindow();
const gestureEvBox = EventBox({ child: kbWindow })
const gesture = Gtk.GestureDrag.new(gestureEvBox);
gesture.connect('drag-begin', async () => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
Hyprland.messageAsync('j/cursorpos').then((out) => {
gesture.startY = JSON.parse(out).y;
}).catch(print);
} catch {
return;
}
});
gesture.connect('drag-update', async () => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
Hyprland.messageAsync('j/cursorpos').then((out) => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset > 0) return;
kbWindow.setCss(`
margin-bottom: ${offset}px;
`);
}).catch(print);
} catch {
return;
}
});
gesture.connect('drag-end', () => {
var offset = gesture.get_offset()[2];
if (offset > 50) {
App.closeWindow(`osk${id}`);
}
else {
kbWindow.setCss(`
transition: margin-bottom 170ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-bottom: 0px;
`);
}
})
return gestureEvBox;
};

View file

@ -0,0 +1,28 @@
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
function moveClientToWorkspace(address, workspace) {
Utils.execAsync(['bash', '-c', `hyprctl dispatch movetoworkspacesilent ${workspace},address:${address} &`]);
}
export function dumpToWorkspace(from, to) {
if (from == to) return;
Hyprland.clients.forEach(client => {
if (client.workspace.id == from) {
moveClientToWorkspace(client.address, to);
}
});
}
export function swapWorkspace(workspaceA, workspaceB) {
if (workspaceA == workspaceB) return;
const clientsA = [];
const clientsB = [];
Hyprland.clients.forEach(client => {
if (client.workspace.id == workspaceA) clientsA.push(client.address);
if (client.workspace.id == workspaceB) clientsB.push(client.address);
});
clientsA.forEach((address) => moveClientToWorkspace(address, workspaceB));
clientsB.forEach((address) => moveClientToWorkspace(address, workspaceA));
}

View file

@ -0,0 +1,28 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { SearchAndWindows } from "./windowcontent.js";
import PopupWindow from '../.widgethacks/popupwindow.js';
import { clickCloseRegion } from '../.commonwidgets/clickcloseregion.js';
export default (id = '') => PopupWindow({
name: `overview${id}`,
// exclusivity: 'ignore',
keymode: 'on-demand',
visible: false,
anchor: ['top', 'bottom', 'left', 'right'],
layer: 'top',
child: Widget.Box({
vertical: true,
children: [
clickCloseRegion({ name: 'overview', multimonitor: false, expand: false }),
Widget.Box({
children: [
clickCloseRegion({ name: 'overview', multimonitor: false }),
SearchAndWindows(),
clickCloseRegion({ name: 'overview', multimonitor: false }),
]
}),
clickCloseRegion({ name: 'overview', multimonitor: false }),
]
}),
})

View file

@ -0,0 +1,165 @@
const { Gio, GLib } = imports.gi;
import App from 'resource:///com/github/Aylur/ags/app.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { execAsync, exec } = Utils;
import Todo from "../../services/todo.js";
import { darkMode } from '../.miscutils/system.js';
export function hasUnterminatedBackslash(inputString) {
// Use a regular expression to match a trailing odd number of backslashes
const regex = /\\+$/;
return regex.test(inputString);
}
export function launchCustomCommand(command) {
const args = command.toLowerCase().split(' ');
if (args[0] == '>raw') { // Mouse raw input
Utils.execAsync('hyprctl -j getoption input:accel_profile')
.then((output) => {
const value = JSON.parse(output)["str"].trim();
if (value != "[[EMPTY]]" && value != "") {
execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print);
}
else {
execAsync(['bash', '-c', `hyprctl keyword input:accel_profile flat`]).catch(print);
}
})
}
else if (args[0] == '>img') { // Change wallpaper
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchwall.sh`, `&`]).catch(print);
}
else if (args[0] == '>color') { // Generate colorscheme from color picker
if (!args[1])
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh --pick`, `&`]).catch(print);
else if (args[1][0] === '#')
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${args[1]}"`, `&`]).catch(print);
}
else if (args[0] == '>light') { // Light mode
darkMode.value = false;
}
else if (args[0] == '>dark') { // Dark mode
darkMode.value = true;
}
else if (args[0] == '>badapple') { // Black and white
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && sed -i "3s/.*/monochrome/" ${GLib.get_user_state_dir()}/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`]))
.catch(print);
}
else if (args[0] == '>adw' || args[0] == '>adwaita') {
const ADWAITA_BLUE = "#3584E4";
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh "${ADWAITA_BLUE}" --no-gradience`, `&`])
.catch(print);
}
else if (args[0] == '>grad' || args[0] == '>gradience') {
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --yes-gradience`, `&`])
.catch(print);
}
else if (args[0] == '>nograd' || args[0] == '>nogradience') {
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh - --no-gradience`, `&`])
.catch(print);
}
else if (args[0] == '>material') { // Use material colors
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && echo "material" > ${GLib.get_user_state_dir()}/ags/user/colorbackend.txt`]).catch(print)
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
.catch(print);
}
else if (args[0] == '>pywal') { // Use Pywal (ik it looks shit but I'm not removing)
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_state_dir()}/ags/user && echo "pywal" > ${GLib.get_user_state_dir()}/ags/user/colorbackend.txt`]).catch(print)
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
.catch(print);
}
else if (args[0] == '>todo') { // Todo
Todo.add(args.slice(1).join(' '));
}
else if (args[0] == '>shutdown') { // Shut down
execAsync([`bash`, `-c`, `systemctl poweroff || loginctl poweroff`]).catch(print);
}
else if (args[0] == '>reboot') { // Reboot
execAsync([`bash`, `-c`, `systemctl reboot || loginctl reboot`]).catch(print);
}
else if (args[0] == '>sleep') { // Sleep
execAsync([`bash`, `-c`, `systemctl suspend || loginctl suspend`]).catch(print);
}
else if (args[0] == '>logout') { // Log out
execAsync([`bash`, `-c`, `pkill Hyprland || pkill sway`]).catch(print);
}
}
export function execAndClose(command, terminal) {
App.closeWindow('overview');
if (terminal) {
execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${command}"`, `&`]).catch(print);
}
else
execAsync(command).catch(print);
}
export function couldBeMath(str) {
const regex = /^[0-9.+*/-]/;
return regex.test(str);
}
export function expandTilde(path) {
if (path.startsWith('~')) {
return GLib.get_home_dir() + path.slice(1);
} else {
return path;
}
}
function getFileIcon(fileInfo) {
let icon = fileInfo.get_icon();
if (icon) {
// Get the icon's name
return icon.get_names()[0];
} else {
// Default icon for files
return 'text-x-generic';
}
}
export function ls({ path = '~', silent = false }) {
let contents = [];
try {
let expandedPath = expandTilde(path);
if (expandedPath.endsWith('/'))
expandedPath = expandedPath.slice(0, -1);
let folder = Gio.File.new_for_path(expandedPath);
let enumerator = folder.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
let fileInfo;
while ((fileInfo = enumerator.next_file(null)) !== null) {
let fileName = fileInfo.get_display_name();
let fileType = fileInfo.get_file_type();
let item = {
parentPath: expandedPath,
name: fileName,
type: fileType === Gio.FileType.DIRECTORY ? 'folder' : 'file',
icon: getFileIcon(fileInfo),
};
// Add file extension for files
if (fileType === Gio.FileType.REGULAR) {
let fileExtension = fileName.split('.').pop();
item.type = `${fileExtension}`;
}
contents.push(item);
contents.sort((a, b) => {
const aIsFolder = a.type.startsWith('folder');
const bIsFolder = b.type.startsWith('folder');
if (aIsFolder && !bIsFolder) {
return -1;
} else if (!aIsFolder && bIsFolder) {
return 1;
} else {
return a.name.localeCompare(b.name); // Sort alphabetically within folders and files
}
});
}
} catch (e) {
if (!silent) console.log(e);
}
return contents;
}

View file

@ -0,0 +1,431 @@
// TODO
// - Make client destroy/create not destroy and recreate the whole thing
// - Active ws hook optimization: only update when moving to next group
//
const { Gdk, Gtk } = imports.gi;
const { Gravity } = imports.gi.Gdk;
import App from 'resource:///com/github/Aylur/ags/app.js';
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 Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const { execAsync, exec } = Utils;
import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js';
import { dumpToWorkspace, swapWorkspace } from "./actions.js";
import { iconExists, substitute } from "../.miscutils/icons.js";
import { monitors } from '../.commondata/hyprlanddata.js';
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
const NUM_OF_WORKSPACES_SHOWN = userOptions.overview.numOfCols * userOptions.overview.numOfRows;
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
const overviewTick = Variable(false);
export default (overviewMonitor = 0) => {
const clientMap = new Map();
const ContextMenuWorkspaceArray = ({ label, actionFunc, thisWorkspace }) => Widget.MenuItem({
label: `${label}`,
setup: (menuItem) => {
let submenu = new Gtk.Menu();
submenu.className = 'menu';
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
const startWorkspace = offset + 1;
const endWorkspace = startWorkspace + NUM_OF_WORKSPACES_SHOWN - 1;
for (let i = startWorkspace; i <= endWorkspace; i++) {
let button = new Gtk.MenuItem({
label: `Workspace ${i}`
});
button.connect("activate", () => {
// execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print);
actionFunc(thisWorkspace, i);
overviewTick.setValue(!overviewTick.value);
});
submenu.append(button);
}
menuItem.set_reserve_indicator(true);
menuItem.set_submenu(submenu);
}
})
const Window = ({ address, at: [x, y], size: [w, h], workspace: { id, name }, class: c, initialClass, monitor, title, xwayland }, screenCoords) => {
const revealInfoCondition = (Math.min(w, h) * userOptions.overview.scale > 70);
if (w <= 0 || h <= 0 || (c === '' && title === '')) return null;
// Non-primary monitors
if (screenCoords.x != 0) x -= screenCoords.x;
if (screenCoords.y != 0) y -= screenCoords.y;
// Other offscreen adjustments
if (x + w <= 0) x += (Math.floor(x / monitors[monitor].width) * monitors[monitor].width);
else if (x < 0) { w = x + w; x = 0; }
if (y + h <= 0) x += (Math.floor(y / monitors[monitor].height) * monitors[monitor].height);
else if (y < 0) { h = y + h; y = 0; }
// Truncate if offscreen
if (x + w > monitors[monitor].width) w = monitors[monitor].width - x;
if (y + h > monitors[monitor].height) h = monitors[monitor].height - y;
if(c.length == 0) c = initialClass;
const iconName = substitute(c);
const appIcon = iconExists(iconName) ? Widget.Icon({
icon: iconName,
size: Math.min(w, h) * userOptions.overview.scale / 2.5,
}) : MaterialIcon('terminal', 'gigantic', {
css: `font-size: ${Math.min(w, h) * userOptions.overview.scale / 2.5}px`,
});
return Widget.Button({
attribute: {
address, x, y, w, h, ws: id,
updateIconSize: (self) => {
appIcon.size = Math.min(self.attribute.w, self.attribute.h) * userOptions.overview.scale / 2.5;
},
},
className: 'overview-tasks-window',
hpack: 'start',
vpack: 'start',
css: `
margin-left: ${Math.round(x * userOptions.overview.scale)}px;
margin-top: ${Math.round(y * userOptions.overview.scale)}px;
margin-right: -${Math.round((x + w) * userOptions.overview.scale)}px;
margin-bottom: -${Math.round((y + h) * userOptions.overview.scale)}px;
`,
onClicked: (self) => {
Hyprland.messageAsync(`dispatch focuswindow address:${address}`);
App.closeWindow('overview');
},
onMiddleClickRelease: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`),
onSecondaryClick: (button) => {
button.toggleClassName('overview-tasks-window-selected', true);
const menu = Widget.Menu({
className: 'menu',
children: [
Widget.MenuItem({
child: Widget.Label({
xalign: 0,
label: "Close (Middle-click)",
}),
onActivate: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`),
}),
ContextMenuWorkspaceArray({
label: "Dump windows to workspace",
actionFunc: dumpToWorkspace,
thisWorkspace: Number(id)
}),
ContextMenuWorkspaceArray({
label: "Swap windows with workspace",
actionFunc: swapWorkspace,
thisWorkspace: Number(id)
}),
],
});
menu.connect("deactivate", () => {
button.toggleClassName('overview-tasks-window-selected', false);
})
menu.connect("selection-done", () => {
button.toggleClassName('overview-tasks-window-selected', false);
})
menu.popup_at_widget(button.get_parent(), Gravity.SOUTH, Gravity.NORTH, null); // Show menu below the button
button.connect("destroy", () => menu.destroy());
},
child: Widget.Box({
homogeneous: true,
child: Widget.Box({
vertical: true,
vpack: 'center',
children: [
appIcon,
// TODO: Add xwayland tag instead of just having italics
Widget.Revealer({
transition: 'slide_right',
revealChild: revealInfoCondition,
child: Widget.Revealer({
transition: 'slide_down',
revealChild: revealInfoCondition,
child: Widget.Label({
maxWidthChars: 1, // Doesn't matter what number
truncate: 'end',
className: `margin-top-5 ${xwayland ? 'txt txt-italic' : 'txt'}`,
css: `
font-size: ${Math.min(monitors[monitor].width, monitors[monitor].height) * userOptions.overview.scale / 14.6}px;
margin: 0px ${Math.min(monitors[monitor].width, monitors[monitor].height) * userOptions.overview.scale / 10}px;
`,
// If the title is too short, include the class
label: (title.length <= 1 ? `${c}: ${title}` : title),
})
})
})
]
})
}),
tooltipText: `${c}: ${title}`,
setup: (button) => {
setupCursorHoverGrab(button);
button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.MOVE);
button.drag_source_set_icon_name(substitute(c));
button.connect('drag-begin', (button) => { // On drag start, add the dragging class
button.toggleClassName('overview-tasks-window-dragging', true);
});
button.connect('drag-data-get', (_w, _c, data) => { // On drag finish, give address
data.set_text(address, address.length);
button.toggleClassName('overview-tasks-window-dragging', false);
});
},
});
}
const Workspace = (index) => {
// const fixed = Widget.Fixed({
// attribute: {
// put: (widget, x, y) => {
// fixed.put(widget, x, y);
// },
// move: (widget, x, y) => {
// fixed.move(widget, x, y);
// },
// }
// });
const fixed = Widget.Box({
attribute: {
put: (widget, x, y) => {
if (!widget.attribute) return;
// Note: x and y are already multiplied by userOptions.overview.scale
const newCss = `
margin-left: ${Math.round(x)}px;
margin-top: ${Math.round(y)}px;
margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px;
margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px;
`;
widget.css = newCss;
fixed.pack_start(widget, false, false, 0);
},
move: (widget, x, y) => {
if (!widget) return;
if (!widget.attribute) return;
// Note: x and y are already multiplied by userOptions.overview.scale
const newCss = `
margin-left: ${Math.round(x)}px;
margin-top: ${Math.round(y)}px;
margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px;
margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px;
`;
widget.css = newCss;
},
}
})
const WorkspaceNumber = ({ index, ...rest }) => Widget.Label({
className: 'overview-tasks-workspace-number',
label: `${index}`,
css: `
margin: ${Math.min(monitors[overviewMonitor].width, monitors[overviewMonitor].height) * userOptions.overview.scale * userOptions.overview.wsNumMarginScale}px;
font-size: ${monitors[overviewMonitor].height * userOptions.overview.scale * userOptions.overview.wsNumScale}px;
`,
setup: (self) => self.hook(Hyprland.active.workspace, (self) => {
// Update when going to new ws group
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN);
self.label = `${currentGroup * NUM_OF_WORKSPACES_SHOWN + index}`;
}),
...rest,
})
const widget = Widget.Box({
className: 'overview-tasks-workspace',
vpack: 'center',
// Rounding and adding 1px to minimum width/height to work around scaling inaccuracy:
css: `
min-width: ${1 + Math.round(monitors[overviewMonitor].width * userOptions.overview.scale)}px;
min-height: ${1 + Math.round(monitors[overviewMonitor].height * userOptions.overview.scale)}px;
`,
children: [Widget.EventBox({
hexpand: true,
onPrimaryClick: () => {
Hyprland.messageAsync(`dispatch workspace ${index}`);
App.closeWindow('overview');
},
setup: (eventbox) => {
eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY);
eventbox.connect('drag-data-received', (_w, _c, _x, _y, data) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
Hyprland.messageAsync(`dispatch movetoworkspacesilent ${index + offset},address:${data.get_text()}`)
overviewTick.setValue(!overviewTick.value);
});
},
child: Widget.Overlay({
child: Widget.Box({}),
overlays: [
WorkspaceNumber({ index: index, hpack: 'start', vpack: 'start' }),
fixed
]
}),
})],
});
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
fixed.attribute.put(WorkspaceNumber(offset + index), 0, 0);
widget.clear = () => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
clientMap.forEach((client, address) => {
if (!client) return;
if ((client.attribute.ws <= offset || client.attribute.ws > offset + NUM_OF_WORKSPACES_SHOWN) ||
(client.attribute.ws == offset + index)) {
client.destroy();
client = null;
clientMap.delete(address);
}
});
}
widget.set = (clientJson, screenCoords) => {
let c = clientMap.get(clientJson.address);
if (c) {
if (c.attribute?.ws !== clientJson.workspace.id) {
c.destroy();
c = null;
clientMap.delete(clientJson.address);
}
else if (c) {
c.attribute.w = clientJson.size[0];
c.attribute.h = clientJson.size[1];
c.attribute.updateIconSize(c);
fixed.attribute.move(c,
Math.max(0, clientJson.at[0] * userOptions.overview.scale),
Math.max(0, clientJson.at[1] * userOptions.overview.scale)
);
return;
}
}
const newWindow = Window(clientJson, screenCoords);
if (newWindow === null) return;
// clientMap.set(clientJson.address, newWindow);
fixed.attribute.put(newWindow,
Math.max(0, newWindow.attribute.x * userOptions.overview.scale),
Math.max(0, newWindow.attribute.y * userOptions.overview.scale)
);
clientMap.set(clientJson.address, newWindow);
};
widget.unset = (clientAddress) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
let c = clientMap.get(clientAddress);
if (!c) return;
c.destroy();
c = null;
clientMap.delete(clientAddress);
};
widget.show = () => {
fixed.show_all();
}
return widget;
};
const arr = (s, n) => {
const array = [];
for (let i = 0; i < n; i++)
array.push(s + i);
return array;
};
const OverviewRow = ({ startWorkspace, workspaces, windowName = 'overview' }) => Widget.Box({
children: arr(startWorkspace, workspaces).map(Workspace),
attribute: {
workspaceGroup: Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN),
monitorMap: [],
getMonitorMap: (box) => {
execAsync('hyprctl -j monitors').then(monitors => {
box.attribute.monitorMap = JSON.parse(monitors).reduce((acc, item) => {
acc[item.id] = { x: item.x, y: item.y };
return acc;
}, {});
});
},
update: (box) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
Hyprland.messageAsync('j/clients').then(clients => {
const allClients = JSON.parse(clients);
const kids = box.get_children();
kids.forEach(kid => kid.clear());
for (let i = 0; i < allClients.length; i++) {
const client = allClients[i];
const childID = client.workspace.id - (offset + startWorkspace);
if (offset + startWorkspace <= client.workspace.id &&
client.workspace.id <= offset + startWorkspace + workspaces) {
const screenCoords = box.attribute.monitorMap[client.monitor];
if (kids[childID]) {
kids[childID].set(client, screenCoords);
}
continue;
}
}
kids.forEach(kid => kid.show());
}).catch(print);
},
updateWorkspace: (box, id) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
if (!( // Not in range, ignore
offset + startWorkspace <= id &&
id <= offset + startWorkspace + workspaces
)) return;
// if (!App.getWindow(windowName)?.visible) return;
Hyprland.messageAsync('j/clients').then(clients => {
const allClients = JSON.parse(clients);
const kids = box.get_children();
for (let i = 0; i < allClients.length; i++) {
const client = allClients[i];
if (client.workspace.id != id) continue;
const screenCoords = box.attribute.monitorMap[client.monitor];
kids[id - (offset + startWorkspace)]?.set(client, screenCoords);
}
kids[id - (offset + startWorkspace)]?.show();
}).catch(print);
},
},
setup: (box) => {
box.attribute.getMonitorMap(box);
box
.hook(overviewTick, (box) => box.attribute.update(box))
.hook(Hyprland, (box, clientAddress) => {
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN;
const kids = box.get_children();
const client = Hyprland.getClient(clientAddress);
if (!client) return;
const id = client.workspace.id;
box.attribute.updateWorkspace(box, id);
kids[id - (offset + startWorkspace)]?.unset(clientAddress);
}, 'client-removed')
.hook(Hyprland, (box, clientAddress) => {
const client = Hyprland.getClient(clientAddress);
if (!client) return;
box.attribute.updateWorkspace(box, client.workspace.id);
}, 'client-added')
.hook(Hyprland.active.workspace, (box) => {
// Full update when going to new ws group
const previousGroup = box.attribute.workspaceGroup;
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN);
if (currentGroup !== previousGroup) {
if (!App.getWindow(windowName) || !App.getWindow(windowName).visible) return;
box.attribute.update(box);
box.attribute.workspaceGroup = currentGroup;
}
})
.hook(App, (box, name, visible) => { // Update on open
if (name == 'overview' && visible) box.attribute.update(box);
})
},
});
return Widget.Revealer({
revealChild: true,
// hpack to prevent unneeded expansion in overview-tasks-workspace:
hpack: 'center',
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Box({
vertical: true,
className: 'overview-tasks',
children: Array.from({ length: userOptions.overview.numOfRows }, (_, index) =>
OverviewRow({
startWorkspace: 1 + index * userOptions.overview.numOfCols,
workspaces: userOptions.overview.numOfCols,
})
)
}),
});
}

View file

@ -0,0 +1,189 @@
const { Gtk } = 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 { execAsync, exec } = Utils;
import { searchItem } from './searchitem.js';
import { execAndClose, couldBeMath, launchCustomCommand } from './miscfunctions.js';
import GeminiService from '../../services/gemini.js';
export const NoResultButton = () => searchItem({
materialIconName: 'Error',
name: "Search invalid",
content: "No results found!",
onActivate: () => {
App.closeWindow('overview');
},
});
export const DirectoryButton = ({ parentPath, name, type, icon }) => {
const actionText = Widget.Revealer({
revealChild: false,
transition: "crossfade",
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Label({
className: 'overview-search-results-txt txt txt-small txt-action',
label: 'Open',
})
});
const actionTextRevealer = Widget.Revealer({
revealChild: false,
transition: "slide_left",
transitionDuration: userOptions.animations.durationSmall,
child: actionText,
});
return Widget.Button({
className: 'overview-search-result-btn',
onClicked: () => {
App.closeWindow('overview');
execAsync(['bash', '-c', `xdg-open '${parentPath}/${name}'`, `&`]).catch(print);
},
child: Widget.Box({
children: [
Widget.Box({
vertical: false,
children: [
Widget.Box({
className: 'overview-search-results-icon',
homogeneous: true,
child: Widget.Icon({
icon: icon,
}),
}),
Widget.Label({
className: 'overview-search-results-txt txt txt-norm',
label: name,
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
]
})
]
}),
setup: (self) => self
.on('focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
})
.on('focus-out-event', (button) => {
actionText.revealChild = false;
actionTextRevealer.revealChild = false;
})
,
})
}
export const CalculationResultButton = ({ result, text }) => searchItem({
materialIconName: 'calculate',
name: `Math result`,
actionName: "Copy",
content: `${result}`,
onActivate: () => {
App.closeWindow('overview');
execAsync(['wl-copy', `${result}`]).catch(print);
},
});
export const DesktopEntryButton = (app) => {
const actionText = Widget.Revealer({
revealChild: false,
transition: "crossfade",
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Label({
className: 'overview-search-results-txt txt txt-small txt-action',
label: 'Launch',
})
});
const actionTextRevealer = Widget.Revealer({
revealChild: false,
transition: "slide_left",
transitionDuration: userOptions.animations.durationSmall,
child: actionText,
});
return Widget.Button({
className: 'overview-search-result-btn',
onClicked: () => {
App.closeWindow('overview');
app.launch();
},
child: Widget.Box({
children: [
Widget.Box({
vertical: false,
children: [
Widget.Box({
className: 'overview-search-results-icon',
homogeneous: true,
child: Widget.Icon({
icon: app.iconName,
}),
}),
Widget.Label({
className: 'overview-search-results-txt txt txt-norm',
label: app.name,
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
]
})
]
}),
setup: (self) => self
.on('focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
})
.on('focus-out-event', (button) => {
actionText.revealChild = false;
actionTextRevealer.revealChild = false;
})
,
})
}
export const ExecuteCommandButton = ({ command, terminal = false }) => searchItem({
materialIconName: `${terminal ? 'terminal' : 'settings_b_roll'}`,
name: `Run command`,
actionName: `Execute ${terminal ? 'in terminal' : ''}`,
content: `${command}`,
onActivate: () => execAndClose(command, terminal),
extraClassName: 'techfont',
})
export const CustomCommandButton = ({ text = '' }) => searchItem({
materialIconName: 'settings_suggest',
name: 'Action',
actionName: 'Run',
content: `${text}`,
onActivate: () => {
App.closeWindow('overview');
launchCustomCommand(text);
},
});
export const SearchButton = ({ text = '' }) => searchItem({
materialIconName: 'travel_explore',
name: 'Search the web',
actionName: 'Go',
content: `${text}`,
onActivate: () => {
App.closeWindow('overview');
let search = userOptions.search.engineBaseUrl + text;
for (let site of userOptions.search.excludedSites) {
if (site) search += ` -site:${site}`;
}
execAsync(['bash', '-c', `xdg-open '${search}' &`]).catch(print);
},
});
export const AiButton = ({ text }) => searchItem({
materialIconName: 'chat_paste_go',
name: 'Ask Gemini',
actionName: 'Ask',
content: `${text}`,
onActivate: () => {
GeminiService.send(text);
App.closeWindow('overview');
App.openWindow('sideleft');
},
});

View file

@ -0,0 +1,65 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
export const searchItem = ({ materialIconName, name, actionName, content, onActivate, extraClassName = '', ...rest }) => {
const actionText = Widget.Revealer({
revealChild: false,
transition: "crossfade",
transitionDuration: userOptions.animations.durationLarge,
child: Widget.Label({
className: 'overview-search-results-txt txt txt-small txt-action',
label: `${actionName}`,
})
});
const actionTextRevealer = Widget.Revealer({
revealChild: false,
transition: "slide_left",
transitionDuration: userOptions.animations.durationSmall,
child: actionText,
})
return Widget.Button({
className: `overview-search-result-btn txt ${extraClassName}`,
onClicked: onActivate,
child: Widget.Box({
children: [
Widget.Box({
vertical: false,
children: [
Widget.Label({
className: `icon-material overview-search-results-icon`,
label: `${materialIconName}`,
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
hpack: 'start',
className: 'overview-search-results-txt txt-smallie txt-subtext',
label: `${name}`,
truncate: "end",
}),
Widget.Label({
hpack: 'start',
className: 'overview-search-results-txt txt-norm',
label: `${content}`,
truncate: "end",
}),
]
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
],
})
]
}),
setup: (self) => self
.on('focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
})
.on('focus-out-event', (button) => {
actionText.revealChild = false;
actionTextRevealer.revealChild = false;
})
,
});
}

View file

@ -0,0 +1,213 @@
const { Gdk, Gtk } = 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';
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
const { execAsync, exec } = Utils;
import { execAndClose, expandTilde, hasUnterminatedBackslash, couldBeMath, launchCustomCommand, ls } from './miscfunctions.js';
import {
CalculationResultButton, CustomCommandButton, DirectoryButton,
DesktopEntryButton, ExecuteCommandButton, SearchButton, AiButton, NoResultButton,
} from './searchbuttons.js';
import { checkKeybind } from '../.widgetutils/keybind.js';
import GeminiService from '../../services/gemini.js';
// Add math funcs
const { abs, sin, cos, tan, cot, asin, acos, atan, acot } = Math;
const pi = Math.PI;
// trigonometric funcs for deg
const sind = x => sin(x * pi / 180);
const cosd = x => cos(x * pi / 180);
const tand = x => tan(x * pi / 180);
const cotd = x => cot(x * pi / 180);
const asind = x => asin(x) * 180 / pi;
const acosd = x => acos(x) * 180 / pi;
const atand = x => atan(x) * 180 / pi;
const acotd = x => acot(x) * 180 / pi;
const MAX_RESULTS = 10;
const OVERVIEW_SCALE = 0.18; // = overview workspace box / screen size
const OVERVIEW_WS_NUM_SCALE = 0.09;
const OVERVIEW_WS_NUM_MARGIN_SCALE = 0.07;
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)];
function iconExists(iconName) {
let iconTheme = Gtk.IconTheme.get_default();
return iconTheme.has_icon(iconName);
}
const OptionalOverview = async () => {
try {
return (await import('./overview_hyprland.js')).default();
} catch {
return Widget.Box({});
// return (await import('./overview_hyprland.js')).default();
}
};
const overviewContent = await OptionalOverview();
export const SearchAndWindows = () => {
var _appSearchResults = [];
const resultsBox = Widget.Box({
className: 'overview-search-results',
vertical: true,
});
const resultsRevealer = Widget.Revealer({
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
transition: 'slide_down',
// duration: 200,
hpack: 'center',
child: resultsBox,
});
const entryPromptRevealer = Widget.Revealer({
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
revealChild: true,
hpack: 'center',
child: Widget.Label({
className: 'overview-search-prompt txt-small txt',
label: getString('Type to search')
}),
});
const entryIconRevealer = Widget.Revealer({
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
hpack: 'end',
child: Widget.Label({
className: 'txt txt-large icon-material overview-search-icon',
label: 'search',
}),
});
const entryIcon = Widget.Box({
className: 'overview-search-prompt-box',
setup: box => box.pack_start(entryIconRevealer, true, true, 0),
});
const entry = Widget.Entry({
className: 'overview-search-box txt-small txt',
hpack: 'center',
onAccept: (self) => { // This is when you hit Enter
resultsBox.children[0].onClicked();
},
onChange: (entry) => { // this is when you type
const isAction = entry.text[0] == '>';
const isDir = (['/', '~'].includes(entry.text[0]));
resultsBox.get_children().forEach(ch => ch.destroy());
// check empty if so then dont do stuff
if (entry.text == '') {
resultsRevealer.revealChild = false;
overviewContent.revealChild = true;
entryPromptRevealer.revealChild = true;
entryIconRevealer.revealChild = false;
entry.toggleClassName('overview-search-box-extended', false);
return;
}
const text = entry.text;
resultsRevealer.revealChild = true;
overviewContent.revealChild = false;
entryPromptRevealer.revealChild = false;
entryIconRevealer.revealChild = true;
entry.toggleClassName('overview-search-box-extended', true);
_appSearchResults = Applications.query(text);
// Calculate
if (userOptions.search.enableFeatures.mathResults && couldBeMath(text)) { // Eval on typing is dangerous; this is a small workaround.
try {
const fullResult = eval(text.replace(/\^/g, "**"));
resultsBox.add(CalculationResultButton({ result: fullResult, text: text }));
} catch (e) {
// console.log(e);
}
}
if (userOptions.search.enableFeatures.directorySearch && isDir) {
var contents = [];
contents = ls({ path: text, silent: true });
contents.forEach((item) => {
resultsBox.add(DirectoryButton(item));
})
}
if (userOptions.search.enableFeatures.actions && isAction) { // Eval on typing is dangerous, this is a workaround.
resultsBox.add(CustomCommandButton({ text: entry.text }));
}
// Add application entries
let appsToAdd = MAX_RESULTS;
_appSearchResults.forEach(app => {
if (appsToAdd == 0) return;
resultsBox.add(DesktopEntryButton(app));
appsToAdd--;
});
// Fallbacks
// if the first word is an actual command
if (userOptions.search.enableFeatures.commands && !isAction && !hasUnterminatedBackslash(text) && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') {
resultsBox.add(ExecuteCommandButton({ command: entry.text, terminal: entry.text.startsWith('sudo') }));
}
// Add fallback: search
if (userOptions.search.enableFeatures.aiSearch)
resultsBox.add(AiButton({ text: entry.text }));
if (userOptions.search.enableFeatures.webSearch)
resultsBox.add(SearchButton({ text: entry.text }));
if (resultsBox.children.length == 0) resultsBox.add(NoResultButton());
resultsBox.show_all();
},
});
return Widget.Box({
vertical: true,
children: [
Widget.Box({
hpack: 'center',
children: [
entry,
Widget.Box({
className: 'overview-search-icon-box',
setup: (box) => {
box.pack_start(entryPromptRevealer, true, true, 0)
},
}),
entryIcon,
]
}),
overviewContent,
resultsRevealer,
],
setup: (self) => self
.hook(App, (_b, name, visible) => {
if (name == 'overview' && !visible) {
resultsBox.children = [];
entry.set_text('');
}
})
.on('key-press-event', (widget, event) => { // Typing
const keyval = event.get_keyval()[1];
const modstate = event.get_state()[1];
if (checkKeybind(event, userOptions.keybinds.overview.altMoveLeft))
entry.set_position(Math.max(entry.get_position() - 1, 0));
else if (checkKeybind(event, userOptions.keybinds.overview.altMoveRight))
entry.set_position(Math.min(entry.get_position() + 1, entry.get_text().length));
else if (checkKeybind(event, userOptions.keybinds.overview.deleteToEnd)) {
const text = entry.get_text();
const pos = entry.get_position();
const newText = text.slice(0, pos);
entry.set_text(newText);
entry.set_position(newText.length);
}
else if (!(modstate & Gdk.ModifierType.CONTROL_MASK)) { // Ctrl not held
if (keyval >= 32 && keyval <= 126 && widget != entry) {
Utils.timeout(1, () => entry.grab_focus());
entry.set_text(entry.text + String.fromCharCode(keyval));
entry.set_position(-1);
}
}
})
,
});
};

View file

@ -0,0 +1,38 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import { enableClickthrough } from "../.widgetutils/clickthrough.js";
import { RoundedCorner } from "../.commonwidgets/cairo_roundedcorner.js";
if(userOptions.appearance.fakeScreenRounding === 2) Hyprland.connect('event', (service, name, data) => {
if (name == 'fullscreen') {
const monitor = Hyprland.active.monitor.id;
if (data == '1') {
for (const window of App.windows) {
if (window.name.startsWith("corner") && window.name.endsWith(monitor)) {
App.closeWindow(window.name);
}
}
} else {
for (const window of App.windows) {
if (window.name.startsWith("corner") && window.name.endsWith(monitor)) {
App.openWindow(window.name);
}
}
}
}
})
export default (monitor = 0, where = 'bottom left', useOverlayLayer = true) => {
const positionString = where.replace(/\s/, ""); // remove space
return Widget.Window({
monitor,
name: `corner${positionString}${monitor}`,
layer: useOverlayLayer ? 'overlay' : 'top',
anchor: where.split(' '),
exclusivity: 'ignore',
visible: true,
child: RoundedCorner(positionString, { className: 'corner-black', }),
setup: enableClickthrough,
});
}

View file

@ -0,0 +1,14 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import SessionScreen from "./sessionscreen.js";
import PopupWindow from '../.widgethacks/popupwindow.js';
export default (id = 0) => PopupWindow({ // On-screen keyboard
monitor: id,
name: `session${id}`,
visible: false,
keymode: 'on-demand',
layer: 'overlay',
exclusivity: 'ignore',
anchor: ['top', 'bottom', 'left', 'right'],
child: SessionScreen({ id: id }),
})

View file

@ -0,0 +1,134 @@
// This is for the cool memory indicator on the sidebar
// For the right pill of the bar, see system.js
const { Gdk, Gtk } = 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';
import { monitors } from '../.commondata/hyprlanddata.js';
const { exec, execAsync } = Utils;
const SessionButton = (name, icon, command, props = {}, colorid = 0) => {
const buttonDescription = Widget.Revealer({
vpack: 'end',
transitionDuration: userOptions.animations.durationSmall,
transition: 'slide_down',
revealChild: false,
child: Widget.Label({
className: 'txt-smaller session-button-desc',
label: name,
}),
});
return Widget.Button({
onClicked: command,
className: `session-button session-color-${colorid}`,
child: Widget.Overlay({
className: 'session-button-box',
child: Widget.Label({
vexpand: true,
className: 'icon-material',
label: icon,
}),
overlays: [
buttonDescription,
]
}),
onHover: (button) => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
button.get_window().set_cursor(cursor);
buttonDescription.revealChild = true;
},
onHoverLost: (button) => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
buttonDescription.revealChild = false;
},
setup: (self) => self
.on('focus-in-event', (self) => {
buttonDescription.revealChild = true;
self.toggleClassName('session-button-focused', true);
})
.on('focus-out-event', (self) => {
buttonDescription.revealChild = false;
self.toggleClassName('session-button-focused', false);
})
,
...props,
});
}
export default ({ id = 0 }) => {
// lock, logout, sleep
const lockButton = SessionButton(getString('Lock'), 'lock', () => { closeWindowOnAllMonitors('session'); execAsync(['loginctl', 'lock-session']).catch(print) }, {}, 1);
const logoutButton = SessionButton(getString('Logout'), 'logout', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'pkill Hyprland || pkill sway || pkill niri || loginctl terminate-user $USER']).catch(print) }, {}, 2);
const sleepButton = SessionButton(getString('Sleep'), 'sleep', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl suspend || loginctl suspend']).catch(print) }, {}, 3);
// hibernate, shutdown, reboot
const hibernateButton = SessionButton(getString('Hibernate'), 'downloading', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl hibernate || loginctl hibernate']).catch(print) }, {}, 4);
const shutdownButton = SessionButton(getString('Shutdown'), 'power_settings_new', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl poweroff || loginctl poweroff']).catch(print) }, {}, 5);
const rebootButton = SessionButton(getString('Reboot'), 'restart_alt', () => { closeWindowOnAllMonitors('session'); execAsync(['bash', '-c', 'systemctl reboot || loginctl reboot']).catch(print) }, {}, 6);
const cancelButton = SessionButton(getString('Cancel'), 'close', () => closeWindowOnAllMonitors('session'), { className: 'session-button-cancel' }, 7);
const sessionDescription = Widget.Box({
vertical: true,
css: 'margin-bottom: 0.682rem;',
children: [
Widget.Label({
className: 'txt-title txt',
label: getString('Session'),
}),
Widget.Label({
justify: Gtk.Justification.CENTER,
className: 'txt-small txt',
label: getString('Use arrow keys to navigate.\nEnter to select, Esc to cancel.')
}),
]
});
const SessionButtonRow = (children) => Widget.Box({
hpack: 'center',
className: 'spacing-h-15',
children: children,
});
const sessionButtonRows = [
SessionButtonRow([lockButton, logoutButton, sleepButton]),
SessionButtonRow([hibernateButton, shutdownButton, rebootButton]),
SessionButtonRow([cancelButton]),
]
return Widget.Box({
className: 'session-bg',
css: `
min-width: ${monitors[id].width}px;
min-height: ${monitors[id].height}px;
`, // idk why but height = screen height doesn't fill
vertical: true,
children: [
Widget.EventBox({
onPrimaryClick: () => closeWindowOnAllMonitors('session'),
onSecondaryClick: () => closeWindowOnAllMonitors('session'),
onMiddleClick: () => closeWindowOnAllMonitors('session'),
}),
Widget.Box({
hpack: 'center',
vexpand: true,
vertical: true,
children: [
Widget.Box({
vpack: 'center',
vertical: true,
className: 'spacing-v-15',
children: [
sessionDescription,
...sessionButtonRows,
]
})
]
})
],
setup: (self) => self
.hook(App, (_b, name, visible) => {
if (visible) lockButton.grab_focus(); // Lock is the default option
})
,
});
}

View file

@ -0,0 +1,365 @@
const { Gdk, Gio, GLib, Gtk } = imports.gi;
import GtkSource from "gi://GtkSource?version=3.0";
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 { Box, Button, Label, Icon, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import md2pango from '../../.miscutils/md2pango.js';
import { darkMode } from "../../.miscutils/system.js";
const LATEX_DIR = `${GLib.get_user_cache_dir()}/ags/media/latex`;
const CUSTOM_SOURCEVIEW_SCHEME_PATH = `${App.configDir}/assets/themes/sourceviewtheme${darkMode.value ? '' : '-light'}.xml`;
const CUSTOM_SCHEME_ID = `custom${darkMode.value ? '' : '-light'}`;
const USERNAME = GLib.get_user_name();
/////////////////////// Custom source view colorscheme /////////////////////////
function loadCustomColorScheme(filePath) {
// Read the XML file content
const file = Gio.File.new_for_path(filePath);
const [success, contents] = file.load_contents(null);
if (!success) {
logError('Failed to load the XML file.');
return;
}
// Parse the XML content and set the Style Scheme
const schemeManager = GtkSource.StyleSchemeManager.get_default();
schemeManager.append_search_path(file.get_parent().get_path());
}
loadCustomColorScheme(CUSTOM_SOURCEVIEW_SCHEME_PATH);
//////////////////////////////////////////////////////////////////////////////
function substituteLang(str) {
const subs = [
{ from: 'javascript', to: 'js' },
{ from: 'bash', to: 'sh' },
];
for (const { from, to } of subs) {
if (from === str) return to;
}
return str;
}
const HighlightedCode = (content, lang) => {
const buffer = new GtkSource.Buffer();
const sourceView = new GtkSource.View({
buffer: buffer,
wrap_mode: Gtk.WrapMode.NONE
});
const langManager = GtkSource.LanguageManager.get_default();
let displayLang = langManager.get_language(substituteLang(lang)); // Set your preferred language
if (displayLang) {
buffer.set_language(displayLang);
}
const schemeManager = GtkSource.StyleSchemeManager.get_default();
buffer.set_style_scheme(schemeManager.get_scheme(CUSTOM_SCHEME_ID));
buffer.set_text(content, -1);
return sourceView;
}
const TextBlock = (content = '') => Label({
hpack: 'fill',
className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
useMarkup: true,
xalign: 0,
wrap: true,
selectable: true,
label: content,
});
Utils.execAsync(['bash', '-c', `rm -rf ${LATEX_DIR}`])
.then(() => Utils.execAsync(['bash', '-c', `mkdir -p ${LATEX_DIR}`]))
.catch(print);
const Latex = (content = '') => {
const latexViewArea = Box({
// vscroll: 'never',
// hscroll: 'automatic',
// homogeneous: true,
attribute: {
render: async (self, text) => {
if (text.length == 0) return;
const styleContext = self.get_style_context();
const fontSize = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
const timeSinceEpoch = Date.now();
const fileName = `${timeSinceEpoch}.tex`;
const outFileName = `${timeSinceEpoch}-symbolic.svg`;
const outIconName = `${timeSinceEpoch}-symbolic`;
const scriptFileName = `${timeSinceEpoch}-render.sh`;
const filePath = `${LATEX_DIR}/${fileName}`;
const outFilePath = `${LATEX_DIR}/${outFileName}`;
const scriptFilePath = `${LATEX_DIR}/${scriptFileName}`;
Utils.writeFile(text, filePath).catch(print);
// Since MicroTex doesn't support file path input properly, we gotta cat it
// And escaping such a command is a fucking pain so I decided to just generate a script
// Note: MicroTex doesn't support `&=`
// You can add this line in the middle for debugging: echo "$text" > ${filePath}.tmp
const renderScript = `#!/usr/bin/env bash
text=$(cat ${filePath} | sed 's/$/ \\\\\\\\/g' | sed 's/&=/=/g')
cd /opt/MicroTeX
./LaTeX -headless -input="$text" -output=${outFilePath} -textsize=${fontSize * 1.1} -padding=0 -maxwidth=${latexViewArea.get_allocated_width() * 0.85} > /dev/null 2>&1
sed -i 's/fill="rgb(0%, 0%, 0%)"/style="fill:#000000"/g' ${outFilePath}
sed -i 's/stroke="rgb(0%, 0%, 0%)"/stroke="${darkMode.value ? '#ffffff' : '#000000'}"/g' ${outFilePath}
`;
Utils.writeFile(renderScript, scriptFilePath).catch(print);
Utils.exec(`chmod a+x ${scriptFilePath}`)
Utils.timeout(100, () => {
Utils.exec(`bash ${scriptFilePath}`);
Gtk.IconTheme.get_default().append_search_path(LATEX_DIR);
self.child?.destroy();
self.child = Gtk.Image.new_from_icon_name(outIconName, 0);
})
}
},
setup: (self) => self.attribute.render(self, content).catch(print),
});
const wholeThing = Box({
className: 'sidebar-chat-latex',
homogeneous: true,
attribute: {
'updateText': (text) => {
latexViewArea.attribute.render(latexViewArea, text).catch(print);
}
},
children: [Scrollable({
vscroll: 'never',
hscroll: 'automatic',
child: latexViewArea
})]
})
return wholeThing;
}
const CodeBlock = (content = '', lang = 'txt') => {
if (lang == 'tex' || lang == 'latex') {
return Latex(content);
}
const topBar = Box({
className: 'sidebar-chat-codeblock-topbar',
children: [
Label({
label: lang,
className: 'sidebar-chat-codeblock-topbar-txt',
}),
Box({
hexpand: true,
}),
Button({
className: 'sidebar-chat-codeblock-topbar-btn',
child: Box({
className: 'spacing-h-5',
children: [
MaterialIcon('content_copy', 'small'),
Label({
label: 'Copy',
})
]
}),
onClicked: (self) => {
const buffer = sourceView.get_buffer();
const copyContent = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), false); // TODO: fix this
execAsync([`wl-copy`, `${copyContent}`]).catch(print);
},
}),
]
})
// Source view
const sourceView = HighlightedCode(content, lang);
const codeBlock = Box({
attribute: {
'updateText': (text) => {
sourceView.get_buffer().set_text(text, -1);
}
},
className: 'sidebar-chat-codeblock',
vertical: true,
children: [
topBar,
Box({
className: 'sidebar-chat-codeblock-code',
homogeneous: true,
children: [Scrollable({
vscroll: 'never',
hscroll: 'automatic',
child: sourceView,
})],
})
]
})
// const schemeIds = styleManager.get_scheme_ids();
// print("Available Style Schemes:");
// for (let i = 0; i < schemeIds.length; i++) {
// print(schemeIds[i]);
// }
return codeBlock;
}
const Divider = () => Box({
className: 'sidebar-chat-divider',
})
const MessageContent = (content) => {
const contentBox = Box({
vertical: true,
attribute: {
'fullUpdate': (self, content, useCursor = false) => {
// Clear and add first text widget
const children = contentBox.get_children();
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.destroy();
}
contentBox.add(TextBlock())
// Loop lines. Put normal text in markdown parser
// and put code into code highlighter (TODO)
let lines = content.split('\n');
let lastProcessed = 0;
let inCode = false;
for (const [index, line] of lines.entries()) {
// Code blocks
const codeBlockRegex = /^\s*```([a-zA-Z0-9]+)?\n?/;
if (codeBlockRegex.test(line)) {
const kids = self.get_children();
const lastLabel = kids[kids.length - 1];
const blockContent = lines.slice(lastProcessed, index).join('\n');
if (!inCode) {
lastLabel.label = md2pango(blockContent);
contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1]));
}
else {
lastLabel.attribute.updateText(blockContent);
contentBox.add(TextBlock());
}
lastProcessed = index + 1;
inCode = !inCode;
}
// Breaks
const dividerRegex = /^\s*---/;
if (!inCode && dividerRegex.test(line)) {
const kids = self.get_children();
const lastLabel = kids[kids.length - 1];
const blockContent = lines.slice(lastProcessed, index).join('\n');
lastLabel.label = md2pango(blockContent);
contentBox.add(Divider());
contentBox.add(TextBlock());
lastProcessed = index + 1;
}
}
if (lastProcessed < lines.length) {
const kids = self.get_children();
const lastLabel = kids[kids.length - 1];
let blockContent = lines.slice(lastProcessed, lines.length).join('\n');
if (!inCode)
lastLabel.label = `${md2pango(blockContent)}${useCursor ? userOptions.ai.writingCursor : ''}`;
else
lastLabel.attribute.updateText(blockContent);
}
// Debug: plain text
// contentBox.add(Label({
// hpack: 'fill',
// className: 'txt sidebar-chat-txtblock sidebar-chat-txt',
// useMarkup: false,
// xalign: 0,
// wrap: true,
// selectable: true,
// label: '------------------------------\n' + md2pango(content),
// }))
contentBox.show_all();
}
}
});
contentBox.attribute.fullUpdate(contentBox, content, false);
return contentBox;
}
export const ChatMessage = (message, modelName = 'Model') => {
const TextSkeleton = (extraClassName = '') => Box({
className: `sidebar-chat-message-skeletonline ${extraClassName}`,
})
const messageContentBox = MessageContent(message.content);
const messageLoadingSkeleton = Box({
vertical: true,
className: 'spacing-v-5',
children: Array.from({ length: 3 }, (_, id) => TextSkeleton(`sidebar-chat-message-skeletonline-offset${id}`)),
})
const messageArea = Stack({
homogeneous: message.role !== 'user',
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
children: {
'thinking': messageLoadingSkeleton,
'message': messageContentBox,
},
shown: message.thinking ? 'thinking' : 'message',
});
const thisMessage = Box({
className: 'sidebar-chat-message',
homogeneous: true,
children: [
Box({
vertical: true,
children: [
Label({
hpack: 'start',
xalign: 0,
className: `txt txt-bold sidebar-chat-name sidebar-chat-name-${message.role == 'user' ? 'user' : 'bot'}`,
wrap: true,
useMarkup: true,
label: (message.role == 'user' ? USERNAME : modelName),
}),
Box({
homogeneous: true,
className: 'sidebar-chat-messagearea',
children: [messageArea]
})
],
setup: (self) => self
.hook(message, (self, isThinking) => {
messageArea.shown = message.thinking ? 'thinking' : 'message';
}, 'notify::thinking')
.hook(message, (self) => { // Message update
messageContentBox.attribute.fullUpdate(messageContentBox, message.content, message.role != 'user');
}, 'notify::content')
.hook(message, (label, isDone) => { // Remove the cursor
messageContentBox.attribute.fullUpdate(messageContentBox, message.content, false);
}, 'notify::done')
,
})
]
});
return thisMessage;
}
export const SystemMessage = (content, commandName, scrolledWindow) => {
const messageContentBox = MessageContent(content);
const thisMessage = Box({
className: 'sidebar-chat-message',
children: [
Box({
vertical: true,
children: [
Label({
xalign: 0,
hpack: 'start',
className: 'txt txt-bold sidebar-chat-name sidebar-chat-name-system',
wrap: true,
label: `System • ${commandName}`,
}),
messageContentBox,
],
})
],
});
return thisMessage;
}

View file

@ -0,0 +1,518 @@
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 { Box, Button, EventBox, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { fileExists } from '../../.miscutils/files.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
import BooruService from '../../../services/booru.js';
import { chatEntry } from '../apiwidgets.js';
import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
import { SystemMessage } from './ai_chatmessage.js';
const IMAGE_REVEAL_DELAY = 13; // Some wait for inits n other weird stuff
const USER_CACHE_DIR = GLib.get_user_cache_dir();
// Create cache folder and clear pics from previous session
Utils.exec(`bash -c 'mkdir -p ${USER_CACHE_DIR}/ags/media/waifus'`);
Utils.exec(`bash -c 'rm ${USER_CACHE_DIR}/ags/media/waifus/*'`);
const TagButton = (command) => Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => { chatEntry.buffer.text += `${command} ` },
setup: setupCursorHover,
label: command,
});
const CommandButton = (command, displayName = command) => Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => sendMessage(command),
setup: setupCursorHover,
label: displayName,
});
export const booruTabIcon = Box({
hpack: 'center',
homogeneous: true,
children: [
MaterialIcon('gallery_thumbnail', 'norm'),
]
});
const BooruInfo = () => {
const booruLogo = Label({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
label: 'gallery_thumbnail',
})
return Box({
vertical: true,
vexpand: true,
className: 'spacing-v-15',
children: [
booruLogo,
Label({
className: 'txt txt-title-small sidebar-chat-welcome-txt',
wrap: true,
justify: Gtk.Justification.CENTER,
label: 'Anime booru',
}),
Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
Label({
className: 'txt-smallie txt-subtext',
wrap: true,
justify: Gtk.Justification.CENTER,
label: getString('Powered by yande.re and konachan'),
}),
Button({
className: 'txt-subtext txt-norm icon-material',
label: 'info',
tooltipText: getString('An image booru. May contain NSFW content.\nWatch your back.\n\nDisclaimer: Not affiliated with the provider\nnor responsible for any of its content.'),
setup: setupCursorHoverInfo,
}),
]
}),
]
});
}
export const BooruSettings = () => MarginRevealer({
transition: 'slide_down',
revealChild: true,
child: Box({
vertical: true,
className: 'sidebar-chat-settings',
children: [
Box({
vertical: true,
hpack: 'fill',
className: 'sidebar-chat-settings-toggles',
children: [
ConfigToggle({
icon: 'menstrual_health',
name: getString('Lewds'),
desc: getString("Shows naughty stuff when enabled.\nYa like those? Add this to user_options.js:\n\t'sidebar': {\n\t'image': {\n\t\t'allowNsfw': true,\n\t}\n}"),
initValue: BooruService.nsfw,
onChange: (self, newValue) => {
BooruService.nsfw = newValue;
},
extraSetup: (self) => self.hook(BooruService, (self) => {
self.attribute.enabled.value = BooruService.nsfw;
}, 'notify::nsfw')
}),
ConfigToggle({
icon: 'sell',
name: getString('Save in folder by tags'),
desc: getString('Saves images in folders by their tags'),
initValue: userOptions.sidebar.image.saveInFolderByTags,
onChange: (self, newValue) => {
userOptions.sidebar.image.saveInFolderByTags = newValue;
},
}),
]
})
]
})
});
const booruWelcome = Box({
vexpand: true,
homogeneous: true,
child: Box({
className: 'spacing-v-15',
vpack: 'center',
vertical: true,
children: [
BooruInfo(),
BooruSettings(),
]
})
});
const BooruPage = (taglist, serviceName = 'Booru') => {
const PageState = (icon, name) => Box({
className: 'spacing-h-5 txt',
children: [
Label({
className: 'sidebar-waifu-txt txt-smallie',
xalign: 0,
label: name,
}),
MaterialIcon(icon, 'norm'),
]
})
const ImageAction = ({ name, icon, action }) => Button({
className: 'sidebar-waifu-image-action txt-norm icon-material',
tooltipText: name,
label: icon,
onClicked: action,
setup: setupCursorHover,
})
const PreviewImage = (data, delay = 0) => {
const imageArea = Widget.DrawingArea({
className: 'sidebar-booru-image-drawingarea',
});
const imageBox = Box({
className: 'sidebar-booru-image',
// css: `background-image: url('${data.preview_url}');`,
attribute: {
'update': (self, data, force = false) => {
const imagePath = `${USER_CACHE_DIR}/ags/media/waifus/${data.md5}.${data.file_ext}`;
const widgetStyleContext = imageArea.get_style_context();
const widgetWidth = widgetStyleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const widgetHeight = widgetWidth / data.aspect_ratio;
imageArea.set_size_request(widgetWidth, widgetHeight);
const showImage = () => {
// const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imagePath, widgetWidth, widgetHeight);
const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(imagePath, widgetWidth, widgetHeight, false);
imageArea.connect("draw", (widget, cr) => {
const borderRadius = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
// Draw a rounded rectangle
cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
cr.arc(widgetWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
cr.arc(widgetWidth - borderRadius, widgetHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
cr.arc(borderRadius, widgetHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
cr.closePath();
cr.clip();
// Paint image as bg
Gdk.cairo_set_source_pixbuf(cr, pixbuf, (widgetWidth - widgetWidth) / 2, (widgetHeight - widgetHeight) / 2);
cr.paint();
});
self.queue_draw();
imageRevealer.revealChild = true;
}
// Show
// const downloadCommand = `wget -O '${imagePath}' '${data.preview_url}'`;
const downloadCommand = `curl -L -o '${imagePath}' '${data.preview_url}'`;
if (!force && fileExists(imagePath)) showImage();
else Utils.timeout(delay, () => Utils.execAsync(['bash', '-c', downloadCommand])
.then(showImage)
.catch(print)
);
},
},
child: imageArea,
setup: (self) => {
Utils.timeout(1000, () => self.attribute.update(self, data));
}
});
const imageActions = Revealer({
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
vpack: 'start',
className: 'sidebar-booru-image-actions spacing-h-3',
children: [
Box({ hexpand: true }),
ImageAction({
name: getString('Go to file url'),
icon: 'file_open',
action: () => execAsync(['xdg-open', `${data.file_url}`]).catch(print),
}),
ImageAction({
name: getString('Go to source'),
icon: 'open_in_new',
action: () => execAsync(['xdg-open', `${data.source}`]).catch(print),
}),
ImageAction({
name: getString('Save image'),
icon: 'save',
action: (self) => {
const currentTags = BooruService.queries.at(-1).realTagList.filter(tag => !tag.includes('rating:'));
const tagDirectory = currentTags.join('+');
let fileExtension = data.file_ext || 'jpg';
const saveCommand = `mkdir -p $(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}${userOptions.sidebar.image.saveInFolderByTags ? tagDirectory : ''} && curl -L -o $(xdg-user-dir PICTURES)/homework/${data.is_nsfw ? '🌶️/' : ''}${userOptions.sidebar.image.saveInFolderByTags ? (tagDirectory + '/') : ''}${data.md5}.${fileExtension} '${data.file_url}'`;
execAsync(['bash', '-c', saveCommand])
.then(() => self.label = 'done')
.catch(print);
},
}),
]
})
});
const imageOverlay = Overlay({
passThrough: true,
child: imageBox,
overlays: [imageActions]
});
const imageRevealer = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: EventBox({
onHover: () => { imageActions.revealChild = true },
onHoverLost: () => { imageActions.revealChild = false },
child: imageOverlay,
})
})
return imageRevealer;
}
const downloadState = Stack({
homogeneous: false,
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'api': PageState('api', getString('Calling API')),
'download': PageState('downloading', getString('Downloading image')),
'done': PageState('done', getString('Finished!')),
'error': PageState('error', getString('Error')),
},
});
const downloadIndicator = MarginRevealer({
vpack: 'center',
transition: 'slide_left',
revealChild: true,
child: downloadState,
});
const pageHeading = Box({
vertical: true,
children: [
Box({
children: [
Label({
hpack: 'start',
className: `sidebar-booru-provider`,
label: `${serviceName}`,
truncate: 'end',
maxWidthChars: 20,
}),
Box({ hexpand: true }),
downloadIndicator,
]
}),
Box({
children: [
Scrollable({
hexpand: true,
vscroll: 'never',
hscroll: 'automatic',
child: Box({
hpack: 'fill',
className: 'spacing-h-5',
children: [
...taglist.map((tag) => TagButton(tag)),
Box({ hexpand: true }),
]
})
}),
]
})
]
});
const pageImages = Box({
hpack: 'start',
homogeneous: true,
className: 'sidebar-booru-imagegrid',
})
const pageImageRevealer = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: pageImages,
});
const thisPage = Box({
homogeneous: true,
className: 'sidebar-chat-message',
attribute: {
'imagePath': '',
'isNsfw': false,
'update': (data, force = false) => { // TODO: Use columns. Sort min to max h/w ratio then greedily put em in...
// Sort by .aspect_ratio
data = data.sort(
(a, b) => a.aspect_ratio - b.aspect_ratio
);
if (data.length == 0) {
downloadState.shown = 'error';
return;
}
const imageColumns = userOptions.sidebar.image.columns;
const imageRows = data.length / imageColumns;
// Init cols
pageImages.children = Array.from(
{ length: imageColumns },
(_, i) => Box({
attribute: { height: 0 },
vertical: true,
})
);
// Greedy add O(n^2) 😭
for (let i = 0; i < data.length; i++) {
// Find column with lowest length
let minHeight = Infinity;
let minIndex = -1;
for (let j = 0; j < imageColumns; j++) {
const height = pageImages.children[j].attribute.height;
if (height < minHeight) {
minHeight = height;
minIndex = j;
}
}
// Add image to it
pageImages.children[minIndex].pack_start(PreviewImage(data[i], minIndex), false, false, 0)
pageImages.children[minIndex].attribute.height += 1 / data[i].aspect_ratio; // we want height/width
}
pageImages.show_all();
// Reveal stuff
Utils.timeout(IMAGE_REVEAL_DELAY,
() => pageImageRevealer.revealChild = true
);
downloadIndicator.attribute.hide();
},
},
children: [Box({
vertical: true,
children: [
pageHeading,
Box({
vertical: true,
children: [pageImageRevealer],
})
]
})],
});
return thisPage;
}
const booruContent = Box({
className: 'spacing-v-15',
vertical: true,
attribute: {
'map': new Map(),
},
setup: (self) => self
.hook(BooruService, (box, id) => {
if (id === undefined) return;
const newPage = BooruPage(BooruService.queries[id].taglist, BooruService.queries[id].providerName);
box.add(newPage);
box.show_all();
box.attribute.map.set(id, newPage);
}, 'newResponse')
.hook(BooruService, (box, id) => {
if (id === undefined) return;
if (!BooruService.responses[id]) return;
box.attribute.map.get(id)?.attribute.update(BooruService.responses[id]);
}, 'updateResponse')
,
});
export const booruView = Scrollable({
className: 'sidebar-chat-viewport',
vexpand: true,
child: Box({
vertical: true,
children: [
booruWelcome,
booruContent,
]
}),
setup: (scrolledWindow) => {
// Show scrollbar
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = scrolledWindow.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
// Avoid click-to-scroll-widget-to-view behavior
Utils.timeout(1, () => {
const viewport = scrolledWindow.child;
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
})
// Scroll to bottom with new content if chat entry not focused
const adjustment = scrolledWindow.get_vadjustment();
adjustment.connect("changed", () => {
if (!chatEntry.hasFocus) return;
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
})
}
});
const booruTags = Revealer({
revealChild: false,
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
className: 'spacing-h-5',
children: [
Scrollable({
vscroll: 'never',
hscroll: 'automatic',
hexpand: true,
child: Box({
className: 'spacing-h-5',
children: [
TagButton('( * )'),
TagButton('hololive'),
]
})
}),
Box({ className: 'separator-line' }),
]
})
});
export const booruCommands = Box({
className: 'spacing-h-5',
setup: (self) => {
self.pack_end(CommandButton('/clear'), false, false, 0);
self.pack_end(CommandButton('/next'), false, false, 0);
self.pack_start(Button({
className: 'sidebar-chat-chip-toggle',
setup: setupCursorHover,
label: getString('Tags →'),
onClicked: () => {
booruTags.revealChild = !booruTags.revealChild;
}
}), false, false, 0);
self.pack_start(booruTags, true, true, 0);
}
});
const clearChat = () => { // destroy!!
booruContent.attribute.map.forEach((value, key, map) => {
value.destroy();
value = null;
});
}
export const sendMessage = (text) => {
// Commands
if (text.startsWith('+')) { // Next page
const lastQuery = BooruService.queries.at(-1);
BooruService.fetch(`${lastQuery.realTagList.join(' ')} ${lastQuery.page + 1}`)
}
else if (text.startsWith('/')) {
if (text.startsWith('/clear')) clearChat();
else if (text.startsWith('/safe')) {
BooruService.nsfw = false;
const message = SystemMessage(`Switched to safe mode`, '/safe', booruView)
booruContent.add(message);
booruContent.show_all();
booruContent.attribute.map.set(Date.now(), message);
}
else if (text.startsWith('/lewd')) {
BooruService.nsfw = true;
const message = SystemMessage(`Tiddies enabled`, '/lewd', booruView)
booruContent.add(message);
booruContent.show_all();
booruContent.attribute.map.set(Date.now(), message);
}
else if (text.startsWith('/mode')) {
const mode = text.slice(text.indexOf(' ') + 1);
BooruService.mode = mode;
const message = SystemMessage(`Changed provider to ${BooruService.providerName}`, '/mode', booruView)
booruContent.add(message);
booruContent.show_all();
booruContent.attribute.map.set(Date.now(), message);
}
else if (text.startsWith('/next')) {
sendMessage('+')
}
}
else BooruService.fetch(text);
}

View file

@ -0,0 +1,355 @@
const { Gtk } = 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 { Box, Button, Icon, Label, Revealer, Scrollable } = Widget;
import GPTService from '../../../services/gpt.js';
import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../.commonwidgets/configwidgets.js';
import { markdownTest } from '../../.miscutils/md2pango.js';
import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { chatEntry } from '../apiwidgets.js';
export const chatGPTTabIcon = Icon({
hpack: 'center',
icon: `openai-symbolic`,
});
const ProviderSwitcher = () => {
const ProviderChoice = (id, provider) => {
const providerSelected = MaterialIcon('check', 'norm', {
setup: (self) => self.hook(GPTService, (self) => {
self.toggleClassName('invisible', GPTService.providerID !== id);
}, 'providerChanged')
});
return Button({
tooltipText: provider.description,
onClicked: () => {
GPTService.providerID = id;
providerList.revealChild = false;
indicatorChevron.label = 'expand_more';
},
child: Box({
className: 'spacing-h-10 txt',
children: [
Icon({
icon: provider['logo_name'],
className: 'txt-large'
}),
Label({
hexpand: true,
xalign: 0,
className: 'txt-small',
label: provider.name,
}),
providerSelected
],
}),
setup: setupCursorHover,
});
}
const indicatorChevron = MaterialIcon('expand_more', 'norm');
const indicatorButton = Button({
tooltipText: getString('Select ChatGPT-compatible API provider'),
child: Box({
className: 'spacing-h-10 txt',
children: [
MaterialIcon('cloud', 'norm'),
Label({
hexpand: true,
xalign: 0,
className: 'txt-small',
label: GPTService.providerID,
setup: (self) => self.hook(GPTService, (self) => {
self.label = `${GPTService.providers[GPTService.providerID]['name']}`;
}, 'providerChanged')
}),
indicatorChevron,
]
}),
onClicked: () => {
providerList.revealChild = !providerList.revealChild;
indicatorChevron.label = (providerList.revealChild ? 'expand_less' : 'expand_more');
},
setup: setupCursorHover,
});
const providerList = Revealer({
revealChild: false,
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
vertical: true, className: 'spacing-v-5 sidebar-chat-providerswitcher-list',
children: [
Box({ className: 'separator-line margin-top-5 margin-bottom-5' }),
Box({
className: 'spacing-v-5',
vertical: true,
setup: (self) => self.hook(GPTService, (self) => {
self.children = Object.entries(GPTService.providers)
.map(([id, provider]) => ProviderChoice(id, provider));
}, 'initialized'),
})
]
})
})
return Box({
hpack: 'center',
vertical: true,
className: 'sidebar-chat-providerswitcher',
children: [
indicatorButton,
providerList,
]
})
}
const GPTInfo = () => {
const openAiLogo = Icon({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
icon: `openai-symbolic`,
});
return Box({
vertical: true,
className: 'spacing-v-15',
children: [
openAiLogo,
Label({
className: 'txt txt-title-small sidebar-chat-welcome-txt',
wrap: true,
justify: Gtk.Justification.CENTER,
label: `Assistant (GPTs)`,
}),
Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
Label({
className: 'txt-smallie txt-subtext',
wrap: true,
justify: Gtk.Justification.CENTER,
label: getString('Provider shown above'),
}),
Button({
className: 'txt-subtext txt-norm icon-material',
label: 'info',
tooltipText: getString('Uses gpt-3.5-turbo.\nNot affiliated, endorsed, or sponsored by OpenAI.\n\nPrivacy: OpenAI claims they do not use your data\nwhen you use their API. Idk about others.'),
setup: setupCursorHoverInfo,
}),
]
}),
]
});
}
const GPTSettings = () => MarginRevealer({
transition: 'slide_down',
revealChild: true,
extraSetup: (self) => self
.hook(GPTService, (self) => Utils.timeout(200, () => {
self.attribute.hide();
}), 'newMsg')
.hook(GPTService, (self) => Utils.timeout(200, () => {
self.attribute.show();
}), 'clear')
,
child: Box({
vertical: true,
className: 'sidebar-chat-settings',
children: [
ConfigSegmentedSelection({
hpack: 'center',
icon: 'casino',
name: 'Randomness',
desc: getString('The model\'s temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1'),
options: [
{ value: 0.00, name: getString('Precise'), },
{ value: 0.50, name: getString('Balanced'), },
{ value: 1.00, name: getString('Creative'), },
],
initIndex: 2,
onChange: (value, name) => {
GPTService.temperature = value;
},
}),
ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
Box({
vertical: true,
hpack: 'fill',
className: 'sidebar-chat-settings-toggles',
children: [
ConfigToggle({
icon: 'model_training',
name: getString('Enhancements'),
desc: getString('Tells the model:\n- It\'s a Linux sidebar assistant\n- Be brief and use bullet points'),
initValue: GPTService.assistantPrompt,
onChange: (self, newValue) => {
GPTService.assistantPrompt = newValue;
},
}),
]
})
]
})
});
export const OpenaiApiKeyInstructions = () => Box({
homogeneous: true,
children: [Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
setup: (self) => self
.hook(GPTService, (self, hasKey) => {
self.revealChild = (GPTService.key.length == 0);
}, 'hasKey')
,
child: Button({
child: Label({
useMarkup: true,
wrap: true,
className: 'txt sidebar-chat-welcome-txt',
justify: Gtk.Justification.CENTER,
label: getString('An API key is required\nYou can grab one <u>here</u>, then enter it below')
}),
setup: setupCursorHover,
onClicked: () => {
Utils.execAsync(['bash', '-c', `xdg-open ${GPTService.getKeyUrl}`]);
}
})
})]
});
const GPTWelcome = () => Box({
vexpand: true,
homogeneous: true,
child: Box({
className: 'spacing-v-15',
vpack: 'center',
vertical: true,
children: [
GPTInfo(),
OpenaiApiKeyInstructions(),
GPTSettings(),
]
})
});
export const chatContent = Box({
className: 'spacing-v-5',
vertical: true,
setup: (self) => self
.hook(GPTService, (box, id) => {
const message = GPTService.messages[id];
if (!message) return;
box.add(ChatMessage(message, `Model (${GPTService.providers[GPTService.providerID]['name']})`))
}, 'newMsg')
,
});
const clearChat = () => {
GPTService.clear();
const children = chatContent.get_children();
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.destroy();
}
}
const CommandButton = (command) => Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => sendMessage(command),
setup: setupCursorHover,
label: command,
});
export const chatGPTCommands = Box({
className: 'spacing-h-5',
children: [
Box({ hexpand: true }),
CommandButton('/key'),
CommandButton('/model'),
CommandButton('/clear'),
]
});
export const sendMessage = (text) => {
// Check if text or API key is empty
if (text.length == 0) return;
if (GPTService.key.length == 0) {
GPTService.key = text;
chatContent.add(SystemMessage(`Key saved to\n\`${GPTService.keyPath}\``, 'API Key', chatGPTView));
text = '';
return;
}
// Commands
if (text.startsWith('/')) {
if (text.startsWith('/clear')) clearChat();
else if (text.startsWith('/model')) chatContent.add(SystemMessage(`${getString("Currently using")} \`${GPTService.modelName}\``, '/model', chatGPTView))
else if (text.startsWith('/prompt')) {
const firstSpaceIndex = text.indexOf(' ');
const prompt = text.slice(firstSpaceIndex + 1);
if (firstSpaceIndex == -1 || prompt.length < 1) {
chatContent.add(SystemMessage(`Usage: \`/prompt MESSAGE\``, '/prompt', chatGPTView))
}
else {
GPTService.addMessage('user', prompt)
}
}
else if (text.startsWith('/key')) {
const parts = text.split(' ');
if (parts.length == 1) chatContent.add(SystemMessage(
`${getString("Key stored in:")}\n\`${GPTService.keyPath}\`\n${getString("To update this key, type")} \`/key YOUR_API_KEY\``,
'/key',
chatGPTView));
else {
GPTService.key = parts[1];
chatContent.add(SystemMessage(`${getString("Updated API Key at")}\n\`${GPTService.keyPath}\``, '/key', chatGPTView));
}
}
else if (text.startsWith('/test'))
chatContent.add(SystemMessage(markdownTest, `Markdown test`, chatGPTView));
else
chatContent.add(SystemMessage(getString("Invalid command."), 'Error', chatGPTView))
}
else {
GPTService.send(text);
}
}
export const chatGPTView = Box({
vertical: true,
children: [
ProviderSwitcher(),
Scrollable({
className: 'sidebar-chat-viewport',
vexpand: true,
child: Box({
vertical: true,
children: [
GPTWelcome(),
chatContent,
]
}),
setup: (scrolledWindow) => {
// Show scrollbar
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = scrolledWindow.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
// Avoid click-to-scroll-widget-to-view behavior
Utils.timeout(1, () => {
const viewport = scrolledWindow.child;
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
})
// Always scroll to bottom with new content
const adjustment = scrolledWindow.get_vadjustment();
adjustment.connect("changed", () => {
if (!chatEntry.hasFocus) return;
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
})
}
})
]
});

View file

@ -0,0 +1,288 @@
const { Gtk } = 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 { Box, Button, Icon, Label, Revealer, Scrollable } = Widget;
import GeminiService from '../../../services/gemini.js';
import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
import { SystemMessage, ChatMessage } from "./ai_chatmessage.js";
import { ConfigToggle, ConfigSegmentedSelection, ConfigGap } from '../../.commonwidgets/configwidgets.js';
import { markdownTest } from '../../.miscutils/md2pango.js';
import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
import { chatEntry } from '../apiwidgets.js';
const MODEL_NAME = `Gemini`;
export const geminiTabIcon = Icon({
hpack: 'center',
icon: `google-gemini-symbolic`,
})
const GeminiInfo = () => {
const geminiLogo = Icon({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
icon: `google-gemini-symbolic`,
});
return Box({
vertical: true,
className: 'spacing-v-15',
children: [
geminiLogo,
Label({
className: 'txt txt-title-small sidebar-chat-welcome-txt',
wrap: true,
justify: Gtk.Justification.CENTER,
label: `Assistant (Gemini)`,
}),
Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
Label({
className: 'txt-smallie txt-subtext',
wrap: true,
justify: Gtk.Justification.CENTER,
label: getString('Powered by Google'),
}),
Button({
className: 'txt-subtext txt-norm icon-material',
label: 'info',
tooltipText: getString("Uses gemini-pro.\nNot affiliated, endorsed, or sponsored by Google.\n\nPrivacy: Chat messages aren't linked to your account,\n but will be read by human reviewers to improve the model."),
setup: setupCursorHoverInfo,
}),
]
}),
]
});
}
export const GeminiSettings = () => MarginRevealer({
transition: 'slide_down',
revealChild: true,
extraSetup: (self) => self
.hook(GeminiService, (self) => Utils.timeout(200, () => {
self.attribute.hide();
}), 'newMsg')
.hook(GeminiService, (self) => Utils.timeout(200, () => {
self.attribute.show();
}), 'clear')
,
child: Box({
vertical: true,
className: 'sidebar-chat-settings',
children: [
ConfigSegmentedSelection({
hpack: 'center',
icon: 'casino',
name: 'Randomness',
desc: getString("Gemini's temperature value.\n Precise = 0\n Balanced = 0.5\n Creative = 1"),
options: [
{ value: 0.00, name: getString('Precise'), },
{ value: 0.50, name: getString('Balanced'), },
{ value: 1.00, name: getString('Creative'), },
],
initIndex: 2,
onChange: (value, name) => {
GeminiService.temperature = value;
},
}),
ConfigGap({ vertical: true, size: 10 }), // Note: size can only be 5, 10, or 15
Box({
vertical: true,
hpack: 'fill',
className: 'sidebar-chat-settings-toggles',
children: [
ConfigToggle({
icon: 'model_training',
name: getString('Enhancements'),
desc: getString("Tells Gemini:\n- It's a Linux sidebar assistant\n- Be brief and use bullet points"),
initValue: GeminiService.assistantPrompt,
onChange: (self, newValue) => {
GeminiService.assistantPrompt = newValue;
},
}),
ConfigToggle({
icon: 'shield',
name: getString('Safety'),
desc: getString("When turned off, tells the API (not the model) \nto not block harmful/explicit content"),
initValue: GeminiService.safe,
onChange: (self, newValue) => {
GeminiService.safe = newValue;
},
}),
ConfigToggle({
icon: 'history',
name: getString('History'),
desc: getString("Saves chat history\nMessages in previous chats won't show automatically, but they are there"),
initValue: GeminiService.useHistory,
onChange: (self, newValue) => {
GeminiService.useHistory = newValue;
},
}),
]
})
]
})
});
export const GoogleAiInstructions = () => Box({
homogeneous: true,
children: [Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
setup: (self) => self
.hook(GeminiService, (self, hasKey) => {
self.revealChild = (GeminiService.key.length == 0);
}, 'hasKey')
,
child: Button({
child: Label({
useMarkup: true,
wrap: true,
className: 'txt sidebar-chat-welcome-txt',
justify: Gtk.Justification.CENTER,
label: 'A Google AI API key is required\nYou can grab one <u>here</u>, then enter it below',
// setup: self => self.set_markup("This is a <a href=\"https://www.github.com\">test link</a>")
}),
setup: setupCursorHover,
onClicked: () => {
Utils.execAsync(['bash', '-c', `xdg-open https://makersuite.google.com/app/apikey &`]);
}
})
})]
});
const geminiWelcome = Box({
vexpand: true,
homogeneous: true,
child: Box({
className: 'spacing-v-15',
vpack: 'center',
vertical: true,
children: [
GeminiInfo(),
GoogleAiInstructions(),
GeminiSettings(),
]
})
});
export const chatContent = Box({
className: 'spacing-v-5',
vertical: true,
setup: (self) => self
.hook(GeminiService, (box, id) => {
const message = GeminiService.messages[id];
if (!message) return;
box.add(ChatMessage(message, MODEL_NAME))
}, 'newMsg')
,
});
const clearChat = () => {
GeminiService.clear();
const children = chatContent.get_children();
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.destroy();
}
}
const CommandButton = (command) => Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => sendMessage(command),
setup: setupCursorHover,
label: command,
});
export const geminiCommands = Box({
className: 'spacing-h-5',
children: [
Box({ hexpand: true }),
CommandButton('/key'),
CommandButton('/model'),
CommandButton('/clear'),
]
});
export const sendMessage = (text) => {
// Check if text or API key is empty
if (text.length == 0) return;
if (GeminiService.key.length == 0) {
GeminiService.key = text;
chatContent.add(SystemMessage(`Key saved to\n\`${GeminiService.keyPath}\``, 'API Key', geminiView));
text = '';
return;
}
// Commands
if (text.startsWith('/')) {
if (text.startsWith('/clear')) clearChat();
else if (text.startsWith('/load')) {
clearChat();
GeminiService.loadHistory();
}
else if (text.startsWith('/model')) chatContent.add(SystemMessage(`${getString("Currently using")} \`${GeminiService.modelName}\``, '/model', geminiView))
else if (text.startsWith('/prompt')) {
const firstSpaceIndex = text.indexOf(' ');
const prompt = text.slice(firstSpaceIndex + 1);
if (firstSpaceIndex == -1 || prompt.length < 1) {
chatContent.add(SystemMessage(`Usage: \`/prompt MESSAGE\``, '/prompt', geminiView))
}
else {
GeminiService.addMessage('user', prompt)
}
}
else if (text.startsWith('/key')) {
const parts = text.split(' ');
if (parts.length == 1) chatContent.add(SystemMessage(
`${getString("Key stored in:")} \n\`${GeminiService.keyPath}\`\n${getString("To update this key, type")} \`/key YOUR_API_KEY\``,
'/key',
geminiView));
else {
GeminiService.key = parts[1];
chatContent.add(SystemMessage(`${getString("Updated API Key at")}\n\`${GeminiService.keyPath}\``, '/key', geminiView));
}
}
else if (text.startsWith('/test'))
chatContent.add(SystemMessage(markdownTest, `Markdown test`, geminiView));
else
chatContent.add(SystemMessage(getString(`Invalid command.`), 'Error', geminiView))
}
else {
GeminiService.send(text);
}
}
export const geminiView = Box({
homogeneous: true,
children: [Scrollable({
className: 'sidebar-chat-viewport',
vexpand: true,
child: Box({
vertical: true,
children: [
geminiWelcome,
chatContent,
]
}),
setup: (scrolledWindow) => {
// Show scrollbar
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = scrolledWindow.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
// Avoid click-to-scroll-widget-to-view behavior
Utils.timeout(1, () => {
const viewport = scrolledWindow.child;
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
})
// Always scroll to bottom with new content
const adjustment = scrolledWindow.get_vadjustment();
adjustment.connect("changed", () => Utils.timeout(1, () => {
if (!chatEntry.hasFocus) return;
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
}))
}
})]
});

View file

@ -0,0 +1,419 @@
// TODO: execAsync(['identify', '-format', '{"w":%w,"h":%h}', imagePath])
// to detect img dimensions
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 { Box, Button, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { fileExists } from '../../.miscutils/files.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { MarginRevealer } from '../../.widgethacks/advancedrevealers.js';
import { setupCursorHover, setupCursorHoverInfo } from '../../.widgetutils/cursorhover.js';
import WaifuService from '../../../services/waifus.js';
import { darkMode } from '../../.miscutils/system.js';
import { chatEntry } from '../apiwidgets.js';
async function getImageViewerApp(preferredApp) {
Utils.execAsync(['bash', '-c', `command -v ${preferredApp}`])
.then((output) => {
if (output != '') return preferredApp;
else return 'xdg-open';
})
.catch(print);
}
const IMAGE_REVEAL_DELAY = 13; // Some wait for inits n other weird stuff
const IMAGE_VIEWER_APP = getImageViewerApp(userOptions.apps.imageViewer); // Gnome's image viewer cuz very comfortable zooming
const USER_CACHE_DIR = GLib.get_user_cache_dir();
// Create cache folder and clear pics from previous session
Utils.exec(`bash -c 'mkdir -p ${USER_CACHE_DIR}/ags/media/waifus'`);
Utils.exec(`bash -c 'rm ${USER_CACHE_DIR}/ags/media/waifus/*'`);
const CommandButton = (command) => Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => sendMessage(command),
setup: setupCursorHover,
label: command,
});
export const waifuTabIcon = Box({
hpack: 'center',
children: [
MaterialIcon('photo', 'norm'),
]
});
const WaifuInfo = () => {
const waifuLogo = Label({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
label: 'photo',
})
return Box({
vertical: true,
vexpand: true,
className: 'spacing-v-15',
children: [
waifuLogo,
Label({
className: 'txt txt-title-small sidebar-chat-welcome-txt',
wrap: true,
justify: Gtk.Justification.CENTER,
label: 'Waifus',
}),
Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
Label({
className: 'txt-smallie txt-subtext',
wrap: true,
justify: Gtk.Justification.CENTER,
label: getString('Powered by waifu.im + other APIs'),
}),
Button({
className: 'txt-subtext txt-norm icon-material',
label: 'info',
tooltipText: getString('Type tags for a random pic.\nNSFW content will not be returned unless\nyou explicitly request such a tag.\n\nDisclaimer: Not affiliated with the providers\nnor responsible for any of their content.'),
setup: setupCursorHoverInfo,
}),
]
}),
]
});
}
const waifuWelcome = Box({
vexpand: true,
homogeneous: true,
child: Box({
className: 'spacing-v-15',
vpack: 'center',
vertical: true,
children: [
WaifuInfo(),
]
})
});
const WaifuImage = (taglist) => {
const ImageState = (icon, name) => Box({
className: 'spacing-h-5 txt',
children: [
Box({ hexpand: true }),
Label({
className: 'sidebar-waifu-txt txt-smallie',
xalign: 0,
label: name,
}),
MaterialIcon(icon, 'norm'),
]
})
const ImageAction = ({ name, icon, action }) => Button({
className: 'sidebar-waifu-image-action txt-norm icon-material',
tooltipText: name,
label: icon,
onClicked: action,
setup: setupCursorHover,
})
const downloadState = Stack({
homogeneous: false,
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationSmall,
children: {
'api': ImageState('api', getString('Calling API')),
'download': ImageState('downloading', getString('Downloading image')),
'done': ImageState('done', getString('Finished!')),
'error': ImageState('error', getString('Error')),
'notfound': ImageState('error', getString('Not found!')),
},
});
const downloadIndicator = MarginRevealer({
vpack: 'center',
transition: 'slide_left',
revealChild: true,
child: downloadState,
});
const blockHeading = Box({
hpack: 'fill',
className: 'spacing-h-5',
children: [
...taglist.map((tag) => CommandButton(tag)),
Box({ hexpand: true }),
downloadIndicator,
]
});
const blockImageActions = Revealer({
transition: 'crossfade',
revealChild: false,
child: Box({
vertical: true,
children: [
Box({
className: 'sidebar-waifu-image-actions spacing-h-3',
children: [
Box({ hexpand: true }),
ImageAction({
name: getString('Go to source'),
icon: 'link',
action: () => execAsync(['xdg-open', `${thisBlock.attribute.imageData.source}`]).catch(print),
}),
ImageAction({
name: getString('Hoard'),
icon: 'save',
action: (self) => {
execAsync(['bash', '-c', `mkdir -p $(xdg-user-dir PICTURES)/homework${thisBlock.attribute.isNsfw ? '/🌶️' : ''} && cp ${thisBlock.attribute.imagePath} $(xdg-user-dir PICTURES)/homework${thisBlock.attribute.isNsfw ? '/🌶️/' : ''}`])
.then(() => self.label = 'done')
.catch(print);
},
}),
ImageAction({
name: getString('Open externally'),
icon: 'open_in_new',
action: () => execAsync([IMAGE_VIEWER_APP, `${thisBlock.attribute.imagePath}`]).catch(print),
}),
]
})
],
})
})
const blockImage = Widget.DrawingArea({
className: 'sidebar-waifu-image',
});
const blockImageRevealer = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: Box({
className: 'margin-top-5',
children: [Overlay({
child: Box({
homogeneous: true,
className: 'sidebar-waifu-image',
children: [blockImage],
}),
overlays: [blockImageActions],
})]
}),
});
const thisBlock = Box({
className: 'sidebar-chat-message',
attribute: {
'imagePath': '',
'isNsfw': false,
'imageData': '',
'update': (imageData, force = false) => {
thisBlock.attribute.imageData = imageData;
const { status, signature, url, extension, source, dominant_color, is_nsfw, width, height, tags } = thisBlock.attribute.imageData;
thisBlock.attribute.isNsfw = is_nsfw;
if (status == 404) {
downloadState.shown = 'notfound';
return;
}
if (status != 200) {
downloadState.shown = 'error';
return;
}
thisBlock.attribute.imagePath = `${USER_CACHE_DIR}/ags/media/waifus/${signature}${extension}`;
downloadState.shown = 'download';
// Width/height
const widgetWidth = Math.min(Math.floor(waifuContent.get_allocated_width() * 0.85), width);
const widgetHeight = Math.ceil(widgetWidth * height / width);
blockImage.set_size_request(widgetWidth, widgetHeight);
const showImage = () => {
downloadState.shown = 'done';
const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(thisBlock.attribute.imagePath, widgetWidth, widgetHeight);
// const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(thisBlock.attribute.imagePath, widgetWidth, widgetHeight, false);
blockImage.set_size_request(widgetWidth, widgetHeight);
blockImage.connect("draw", (widget, cr) => {
const borderRadius = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
// Draw a rounded rectangle
cr.arc(borderRadius, borderRadius, borderRadius, Math.PI, 1.5 * Math.PI);
cr.arc(widgetWidth - borderRadius, borderRadius, borderRadius, 1.5 * Math.PI, 2 * Math.PI);
cr.arc(widgetWidth - borderRadius, widgetHeight - borderRadius, borderRadius, 0, 0.5 * Math.PI);
cr.arc(borderRadius, widgetHeight - borderRadius, borderRadius, 0.5 * Math.PI, Math.PI);
cr.closePath();
cr.clip();
// Paint image as bg
Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cr.paint();
});
// Reveal stuff
Utils.timeout(IMAGE_REVEAL_DELAY, () => {
blockImageRevealer.revealChild = true;
})
Utils.timeout(IMAGE_REVEAL_DELAY + blockImageRevealer.transitionDuration,
() => blockImageActions.revealChild = true
);
downloadIndicator.attribute.hide();
}
// Show
if (!force && fileExists(thisBlock.attribute.imagePath)) showImage();
else Utils.execAsync(['bash', '-c', `wget -O '${thisBlock.attribute.imagePath}' '${url}'`])
.then(showImage)
.catch(print);
thisBlock.css = `background-color: mix(${darkMode.value ? 'black' : 'white'}, ${dominant_color}, 0.97);`;
},
},
children: [
Box({
vertical: true,
children: [
blockHeading,
Box({
vertical: true,
hpack: 'start',
children: [blockImageRevealer],
})
]
})
],
});
return thisBlock;
}
const waifuContent = Box({
className: 'spacing-v-15',
vertical: true,
attribute: {
'map': new Map(),
},
setup: (self) => self
.hook(WaifuService, (box, id) => {
if (id === undefined) return;
const newImageBlock = WaifuImage(WaifuService.queries[id]);
box.add(newImageBlock);
box.show_all();
box.attribute.map.set(id, newImageBlock);
}, 'newResponse')
.hook(WaifuService, (box, id) => {
if (id === undefined) return;
const data = WaifuService.responses[id];
if (!data) return;
const imageBlock = box.attribute.map.get(id);
imageBlock?.attribute.update(data);
}, 'updateResponse')
,
});
export const waifuView = Scrollable({
className: 'sidebar-chat-viewport',
vexpand: true,
child: Box({
vertical: true,
children: [
waifuWelcome,
waifuContent,
]
}),
setup: (scrolledWindow) => {
// Show scrollbar
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = scrolledWindow.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
// Avoid click-to-scroll-widget-to-view behavior
Utils.timeout(1, () => {
const viewport = scrolledWindow.child;
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
})
// Always scroll to bottom with new content
const adjustment = scrolledWindow.get_vadjustment();
adjustment.connect("changed", () => {
if (!chatEntry.hasFocus) return;
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
})
}
});
const waifuTags = Revealer({
revealChild: false,
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
className: 'spacing-h-5',
children: [
Scrollable({
vscroll: 'never',
hscroll: 'automatic',
hexpand: true,
child: Box({
className: 'spacing-h-5',
children: [
CommandButton('waifu'),
CommandButton('maid'),
CommandButton('uniform'),
CommandButton('oppai'),
CommandButton('selfies'),
CommandButton('marin-kitagawa'),
CommandButton('raiden-shogun'),
CommandButton('mori-calliope'),
]
})
}),
Box({ className: 'separator-line' }),
]
})
});
export const waifuCommands = Box({
className: 'spacing-h-5',
setup: (self) => {
self.pack_end(CommandButton('/clear'), false, false, 0);
self.pack_start(Button({
className: 'sidebar-chat-chip-toggle',
setup: setupCursorHover,
label: getString('Tags →'),
onClicked: () => {
waifuTags.revealChild = !waifuTags.revealChild;
}
}), false, false, 0);
self.pack_start(waifuTags, true, true, 0);
}
});
const clearChat = () => { // destroy!!
waifuContent.attribute.map.forEach((value, key, map) => {
value.destroy();
value = null;
});
}
function newSimpleImageCall(name, url, width, height, dominantColor = '#9392A6') {
const timeSinceEpoch = Date.now();
const newImage = WaifuImage([`/${name}`]);
waifuContent.add(newImage);
waifuContent.attribute.map.set(timeSinceEpoch, newImage);
Utils.timeout(IMAGE_REVEAL_DELAY, () => newImage?.attribute.update({
status: 200,
url: url,
extension: '',
signature: timeSinceEpoch,
source: url,
dominant_color: dominantColor,
is_nsfw: false,
width: width,
height: height,
tags: [`/${name}`],
}, true));
}
export const sendMessage = (text) => {
// Commands
if (text.startsWith('/')) {
if (text.startsWith('/clear')) clearChat();
else if (text.startsWith('/test'))
newSimpleImageCall('test', 'https://picsum.photos/600/400', 300, 200);
else if (text.startsWith('/chino'))
newSimpleImageCall('chino', 'https://chino.pages.dev/chino', 300, 400, '#B2AEF3');
else if (text.startsWith('/place'))
newSimpleImageCall('place', 'https://placewaifu.com/image/400/600', 400, 600, '#F0A235');
}
else WaifuService.fetch(text);
}

View file

@ -0,0 +1,223 @@
const { Gtk, Gdk } = 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, Button, CenterBox, Entry, EventBox, Icon, Label, Overlay, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { setupCursorHover, setupCursorHoverInfo } from '../.widgetutils/cursorhover.js';
// APIs
import GPTService from '../../services/gpt.js';
import Gemini from '../../services/gemini.js';
import { geminiView, geminiCommands, sendMessage as geminiSendMessage, geminiTabIcon } from './apis/gemini.js';
import { chatGPTView, chatGPTCommands, sendMessage as chatGPTSendMessage, chatGPTTabIcon } from './apis/chatgpt.js';
import { waifuView, waifuCommands, sendMessage as waifuSendMessage, waifuTabIcon } from './apis/waifu.js';
import { booruView, booruCommands, sendMessage as booruSendMessage, booruTabIcon } from './apis/booru.js';
import { enableClickthrough } from "../.widgetutils/clickthrough.js";
import { checkKeybind } from '../.widgetutils/keybind.js';
const TextView = Widget.subclass(Gtk.TextView, "AgsTextView");
import { widgetContent } from './sideleft.js';
import { IconTabContainer } from '../.commonwidgets/tabcontainer.js';
const EXPAND_INPUT_THRESHOLD = 30;
const APILIST = {
'gemini': {
name: 'Assistant (Gemini Pro)',
sendCommand: geminiSendMessage,
contentWidget: geminiView,
commandBar: geminiCommands,
tabIcon: geminiTabIcon,
placeholderText: getString('Message Gemini...'),
},
'gpt': {
name: 'Assistant (GPTs)',
sendCommand: chatGPTSendMessage,
contentWidget: chatGPTView,
commandBar: chatGPTCommands,
tabIcon: chatGPTTabIcon,
placeholderText: getString('Message the model...'),
},
'waifu': {
name: 'Waifus',
sendCommand: waifuSendMessage,
contentWidget: waifuView,
commandBar: waifuCommands,
tabIcon: waifuTabIcon,
placeholderText: getString('Enter tags'),
},
'booru': {
name: 'Booru',
sendCommand: booruSendMessage,
contentWidget: booruView,
commandBar: booruCommands,
tabIcon: booruTabIcon,
placeholderText: getString('Enter tags'),
},
}
const APIS = userOptions.sidebar.pages.apis.order.map((apiName) => APILIST[apiName]);
let currentApiId = 0;
function apiSendMessage(textView) {
// Get text
const buffer = textView.get_buffer();
const [start, end] = buffer.get_bounds();
const text = buffer.get_text(start, end, true).trimStart();
if (!text || text.length == 0) return;
// Send
APIS[currentApiId].sendCommand(text)
// Reset
buffer.set_text("", -1);
chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false);
chatEntry.set_valign(Gtk.Align.CENTER);
}
export const chatEntry = TextView({
hexpand: true,
wrapMode: Gtk.WrapMode.WORD_CHAR,
acceptsTab: false,
className: 'sidebar-chat-entry txt txt-smallie',
setup: (self) => self
.hook(App, (self, currentName, visible) => {
if (visible && currentName === 'sideleft') {
self.grab_focus();
}
})
.hook(GPTService, (self) => {
if (APIS[currentApiId].name != 'Assistant (GPTs)') return;
self.placeholderText = (GPTService.key.length > 0 ? getString('Message the model...') : getString('Enter API Key...'));
}, 'hasKey')
.hook(Gemini, (self) => {
if (APIS[currentApiId].name != 'Assistant (Gemini Pro)') return;
self.placeholderText = (Gemini.key.length > 0 ? getString('Message Gemini...') : getString('Enter Google AI API Key...'));
}, 'hasKey')
.on("key-press-event", (widget, event) => {
// Don't send when Shift+Enter
if (event.get_keyval()[1] === Gdk.KEY_Return || event.get_keyval()[1] === Gdk.KEY_KP_Enter) {
if (event.get_state()[1] !== 17) {// SHIFT_MASK doesn't work but 17 should be shift
apiSendMessage(widget);
return true;
}
return false;
}
// Keybinds
if (checkKeybind(event, userOptions.keybinds.sidebar.cycleTab))
widgetContent.cycleTab();
else if (checkKeybind(event, userOptions.keybinds.sidebar.nextTab))
widgetContent.nextTab();
else if (checkKeybind(event, userOptions.keybinds.sidebar.prevTab))
widgetContent.prevTab();
else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.nextTab)) {
apiWidgets.attribute.nextTab();
return true;
}
else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.prevTab)) {
apiWidgets.attribute.prevTab();
return true;
}
})
,
});
chatEntry.get_buffer().connect("changed", (buffer) => {
const bufferText = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), true);
chatSendButton.toggleClassName('sidebar-chat-send-available', bufferText.length > 0);
chatPlaceholderRevealer.revealChild = (bufferText.length == 0);
if (buffer.get_line_count() > 1 || bufferText.length > EXPAND_INPUT_THRESHOLD) {
chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', true);
chatEntry.set_valign(Gtk.Align.FILL);
chatPlaceholder.set_valign(Gtk.Align.FILL);
}
else {
chatEntryWrapper.toggleClassName('sidebar-chat-wrapper-extended', false);
chatEntry.set_valign(Gtk.Align.CENTER);
chatPlaceholder.set_valign(Gtk.Align.CENTER);
}
});
const chatEntryWrapper = Scrollable({
className: 'sidebar-chat-wrapper',
hscroll: 'never',
vscroll: 'always',
child: chatEntry,
});
const chatSendButton = Button({
className: 'txt-norm icon-material sidebar-chat-send',
vpack: 'end',
label: 'arrow_upward',
setup: setupCursorHover,
onClicked: (self) => {
APIS[currentApiId].sendCommand(chatEntry.get_buffer().text);
chatEntry.get_buffer().set_text("", -1);
},
});
const chatPlaceholder = Label({
className: 'txt-subtext txt-smallie margin-left-5',
hpack: 'start',
vpack: 'center',
label: APIS[currentApiId].placeholderText,
});
const chatPlaceholderRevealer = Revealer({
revealChild: true,
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
child: chatPlaceholder,
setup: enableClickthrough,
});
const textboxArea = Box({ // Entry area
className: 'sidebar-chat-textarea',
children: [
Overlay({
passThrough: true,
child: chatEntryWrapper,
overlays: [chatPlaceholderRevealer],
}),
Box({ className: 'width-10' }),
chatSendButton,
]
});
const apiCommandStack = Stack({
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationLarge,
children: APIS.reduce((acc, api) => {
acc[api.name] = api.commandBar;
return acc;
}, {}),
})
export const apiContentStack = IconTabContainer({
tabSwitcherClassName: 'sidebar-icontabswitcher',
className: 'margin-top-5',
iconWidgets: APIS.map((api) => api.tabIcon),
names: APIS.map((api) => api.name),
children: APIS.map((api) => api.contentWidget),
onChange: (self, id) => {
apiCommandStack.shown = APIS[id].name;
chatPlaceholder.label = APIS[id].placeholderText;
currentApiId = id;
}
});
function switchToTab(id) {
apiContentStack.shown.value = id;
}
const apiWidgets = Widget.Box({
attribute: {
'nextTab': () => switchToTab(Math.min(currentApiId + 1, APIS.length - 1)),
'prevTab': () => switchToTab(Math.max(0, currentApiId - 1)),
},
vertical: true,
className: 'spacing-v-10',
homogeneous: false,
children: [
apiContentStack,
apiCommandStack,
textboxArea,
],
});
export default apiWidgets;

View file

@ -0,0 +1,18 @@
import PopupWindow from '../.widgethacks/popupwindow.js';
import SidebarLeft from "./sideleft.js";
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box } = Widget;
import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
export default () => PopupWindow({
keymode: 'on-demand',
anchor: ['left', 'top', 'bottom'],
name: 'sideleft',
layer: 'top',
child: Box({
children: [
SidebarLeft(),
clickCloseRegion({ name: 'sideleft', multimonitor: false, fillMonitor: 'horizontal' }),
]
})
});

View file

@ -0,0 +1,121 @@
const { Gdk } = 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 { Box, Button, EventBox, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import toolBox from './toolbox.js';
import apiWidgets from './apiwidgets.js';
import { chatEntry } from './apiwidgets.js';
import { TabContainer } from '../.commonwidgets/tabcontainer.js';
import { checkKeybind } from '../.widgetutils/keybind.js';
const SIDEBARTABS = {
'apis': {
name: 'apis',
content: apiWidgets,
materialIcon: 'api',
friendlyName: 'APIs',
},
'tools': {
name: 'tools',
content: toolBox,
materialIcon: 'home_repair_service',
friendlyName: 'Tools',
},
}
const CONTENTS = userOptions.sidebar.pages.order.map((tabName) => SIDEBARTABS[tabName])
const pinButton = Button({
attribute: {
'enabled': false,
'toggle': (self) => {
self.attribute.enabled = !self.attribute.enabled;
self.toggleClassName('sidebar-pin-enabled', self.attribute.enabled);
const sideleftWindow = App.getWindow('sideleft');
const sideleftContent = sideleftWindow.get_children()[0].get_children()[0].get_children()[1];
sideleftContent.toggleClassName('sidebar-pinned', self.attribute.enabled);
if (self.attribute.enabled) {
sideleftWindow.exclusivity = 'on-demad';
}
else {
sideleftWindow.exclusivity = 'normal';
}
},
},
vpack: 'start',
className: 'sidebar-pin',
child: MaterialIcon('push_pin', 'larger'),
tooltipText: 'Pin sidebar (Ctrl+P)',
onClicked: (self) => self.attribute.toggle(self),
setup: (self) => {
setupCursorHover(self);
self.hook(App, (self, currentName, visible) => {
if (currentName === 'sideleft' && visible) self.grab_focus();
})
},
})
export const widgetContent = TabContainer({
icons: CONTENTS.map((item) => item.materialIcon),
names: CONTENTS.map((item) => item.friendlyName),
children: CONTENTS.map((item) => item.content),
className: 'sidebar-left spacing-v-10',
setup: (self) => self.hook(App, (self, currentName, visible) => {
if (currentName === 'sideleft')
self.toggleClassName('sidebar-pinned', pinButton.attribute.enabled && visible);
}),
});
export default () => Box({
// vertical: true,
vexpand: true,
css: 'min-width: 2px;',
children: [
widgetContent,
],
setup: (self) => self
.on('key-press-event', (widget, event) => { // Handle keybinds
if (checkKeybind(event, userOptions.keybinds.sidebar.pin))
pinButton.attribute.toggle(pinButton);
else if (checkKeybind(event, userOptions.keybinds.sidebar.cycleTab))
widgetContent.cycleTab();
else if (checkKeybind(event, userOptions.keybinds.sidebar.nextTab))
widgetContent.nextTab();
else if (checkKeybind(event, userOptions.keybinds.sidebar.prevTab))
widgetContent.prevTab();
if (widgetContent.attribute.names[widgetContent.attribute.shown.value] == 'APIs') { // If api tab is focused
// Focus entry when typing
if ((
!(event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK) &&
event.get_keyval()[1] >= 32 && event.get_keyval()[1] <= 126 &&
widget != chatEntry && event.get_keyval()[1] != Gdk.KEY_space)
||
((event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK) &&
event.get_keyval()[1] === Gdk.KEY_v)
) {
chatEntry.grab_focus();
const buffer = chatEntry.get_buffer();
buffer.set_text(buffer.text + String.fromCharCode(event.get_keyval()[1]), -1);
buffer.place_cursor(buffer.get_iter_at_offset(-1));
}
// Switch API type
else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.nextTab)) {
const toSwitchTab = widgetContent.attribute.children[widgetContent.attribute.shown.value];
toSwitchTab.nextTab();
}
else if (checkKeybind(event, userOptions.keybinds.sidebar.apis.prevTab)) {
const toSwitchTab = widgetContent.attribute.children[widgetContent.attribute.shown.value];
toSwitchTab.prevTab();
}
}
})
,
});

View file

@ -0,0 +1,20 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Label, Scrollable } = Widget;
import QuickScripts from './tools/quickscripts.js';
import ColorPicker from './tools/colorpicker.js';
import Name from './tools/name.js';
export default Scrollable({
hscroll: "never",
vscroll: "automatic",
child: Box({
vertical: true,
className: 'spacing-v-10',
children: [
QuickScripts(),
ColorPicker(),
Box({ vexpand: true }),
Name(),
]
})
});

View file

@ -0,0 +1,99 @@
#!/bin/bash
# Function to get the current resolution
get_current_resolution() {
local output
output=$(hyprctl monitors -j)
local width height refreshRate
width=$(echo "$output" | jq -r '.[0].width')
height=$(echo "$output" | jq -r '.[0].height')
refreshRate=$(echo "$output" | jq -r '.[0].refreshRate')
echo "$width $height $refreshRate"
}
# Function to update the Hyprland configuration with the new resolution
update_resolution_config() {
local newWidth="$1"
local newHeight="$2"
local newRefreshRate="$3"
local currentRes
currentRes=$(get_current_resolution)
local width height refreshRate
width=${newWidth:-$(echo "$currentRes" | awk '{print $1}')}
height=${newHeight:-$(echo "$currentRes" | awk '{print $2}')}
refreshRate=${newRefreshRate:-$(echo "$currentRes" | awk '{print $3}')}
local modelineOutput
modelineOutput=$(gtf "$width" "$height" "$refreshRate")
local modeline
modeline=$(echo "$modelineOutput" | grep -oP 'Modeline "\K[^"]+')
if [ -z "$modeline" ]; then
echo "Failed to generate modeline"
exit 1
fi
# Extract the resolution and refresh rate from the modeline
local resolution
resolution=$(echo "$modeline" | grep -oP '^[0-9]+x[0-9]+')
local rate
rate=$(echo "$modeline" | grep -oP '[0-9]+.[0-9]+$')
if [ -z "$resolution" ] || [ -z "$rate" ]; then
echo "Failed to extract resolution or refresh rate from modeline"
exit 1
fi
local configPath="${HOME}/.config/hypr/hyprland/general.conf"
local newConfigContent
newConfigContent=$(sed "s/^monitor=.*$/monitor=eDP-1, $resolution@$rate, auto, 1/" "$configPath")
echo "$newConfigContent" > "$configPath"
}
# Main script
echo "Welcome to the Resolution Configurator"
echo ""
echo " +---------------------------+"
echo " | _____ |"
echo " | | | |"
echo " | | | |"
echo " | |_____| |"
echo " | |"
echo " +---------------------------+"
echo ""
echo "Current resolution and refresh rate:"
currentRes=$(get_current_resolution)
width=$(echo "$currentRes" | awk '{print $1}')
height=$(echo "$currentRes" | awk '{print $2}')
refreshRate=$(echo "$currentRes" | awk '{print $3}')
echo "Width: $width px"
echo "Height: $height px"
echo "Refresh Rate: $refreshRate Hz"
echo ""
read -p "Enter new width (or press Enter to keep current width): " newWidth
read -p "Enter new height (or press Enter to keep current height): " newHeight
read -p "Enter new refresh rate (or press Enter to keep current refresh rate): " newRefreshRate
# Validate inputs (if provided)
if [[ ! "$newWidth" =~ ^[0-9]+$ && -n "$newWidth" ]]; then
echo "Invalid width value."
exit 1
fi
if [[ ! "$newHeight" =~ ^[0-9]+$ && -n "$newHeight" ]]; then
echo "Invalid height value."
exit 1
fi
if [[ ! "$newRefreshRate" =~ ^[0-9]+$ && -n "$newRefreshRate" ]]; then
echo "Invalid refresh rate value."
exit 1
fi
update_resolution_config "$newWidth" "$newHeight" "$newRefreshRate"
echo "Resolution updated successfully."

View file

@ -0,0 +1,198 @@
// It's weird, I know
const { Gio, GLib } = imports.gi;
import Service from 'resource:///com/github/Aylur/ags/service.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { exec, execAsync } = Utils;
import { clamp } from '../../.miscutils/mathfuncs.js';
export class ColorPickerSelection extends Service {
static {
Service.register(this, {
'picked': [],
'assigned': ['int'],
'hue': [],
'sl': [],
});
}
_hue = 198;
_xAxis = 94;
_yAxis = 80;
get hue() { return this._hue; }
set hue(value) {
this._hue = clamp(value, 0, 360);
this.emit('hue');
this.emit('picked');
this.emit('changed');
}
get xAxis() { return this._xAxis; }
set xAxis(value) {
this._xAxis = clamp(value, 0, 100);
this.emit('sl');
this.emit('picked');
this.emit('changed');
}
get yAxis() { return this._yAxis; }
set yAxis(value) {
this._yAxis = clamp(value, 0, 100);
this.emit('sl');
this.emit('picked');
this.emit('changed');
}
setColorFromHex(hexString, id) {
const hsl = hexToHSL(hexString);
this._hue = hsl.hue;
this._xAxis = hsl.saturation;
// this._yAxis = hsl.lightness;
this._yAxis = (100 - hsl.saturation / 2) / 100 * hsl.lightness;
// console.log(this._hue, this._xAxis, this._yAxis)
this.emit('assigned', id);
this.emit('changed');
}
constructor() {
super();
this.emit('changed');
}
}
export function hslToRgbValues(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const to255 = x => Math.round(x * 255);
r = to255(r);
g = to255(g);
b = to255(b);
return `${Math.round(r)},${Math.round(g)},${Math.round(b)}`;
// return `rgb(${r},${g},${b})`;
}
export function hslToHex(h, s, l) {
h /= 360;
s /= 100;
l /= 100;
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
const toHex = x => {
const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// export function hexToHSL(hex) {
// // Remove the '#' if present
// hex = hex.replace(/^#/, '');
// // Parse the hex value into RGB components
// const bigint = parseInt(hex, 16);
// const r = (bigint >> 16) & 255;
// const g = (bigint >> 8) & 255;
// const b = bigint & 255;
// // Normalize RGB values to range [0, 1]
// const normalizedR = r / 255;
// const normalizedG = g / 255;
// const normalizedB = b / 255;
// // Find the maximum and minimum values
// const max = Math.max(normalizedR, normalizedG, normalizedB);
// const min = Math.min(normalizedR, normalizedG, normalizedB);
// // Calculate the lightness
// const lightness = (max + min) / 2;
// // If the color is grayscale, set saturation to 0
// if (max === min) {
// return {
// hue: 0,
// saturation: 0,
// lightness: lightness * 100 // Convert to percentage
// };
// }
// // Calculate the saturation
// const d = max - min;
// const saturation = lightness > 0.5 ? d / (2 - max - min) : d / (max + min);
// // Calculate the hue
// let hue;
// if (max === normalizedR) {
// hue = ((normalizedG - normalizedB) / d + (normalizedG < normalizedB ? 6 : 0)) * 60;
// } else if (max === normalizedG) {
// hue = ((normalizedB - normalizedR) / d + 2) * 60;
// } else {
// hue = ((normalizedR - normalizedG) / d + 4) * 60;
// }
// return {
// hue: Math.round(hue),
// saturation: Math.round(saturation * 100), // Convert to percentage
// lightness: Math.round(lightness * 100) // Convert to percentage
// };
// }
export function hexToHSL(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
var r = parseInt(result[1], 16);
var g = parseInt(result[2], 16);
var b = parseInt(result[3], 16);
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
s = s * 100;
s = Math.round(s);
l = l * 100;
l = Math.round(l);
h = Math.round(360 * h);
return {
hue: h,
saturation: s,
lightness: l
};
}

View file

@ -0,0 +1,283 @@
// TODO: Make selection update when entry changes
const { Gtk } = imports.gi;
import App from 'resource:///com/github/Aylur/ags/app.js';
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';
const { execAsync, exec } = Utils;
const { Box, Button, Entry, EventBox, Icon, Label, Overlay, Scrollable } = Widget;
import SidebarModule from './module.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { ColorPickerSelection, hslToHex, hslToRgbValues, hexToHSL } from './color.js';
import { clamp } from '../../.miscutils/mathfuncs.js';
export default () => {
const selectedColor = new ColorPickerSelection();
function shouldUseBlackColor() {
return ((selectedColor.xAxis < 40 || (45 <= selectedColor.hue && selectedColor.hue <= 195)) &&
selectedColor.yAxis > 60);
}
const colorBlack = 'rgba(0,0,0,0.9)';
const colorWhite = 'rgba(255,255,255,0.9)';
const hueRange = Box({
homogeneous: true,
className: 'sidebar-module-colorpicker-wrapper',
children: [Box({
className: 'sidebar-module-colorpicker-hue',
css: `background: linear-gradient(to bottom, #ff6666, #ffff66, #66dd66, #66ffff, #6666ff, #ff66ff, #ff6666);`,
})],
});
const hueSlider = Box({
vpack: 'start',
className: 'sidebar-module-colorpicker-cursorwrapper',
css: `margin-top: ${13.636 * selectedColor.hue / 360}rem;`,
homogeneous: true,
children: [Box({
className: 'sidebar-module-colorpicker-hue-cursor',
})],
setup: (self) => self.hook(selectedColor, () => {
const widgetHeight = hueRange.children[0].get_allocated_height();
self.setCss(`margin-top: ${13.636 * selectedColor.hue / 360}rem;`)
}),
});
const hueSelector = Box({
children: [EventBox({
child: Overlay({
child: hueRange,
overlays: [hueSlider],
}),
attribute: {
clicked: false,
setHue: (self, event) => {
const widgetHeight = hueRange.children[0].get_allocated_height();
const [_, cursorX, cursorY] = event.get_coords();
const cursorYPercent = clamp(cursorY / widgetHeight, 0, 1);
selectedColor.hue = Math.round(cursorYPercent * 360);
}
},
setup: (self) => self
.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
self.attribute.setHue(self, event);
})
.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;
self.attribute.setHue(self, event);
})
.on('button-release-event', (self) => self.attribute.clicked = false)
,
})]
});
const saturationAndLightnessRange = Box({
homogeneous: true,
children: [Box({
className: 'sidebar-module-colorpicker-saturationandlightness',
attribute: {
update: (self) => {
// css: `background: linear-gradient(to right, #ffffff, color);`,
self.setCss(`background:
linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1)),
linear-gradient(to right, #ffffff, ${hslToHex(selectedColor.hue, 100, 50)});
`);
},
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})],
});
const saturationAndLightnessCursor = Box({
className: 'sidebar-module-colorpicker-saturationandlightness-cursorwrapper',
children: [Box({
vpack: 'start',
hpack: 'start',
homogeneous: true,
css: `
margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
`, // Why 13.636rem? see class name in stylesheet
attribute: {
update: (self) => {
const allocation = saturationAndLightnessRange.children[0].get_allocation();
self.setCss(`
margin-left: ${13.636 * selectedColor.xAxis / 100}rem;
margin-top: ${13.636 * (100 - selectedColor.yAxis) / 100}rem;
`); // Why 13.636rem? see class name in stylesheet
}
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'assigned')
,
children: [Box({
className: 'sidebar-module-colorpicker-saturationandlightness-cursor',
css: `
background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
`,
attribute: {
update: (self) => {
self.setCss(`
background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};
border-color: ${shouldUseBlackColor() ? colorBlack : colorWhite};
`);
}
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
})],
})]
});
const saturationAndLightnessSelector = Box({
homogeneous: true,
className: 'sidebar-module-colorpicker-saturationandlightness-wrapper',
children: [EventBox({
child: Overlay({
child: saturationAndLightnessRange,
overlays: [saturationAndLightnessCursor],
}),
attribute: {
clicked: false,
setSaturationAndLightness: (self, event) => {
const allocation = saturationAndLightnessRange.children[0].get_allocation();
const [_, cursorX, cursorY] = event.get_coords();
const cursorXPercent = clamp(cursorX / allocation.width, 0, 1);
const cursorYPercent = clamp(cursorY / allocation.height, 0, 1);
selectedColor.xAxis = Math.round(cursorXPercent * 100);
selectedColor.yAxis = Math.round(100 - cursorYPercent * 100);
}
},
setup: (self) => self
.on('motion-notify-event', (self, event) => {
if (!self.attribute.clicked) return;
self.attribute.setSaturationAndLightness(self, event);
})
.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;
self.attribute.setSaturationAndLightness(self, event);
})
.on('button-release-event', (self) => self.attribute.clicked = false)
,
})]
});
const resultColorBox = Box({
className: 'sidebar-module-colorpicker-result-box',
homogeneous: true,
css: `background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`,
children: [Label({
className: 'txt txt-small',
label: getString('Result'),
}),],
attribute: {
update: (self) => {
self.setCss(`background-color: ${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))};`);
self.children[0].setCss(`color: ${shouldUseBlackColor() ? colorBlack : colorWhite};`)
}
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
,
});
const ResultBox = ({ colorSystemName, updateCallback, copyCallback }) => Box({
children: [
Box({
vertical: true,
hexpand: true,
children: [
Label({
xalign: 0,
className: 'txt-tiny',
label: colorSystemName,
}),
Overlay({
child: Entry({
widthChars: 10,
className: 'txt-small techfont',
attribute: {
id: 0,
update: updateCallback,
},
setup: (self) => self
.hook(selectedColor, self.attribute.update, 'sl')
.hook(selectedColor, self.attribute.update, 'hue')
.hook(selectedColor, self.attribute.update, 'assigned')
// .on('activate', (self) => {
// const newColor = self.text;
// if (newColor.length != 7) return;
// selectedColor.setColorFromHex(self.text, self.attribute.id);
// })
,
}),
})
]
}),
Button({
child: MaterialIcon('content_copy', 'norm'),
onClicked: (self) => {
copyCallback(self);
self.child.label = 'done';
Utils.timeout(1000, () => self.child.label = 'content_copy');
},
setup: setupCursorHover,
})
]
});
const resultHex = ResultBox({
colorSystemName: 'Hex',
updateCallback: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
},
copyCallback: () => Utils.execAsync(['wl-copy', `${hslToHex(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}`]),
})
const resultRgb = ResultBox({
colorSystemName: 'RGB',
updateCallback: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100));
},
copyCallback: () => Utils.execAsync(['wl-copy', `rgb(${hslToRgbValues(selectedColor.hue, selectedColor.xAxis, selectedColor.yAxis / (1 + selectedColor.xAxis / 100))})`]),
})
const resultHsl = ResultBox({
colorSystemName: 'HSL',
updateCallback: (self, id) => {
if (id && self.attribute.id === id) return;
self.text = `${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%`;
},
copyCallback: () => Utils.execAsync(['wl-copy', `hsl(${selectedColor.hue},${selectedColor.xAxis}%,${Math.round(selectedColor.yAxis / (1 + selectedColor.xAxis / 100))}%)`]),
})
const result = Box({
className: 'sidebar-module-colorpicker-result-area spacing-v-5 txt',
hexpand: true,
vertical: true,
children: [
resultColorBox,
resultHex,
resultRgb,
resultHsl,
]
})
return SidebarModule({
icon: MaterialIcon('colorize', 'norm'),
name: getString('<span strikethrough="true">Inaccurate</span> Color picker'),
revealChild: false,
child: Box({
className: 'spacing-h-5',
children: [
hueSelector,
saturationAndLightnessSelector,
result,
]
})
});
}

View file

@ -0,0 +1,57 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
const { Box, Button, Icon, Label, Revealer } = Widget;
export default ({
icon,
name,
child,
revealChild = true,
}) => {
const headerButtonIcon = MaterialIcon(revealChild ? 'expand_less' : 'expand_more', 'norm');
const header = Button({
onClicked: () => {
content.revealChild = !content.revealChild;
headerButtonIcon.label = content.revealChild ? 'expand_less' : 'expand_more';
},
setup: setupCursorHover,
child: Box({
className: 'txt spacing-h-10',
children: [
icon,
Label({
className: 'txt-norm',
label: `${name}`,
useMarkup: true,
}),
Box({
hexpand: true,
}),
Box({
className: 'sidebar-module-btn-arrow',
homogeneous: true,
children: [headerButtonIcon],
})
]
})
});
const content = Revealer({
revealChild: revealChild,
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
className: 'margin-top-5',
homogeneous: true,
children: [child],
}),
});
return Box({
className: 'sidebar-module',
vertical: true,
children: [
header,
content,
]
});
}

View file

@ -0,0 +1,26 @@
const { Gtk } = 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';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
const { execAsync, exec } = Utils;
const { Box, Button, CenterBox, EventBox, Icon, Label, Scrollable } = Widget;
export default () => Box({
className: 'txt sidebar-module techfont',
children: [
Label({
label: getString('illogical-impulse')
}),
Box({ hexpand: true }),
Button({
className: 'sidebar-module-btn-arrow',
onClicked: () => execAsync(['xdg-open', 'https://github.com/end-4/dots-hyprland']).catch(print),
child: Icon({
className: 'txt txt-norm',
icon: 'github-symbolic',
}),
setup: setupCursorHover,
})
]
})

View file

@ -0,0 +1,103 @@
const { Gtk } = 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 { execAsync, exec } = Utils;
const { Box, Button, EventBox, Icon, Label, Scrollable } = Widget;
import SidebarModule from './module.js';
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { distroID, isArchDistro, isDebianDistro, hasFlatpak } from '../../.miscutils/system.js';
const scripts = [
{
icon: 'desktop-symbolic',
name: getString('Change screen resolution'),
command: `bash ${App.configDir}/modules/sideleft/tools/changeres.sh`,
enabled: true,
},
{
icon: 'nixos-symbolic',
name: getString('Trim system generations to 5'),
command: `sudo ${App.configDir}/scripts/quickscripts/nixos-trim-generations.sh 5 0 system`,
enabled: distroID == 'nixos',
},
{
icon: 'nixos-symbolic',
name: getString('Trim home manager generations to 5'),
command: `${App.configDir}/scripts/quickscripts/nixos-trim-generations.sh 5 0 home-manager`,
enabled: distroID == 'nixos',
},
{
icon: 'ubuntu-symbolic',
name: getString('Update packages'),
command: `sudo apt update && sudo apt upgrade -y`,
enabled: isDebianDistro,
},
{
icon: 'fedora-symbolic',
name: getString('Update packages'),
command: `sudo dnf upgrade -y`,
enabled: distroID == 'fedora',
},
{
icon: 'arch-symbolic',
name: getString('Update packages'),
command: `sudo pacman -Syyu`,
enabled: isArchDistro,
},
{
icon: 'arch-symbolic',
name: getString('Remove orphan packages'),
command: `sudo pacman -R $(pacman -Qdtq)`,
enabled: isArchDistro,
},
{
icon: 'flatpak-symbolic',
name: getString('Uninstall unused flatpak packages'),
command: `flatpak uninstall --unused`,
enabled: hasFlatpak,
},
];
export default () => SidebarModule({
icon: MaterialIcon('code', 'norm'),
name: getString('Quick scripts'),
child: Box({
vertical: true,
className: 'spacing-v-5',
children: scripts.map((script) => {
if (!script.enabled) return null;
const scriptStateIcon = MaterialIcon('not_started', 'norm');
return Box({
className: 'spacing-h-5 txt',
children: [
Icon({
className: 'sidebar-module-btn-icon txt-large',
icon: script.icon,
}),
Label({
className: 'txt-small',
hpack: 'start',
hexpand: true,
label: script.name,
tooltipText: script.command,
}),
Button({
className: 'sidebar-module-scripts-button',
child: scriptStateIcon,
onClicked: () => {
closeEverything();
execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${script.command}"`]).catch(print)
.then(() => {
scriptStateIcon.label = 'done';
})
},
setup: setupCursorHover,
}),
],
})
}),
})
});

View file

@ -0,0 +1,203 @@
const { Gio } = 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, Button, Label } = Widget;
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { TodoWidget } from "./todolist.js";
import { getCalendarLayout } from "./calendar_layout.js";
let calendarJson = getCalendarLayout(undefined, true);
let monthshift = 0;
function getDateInXMonthsTime(x) {
var currentDate = new Date(); // Get the current date
var targetMonth = currentDate.getMonth() + x; // Calculate the target month
var targetYear = currentDate.getFullYear(); // Get the current year
// Adjust the year and month if necessary
targetYear += Math.floor(targetMonth / 12);
targetMonth = (targetMonth % 12 + 12) % 12;
// Create a new date object with the target year and month
var targetDate = new Date(targetYear, targetMonth, 1);
// Set the day to the last day of the month to get the desired date
// targetDate.setDate(0);
return targetDate;
}
const weekDays = [ // MONDAY IS THE FIRST DAY OF THE WEEK :HESRIGHTYOUKNOW:
{ day: getString('Mo'), today: 0 },
{ day: getString('Tu'), today: 0 },
{ day: getString('We'), today: 0 },
{ day: getString('Th'), today: 0 },
{ day: getString('Fr'), today: 0 },
{ day: getString('Sa'), today: 0 },
{ day: getString('Su'), today: 0 },
]
const CalendarDay = (day, today) => Widget.Button({
className: `sidebar-calendar-btn ${today == 1 ? 'sidebar-calendar-btn-today' : (today == -1 ? 'sidebar-calendar-btn-othermonth' : '')}`,
child: Widget.Overlay({
child: Box({}),
overlays: [Label({
hpack: 'center',
className: 'txt-smallie txt-semibold sidebar-calendar-btn-txt',
label: String(day),
})],
})
})
const CalendarWidget = () => {
const calendarMonthYear = Widget.Button({
className: 'txt txt-large sidebar-calendar-monthyear-btn',
onClicked: () => shiftCalendarXMonths(0),
setup: (button) => {
button.label = `${new Date().toLocaleString('default', { month: 'long' })} ${new Date().getFullYear()}`;
setupCursorHover(button);
}
});
const addCalendarChildren = (box, calendarJson) => {
const children = box.get_children();
for (let i = 0; i < children.length; i++) {
const child = children[i];
child.destroy();
}
box.children = calendarJson.map((row, i) => Widget.Box({
className: 'spacing-h-5',
children: row.map((day, i) => CalendarDay(day.day, day.today)),
}))
}
function shiftCalendarXMonths(x) {
if (x == 0) monthshift = 0;
else monthshift += x;
var newDate;
if (monthshift == 0) newDate = new Date();
else newDate = getDateInXMonthsTime(monthshift);
calendarJson = getCalendarLayout(newDate, (monthshift == 0));
calendarMonthYear.label = `${monthshift == 0 ? '' : '• '}${newDate.toLocaleString('default', { month: 'long' })} ${newDate.getFullYear()}`;
addCalendarChildren(calendarDays, calendarJson);
}
const calendarHeader = Widget.Box({
className: 'spacing-h-5 sidebar-calendar-header',
setup: (box) => {
box.pack_start(calendarMonthYear, false, false, 0);
box.pack_end(Widget.Box({
className: 'spacing-h-5',
children: [
Button({
className: 'sidebar-calendar-monthshift-btn',
onClicked: () => shiftCalendarXMonths(-1),
child: MaterialIcon('chevron_left', 'norm'),
setup: setupCursorHover,
}),
Button({
className: 'sidebar-calendar-monthshift-btn',
onClicked: () => shiftCalendarXMonths(1),
child: MaterialIcon('chevron_right', 'norm'),
setup: setupCursorHover,
})
]
}), false, false, 0);
}
})
const calendarDays = Widget.Box({
hexpand: true,
vertical: true,
className: 'spacing-v-5',
setup: (box) => {
addCalendarChildren(box, calendarJson);
}
});
return Widget.EventBox({
onScrollUp: () => shiftCalendarXMonths(-1),
onScrollDown: () => shiftCalendarXMonths(1),
child: Widget.Box({
hpack: 'center',
children: [
Widget.Box({
hexpand: true,
vertical: true,
className: 'spacing-v-5',
children: [
calendarHeader,
Widget.Box({
homogeneous: true,
className: 'spacing-h-5',
children: weekDays.map((day, i) => CalendarDay(day.day, day.today))
}),
calendarDays,
]
})
]
})
});
};
const defaultShown = 'calendar';
const contentStack = Widget.Stack({
hexpand: true,
children: {
'calendar': CalendarWidget(),
'todo': TodoWidget(),
// 'stars': Widget.Label({ label: 'GitHub feed will be here' }),
},
transition: 'slide_up_down',
transitionDuration: userOptions.animations.durationLarge,
setup: (stack) => Utils.timeout(1, () => {
stack.shown = defaultShown;
})
})
const StackButton = (stackItemName, icon, name) => Widget.Button({
className: 'button-minsize sidebar-navrail-btn txt-small spacing-h-5',
onClicked: (button) => {
contentStack.shown = stackItemName;
const kids = button.get_parent().get_children();
for (let i = 0; i < kids.length; i++) {
if (kids[i] != button) kids[i].toggleClassName('sidebar-navrail-btn-active', false);
else button.toggleClassName('sidebar-navrail-btn-active', true);
}
},
child: Box({
className: 'spacing-v-5',
vertical: true,
children: [
Label({
className: `txt icon-material txt-hugeass`,
label: icon,
}),
Label({
label: name,
className: 'txt txt-smallie',
}),
]
}),
setup: (button) => Utils.timeout(1, () => {
setupCursorHover(button);
button.toggleClassName('sidebar-navrail-btn-active', defaultShown === stackItemName);
})
});
export const ModuleCalendar = () => Box({
className: 'sidebar-group spacing-h-5',
setup: (box) => {
box.pack_start(Box({
vpack: 'center',
homogeneous: true,
vertical: true,
className: 'sidebar-navrail spacing-v-10',
children: [
StackButton('calendar', 'calendar_month', getString('Calendar')),
StackButton('todo', 'done_outline', getString('To Do')),
// StackButton(box, 'stars', 'star', 'GitHub'),
]
}), false, false, 0);
box.pack_end(contentStack, false, false, 0);
}
})

View file

@ -0,0 +1,85 @@
function checkLeapYear(year) {
return (
year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0));
}
function getMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
if (month == 2 && leapYear) return 29;
if (month == 2 && !leapYear) return 28;
return 30;
}
function getNextMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 1 && leapYear) return 29;
if (month == 1 && !leapYear) return 28;
if (month == 12) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getPrevMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 3 && leapYear) return 29;
if (month == 3 && !leapYear) return 28;
if (month == 1) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
export function getCalendarLayout(dateObject, highlight) {
if (!dateObject) dateObject = new Date();
const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
const day = dateObject.getDate();
const month = dateObject.getMonth() + 1;
const year = dateObject.getFullYear();
const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
const daysInMonth = getMonthDays(month, year);
const daysInNextMonth = getNextMonthDays(month, year);
const daysInPrevMonth = getPrevMonthDays(month, year);
// Fill
var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
var toFill, dim;
if(weekdayOfMonthFirst == 0) {
toFill = 1;
dim = daysInMonth;
}
else {
toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
dim = daysInPrevMonth;
}
var calendar = [...Array(6)].map(() => Array(7));
var i = 0, j = 0;
while (i < 6 && j < 7) {
calendar[i][j] = {
"day": toFill,
"today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
monthDiff == 0 ? 0 :
-1
))
};
// Increment
toFill++;
if (toFill > dim) { // Next month?
monthDiff++;
if (monthDiff == 0)
dim = daysInMonth;
else if (monthDiff == 1)
dim = daysInNextMonth;
toFill = 1;
}
// Next tile
j++;
if (j == 7) {
j = 0;
i++;
}
}
return calendar;
}

View file

@ -0,0 +1,217 @@
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Button, Icon, Label, Revealer, Scrollable, Slider, Stack } = Widget;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { iconExists } from '../../.miscutils/icons.js';
const AppVolume = (stream) => Box({
className: 'sidebar-volmixer-stream spacing-h-10',
children: [
Icon({
className: 'sidebar-volmixer-stream-appicon',
vpack: 'center',
tooltipText: stream.stream.name,
setup: (self) => {
self.hook(stream, (self) => {
self.icon = stream.stream.name.toLowerCase();
})
},
}),
Box({
hexpand: true,
vpack: 'center',
vertical: true,
className: 'spacing-v-5',
children: [
Label({
xalign: 0,
maxWidthChars: 1,
truncate: 'end',
label: stream.description,
className: 'txt-small',
setup: (self) => self.hook(stream, (self) => {
self.label = `${stream.stream.name}${stream.description}`
})
}),
Slider({
drawValue: false,
hpack: 'fill',
className: 'sidebar-volmixer-stream-slider',
value: stream.volume,
min: 0, max: 1,
onChange: ({ value }) => {
stream.volume = value;
},
setup: (self) => self.hook(stream, (self) => {
self.value = stream.volume;
})
}),
// Box({
// homogeneous: true,
// className: 'test',
// children: [AnimatedSlider({
// className: 'sidebar-volmixer-stream-slider',
// value: stream.volume,
// })],
// })
]
})
]
});
const AudioDevices = (input = false) => {
const dropdownShown = Variable(false);
const DeviceStream = (stream) => Button({
tooltipText: stream.description,
child: Box({
className: 'txt spacing-h-10',
children: [
iconExists(stream.iconName) ? Icon({
className: 'txt-norm symbolic-icon',
icon: stream.iconName,
}) : MaterialIcon(input ? 'mic_external_on' : 'media_output', 'norm'),
Label({
hexpand: true,
xalign: 0,
className: 'txt-small',
truncate: 'end',
maxWidthChars: 1,
label: stream.description,
}),
],
}),
onClicked: (self) => {
if (input) Audio.microphone = stream;
else Audio.speaker = stream;
dropdownShown.value = false;
},
setup: setupCursorHover,
})
const activeDevice = Button({
onClicked: () => { dropdownShown.value = !dropdownShown.value; },
child: Box({
className: 'txt spacing-h-10',
children: [
MaterialIcon(input ? 'mic_external_on' : 'media_output', 'norm'),
Label({
hexpand: true,
xalign: 0,
className: 'txt-small',
truncate: 'end',
maxWidthChars: 1,
label: `${input ? '[In]' : '[Out]'}`,
setup: (self) => self.hook(Audio, (self) => {
self.label = `${input ? '[In]' : '[Out]'} ${input ? Audio.microphone.description : Audio.speaker.description}`;
})
}),
Label({
className: `icon-material txt-norm`,
setup: (self) => self.hook(dropdownShown, (self) => {
self.label = dropdownShown.value ? 'expand_less' : 'expand_more';
})
})
],
}),
setup: setupCursorHover,
});
const deviceSelector = Revealer({
transition: 'slide_down',
revealChild: dropdownShown.bind("value"),
transitionDuration: userOptions.animations.durationSmall,
child: Box({
vertical: true,
children: [
Box({ className: 'separator-line margin-top-5 margin-bottom-5' }),
Box({
vertical: true,
className: 'spacing-v-5 margin-top-5',
attribute: {
'updateStreams': (self) => {
const streams = input ? Audio.microphones : Audio.speakers;
self.children = streams.map(stream => DeviceStream(stream));
},
},
setup: (self) => self
.hook(Audio, self.attribute.updateStreams, 'stream-added')
.hook(Audio, self.attribute.updateStreams, 'stream-removed')
,
}),
]
})
})
return Box({
hpack: 'fill',
className: 'sidebar-volmixer-deviceselector',
vertical: true,
children: [
activeDevice,
deviceSelector,
]
})
}
export default (props) => {
const emptyContent = Box({
homogeneous: true,
children: [Box({
vertical: true,
vpack: 'center',
className: 'txt spacing-v-10',
children: [
Box({
vertical: true,
className: 'spacing-v-5 txt-subtext',
children: [
MaterialIcon('brand_awareness', 'gigantic'),
Label({ label: getString('No audio source'), className: 'txt-small' }),
]
}),
]
})]
});
const appList = Scrollable({
vexpand: true,
child: Box({
attribute: {
'updateStreams': (self) => {
const streams = Audio.apps;
self.children = streams.map(stream => AppVolume(stream));
},
},
vertical: true,
className: 'spacing-v-5',
setup: (self) => self
.hook(Audio, self.attribute.updateStreams, 'stream-added')
.hook(Audio, self.attribute.updateStreams, 'stream-removed')
,
})
})
const devices = Box({
vertical: true,
className: 'spacing-v-5',
children: [
AudioDevices(false),
AudioDevices(true),
]
})
const mainContent = Stack({
children: {
'empty': emptyContent,
'list': appList,
},
setup: (self) => self.hook(Audio, (self) => {
self.shown = (Audio.apps.length > 0 ? 'list' : 'empty')
}),
})
return Box({
...props,
className: 'spacing-v-5',
vertical: true,
children: [
mainContent,
devices,
]
});
}

View file

@ -0,0 +1,160 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Button, Icon, Label, Scrollable, Slider, Stack, Overlay } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
// can't connect: sync_problem
const USE_SYMBOLIC_ICONS = true;
const BluetoothDevice = (device) => {
// console.log(device);
const deviceIcon = Icon({
className: 'sidebar-bluetooth-appicon',
vpack: 'center',
tooltipText: device.name,
setup: (self) => self.hook(device, (self) => {
self.icon = `${device.iconName}${USE_SYMBOLIC_ICONS ? '-symbolic' : ''}`;
}),
});
const deviceStatus = Box({
hexpand: true,
vpack: 'center',
vertical: true,
children: [
Label({
xalign: 0,
maxWidthChars: 1,
truncate: 'end',
label: device.name,
className: 'txt-small',
setup: (self) => self.hook(device, (self) => {
self.label = device.name;
}),
}),
Label({
xalign: 0,
maxWidthChars: 1,
truncate: 'end',
label: device.connected ? 'Connected' : (device.paired ? 'Paired' : ''),
className: 'txt-subtext',
setup: (self) => self.hook(device, (self) => {
self.label = device.connected ? getString('Connected') : (device.paired ? getString('Paired') : '');
}),
}),
]
});
const deviceConnectButton = ConfigToggle({
vpack: 'center',
expandWidget: false,
desc: 'Toggle connection',
initValue: device.connected,
onChange: (self, newValue) => {
device.setConnection(newValue);
},
extraSetup: (self) => self.hook(device, (self) => {
Utils.timeout(200, () => self.enabled.value = device.connected);
}),
})
const deviceRemoveButton = Button({
vpack: 'center',
className: 'sidebar-bluetooth-device-remove',
child: MaterialIcon('delete', 'norm'),
tooltipText: getString('Remove device'),
setup: setupCursorHover,
onClicked: () => execAsync(['bluetoothctl', 'remove', device.address]).catch(print),
});
return Box({
className: 'sidebar-bluetooth-device spacing-h-10',
children: [
deviceIcon,
deviceStatus,
Box({
className: 'spacing-h-5',
children: [
deviceConnectButton,
deviceRemoveButton,
]
})
]
})
}
export default (props) => {
const emptyContent = Box({
homogeneous: true,
children: [Box({
vertical: true,
vpack: 'center',
className: 'txt spacing-v-10',
children: [
Box({
vertical: true,
className: 'spacing-v-5 txt-subtext',
children: [
MaterialIcon('bluetooth_disabled', 'gigantic'),
Label({ label: 'No Bluetooth devices', className: 'txt-small' }),
]
}),
]
})]
});
const deviceList = Overlay({
passThrough: true,
child: Scrollable({
vexpand: true,
child: Box({
attribute: {
'updateDevices': (self) => {
const devices = Bluetooth.devices;
self.children = devices.map(d => BluetoothDevice(d));
},
},
vertical: true,
className: 'spacing-v-5 margin-bottom-15',
setup: (self) => self
.hook(Bluetooth, self.attribute.updateDevices, 'device-added')
.hook(Bluetooth, self.attribute.updateDevices, 'device-removed')
,
})
}),
overlays: [Box({
className: 'sidebar-centermodules-scrollgradient-bottom'
})]
});
const mainContent = Stack({
children: {
'empty': emptyContent,
'list': deviceList,
},
setup: (self) => self.hook(Bluetooth, (self) => {
self.shown = (Bluetooth.devices.length > 0 ? 'list' : 'empty')
}),
})
const bottomBar = Box({
homogeneous: true,
children: [Button({
hpack: 'center',
className: 'txt-small txt sidebar-centermodules-bottombar-button',
onClicked: () => {
execAsync(['bash', '-c', userOptions.apps.bluetooth]).catch(print);
closeEverything();
},
label: getString('More'),
setup: setupCursorHover,
})],
})
return Box({
...props,
className: 'spacing-v-5',
vertical: true,
children: [
mainContent,
bottomBar
]
});
}

View file

@ -0,0 +1,130 @@
const { GLib } = imports.gi;
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Button, Icon, Label, Scrollable, Slider, Stack } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { ConfigGap, ConfigSpinButton, ConfigToggle } from '../../.commonwidgets/configwidgets.js';
const HyprlandToggle = ({ icon, name, desc = null, option, enableValue = 1, disableValue = 0, extraOnChange = () => { } }) => ConfigToggle({
icon: icon,
name: name,
desc: desc,
initValue: JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"] != 0,
onChange: (self, newValue) => {
execAsync(['hyprctl', 'keyword', option, `${newValue ? enableValue : disableValue}`]).catch(print);
extraOnChange(self, newValue);
}
});
const HyprlandSpinButton = ({ icon, name, desc = null, option, ...rest }) => ConfigSpinButton({
icon: icon,
name: name,
desc: desc,
initValue: Number(JSON.parse(exec(`hyprctl getoption -j ${option}`))["int"]),
onChange: (self, newValue) => {
execAsync(['hyprctl', 'keyword', option, `${newValue}`]).catch(print);
},
...rest,
});
const Subcategory = (children) => Box({
className: 'margin-left-20',
vertical: true,
children: children,
})
export default (props) => {
const ConfigSection = ({ name, children }) => Box({
vertical: true,
className: 'spacing-v-5',
children: [
Label({
hpack: 'center',
className: 'txt txt-large margin-left-10',
label: name,
}),
Box({
className: 'margin-left-10 margin-right-10',
vertical: true,
children: children,
})
]
})
const mainContent = Scrollable({
vexpand: true,
child: Box({
vertical: true,
className: 'spacing-v-10',
children: [
ConfigSection({
name: getString('Effects'), children: [
ConfigToggle({
icon: 'border_clear',
name: getString('Transparency'),
desc: getString('[AGS]\nMake shell elements transparent\nBlur is also recommended if you enable this'),
initValue: exec(`bash -c "sed -n \'2p\' ${GLib.get_user_state_dir()}/ags/user/colormode.txt"`) == "transparent",
onChange: (self, newValue) => {
const transparency = newValue == 0 ? "opaque" : "transparent";
console.log(transparency);
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);
},
}),
HyprlandToggle({ icon: 'blur_on', name: getString('Blur'), desc: getString("[Hyprland]\nEnable blur on transparent elements\nDoesn't affect performance/power consumption unless you have transparent windows."), option: "decoration:blur:enabled" }),
Subcategory([
HyprlandToggle({ icon: 'stack_off', name: getString('X-ray'), desc: getString("[Hyprland]\nMake everything behind a window/layer except the wallpaper not rendered on its blurred surface\nRecommended to improve performance (if you don't abuse transparency/blur) "), option: "decoration:blur:xray" }),
HyprlandSpinButton({ icon: 'target', name: getString('Size'), desc: getString('[Hyprland]\nAdjust the blur radius. Generally doesn\'t affect performance\nHigher = more color spread'), option: 'decoration:blur:size', minValue: 1, maxValue: 1000 }),
HyprlandSpinButton({ icon: 'repeat', name: getString('Passes'), desc: getString('[Hyprland] Adjust the number of runs of the blur algorithm\nMore passes = more spread and power consumption\n4 is recommended\n2- would look weird and 6+ would look lame.'), option: 'decoration:blur:passes', minValue: 1, maxValue: 10 }),
]),
ConfigGap({}),
HyprlandToggle({
icon: 'animation', name: getString('Animations'), desc: getString('[Hyprland] [GTK]\nEnable animations'), option: 'animations:enabled',
extraOnChange: (self, newValue) => execAsync(['gsettings', 'set', 'org.gnome.desktop.interface', 'enable-animations', `${newValue}`])
}),
Subcategory([
ConfigSpinButton({
icon: 'clear_all',
name: getString('Choreography delay'),
desc: getString('In milliseconds, the delay between animations of a series'),
initValue: userOptions.animations.choreographyDelay,
step: 10, minValue: 0, maxValue: 1000,
onChange: (self, newValue) => {
userOptions.animations.choreographyDelay = newValue
},
})
]),
]
}),
ConfigSection({
name: getString('Developer'), children: [
HyprlandToggle({ icon: 'speed', name: getString('Show FPS'), desc: getString("[Hyprland]\nShow FPS overlay on top-left corner"), option: "debug:overlay" }),
HyprlandToggle({ icon: 'sort', name: getString('Log to stdout'), desc: getString("[Hyprland]\nPrint LOG, ERR, WARN, etc. messages to the console"), option: "debug:enable_stdout_logs" }),
HyprlandToggle({ icon: 'motion_sensor_active', name: getString('Damage tracking'), desc: getString("[Hyprland]\nEnable damage tracking\nGenerally, leave it on.\nTurn off only when a shader doesn't work"), option: "debug:damage_tracking", enableValue: 2 }),
HyprlandToggle({ icon: 'destruction', name: getString('Damage blink'), desc: getString("[Hyprland] [Epilepsy warning!]\nShow screen damage flashes"), option: "debug:damage_blink" }),
]
}),
]
})
});
const footNote = Box({
homogeneous: true,
children: [Label({
hpack: 'center',
className: 'txt txt-italic txt-subtext margin-5',
label: getString('Not all changes are saved'),
})]
})
return Box({
...props,
className: 'spacing-v-5',
vertical: true,
children: [
mainContent,
footNote,
]
});
}

View file

@ -0,0 +1,176 @@
// This file is for the notification list on the sidebar
// For the popup notifications, see onscreendisplay.js
// The actual widget for each single notification is in ags/modules/.commonwidgets/notification.js
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
const { Box, Button, Label, Revealer, Scrollable, Stack } = Widget;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import Notification from '../../.commonwidgets/notification.js';
import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
export default (props) => {
const notifEmptyContent = Box({
homogeneous: true,
children: [Box({
vertical: true,
vpack: 'center',
className: 'txt spacing-v-10',
children: [
Box({
vertical: true,
className: 'spacing-v-5 txt-subtext',
children: [
MaterialIcon('notifications_active', 'gigantic'),
Label({ label: getString('No notifications'), className: 'txt-small' }),
]
}),
]
})]
});
const notificationList = Box({
vertical: true,
vpack: 'start',
className: 'spacing-v-5-revealer',
setup: (self) => self
.hook(Notifications, (box, id) => {
if (box.get_children().length == 0) { // On init there's no notif, or 1st notif
Notifications.notifications
.forEach(n => {
box.pack_end(Notification({
notifObject: n,
isPopup: false,
}), false, false, 0)
});
box.show_all();
return;
}
// 2nd or later notif
const notif = Notifications.getNotification(id);
const NewNotif = Notification({
notifObject: notif,
isPopup: false,
});
if (NewNotif) {
box.pack_end(NewNotif, false, false, 0);
box.show_all();
}
}, 'notified')
.hook(Notifications, (box, id) => {
if (!id) return;
for (const ch of box.children) {
if (ch._id === id) {
ch.attribute.destroyWithAnims();
}
}
}, 'closed')
,
});
const ListActionButton = (icon, name, action) => Button({
className: 'sidebar-centermodules-bottombar-button',
onClicked: action,
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
MaterialIcon(icon, 'norm'),
Label({
className: 'txt-small',
label: name,
})
]
}),
setup: setupCursorHover,
});
const silenceButton = ListActionButton('notifications_paused', getString('Silence'), (self) => {
Notifications.dnd = !Notifications.dnd;
self.toggleClassName('notif-listaction-btn-enabled', Notifications.dnd);
});
// const silenceToggle = ConfigToggle({
// expandWidget: false,
// icon: 'do_not_disturb_on',
// name: 'Do Not Disturb',
// initValue: false,
// onChange: (self, newValue) => {
// Notifications.dnd = newValue;
// },
// })
const clearButton = Revealer({
transition: 'slide_right',
transitionDuration: userOptions.animations.durationSmall,
setup: (self) => self.hook(Notifications, (self) => {
self.revealChild = Notifications.notifications.length > 0;
}),
child: ListActionButton('clear_all', getString('Clear'), () => {
Notifications.clear();
const kids = notificationList.get_children();
for (let i = 0; i < kids.length; i++) {
const kid = kids[i];
Utils.timeout(userOptions.animations.choreographyDelay * i, () => kid.attribute.destroyWithAnims());
}
})
})
const notifCount = Label({
attribute: {
updateCount: (self) => {
const count = Notifications.notifications.length;
if (count > 0) self.label = `${count} ${getString("notifications")}`;
else self.label = '';
},
},
hexpand: true,
xalign: 0,
className: 'txt-small margin-left-10',
label: `${Notifications.notifications.length}`,
setup: (self) => self
.hook(Notifications, (box, id) => self.attribute.updateCount(self), 'notified')
.hook(Notifications, (box, id) => self.attribute.updateCount(self), 'dismissed')
.hook(Notifications, (box, id) => self.attribute.updateCount(self), 'closed')
,
});
const listTitle = Box({
vpack: 'start',
className: 'txt spacing-h-5',
children: [
notifCount,
silenceButton,
// silenceToggle,
// Box({ hexpand: true }),
clearButton,
]
});
const notifList = Scrollable({
hexpand: true,
hscroll: 'never',
vscroll: 'automatic',
child: Box({
vexpand: true,
homogeneous: true,
children: [notificationList],
}),
setup: (self) => {
const vScrollbar = self.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
}
});
const listContents = Stack({
transition: 'crossfade',
transitionDuration: userOptions.animations.durationLarge,
children: {
'empty': notifEmptyContent,
'list': notifList,
},
setup: (self) => self.hook(Notifications, (self) => {
self.shown = (Notifications.notifications.length > 0 ? 'list' : 'empty')
}),
});
return Box({
...props,
className: 'spacing-v-5',
vertical: true,
children: [
listContents,
listTitle,
]
});
}

View file

@ -0,0 +1,216 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Network from "resource:///com/github/Aylur/ags/service/network.js";
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Button, Entry, Icon, Label, Revealer, Scrollable, Slider, Stack, Overlay } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../../.commonwidgets/materialicon.js';
import { setupCursorHover } from '../../.widgetutils/cursorhover.js';
import { ConfigToggle } from '../../.commonwidgets/configwidgets.js';
const MATERIAL_SYMBOL_SIGNAL_STRENGTH = {
'network-wireless-signal-excellent-symbolic': "signal_wifi_4_bar",
'network-wireless-signal-good-symbolic': "network_wifi_3_bar",
'network-wireless-signal-ok-symbolic': "network_wifi_2_bar",
'network-wireless-signal-weak-symbolic': "network_wifi_1_bar",
'network-wireless-signal-none-symbolic': "signal_wifi_0_bar",
}
let connectAttempt = '';
const WifiNetwork = (accessPoint) => {
const networkStrength = MaterialIcon(MATERIAL_SYMBOL_SIGNAL_STRENGTH[accessPoint.iconName], 'hugerass')
const networkName = Box({
vertical: true,
children: [
Label({
hpack: 'start',
label: accessPoint.ssid
}),
accessPoint.active ? Label({
hpack: 'start',
className: 'txt-smaller txt-subtext',
label: getString("Selected"),
}) : null,
]
});
return Button({
onClicked: accessPoint.active ? () => { } : () => execAsync(`nmcli device wifi connect ${accessPoint.bssid}`)
// .catch(e => {
// Utils.notify({
// summary: "Network",
// body: e,
// actions: { "Open network manager": () => execAsync("nm-connection-editor").catch(print) }
// });
// })
.catch(print),
child: Box({
className: 'sidebar-wifinetworks-network spacing-h-10',
children: [
networkStrength,
networkName,
Box({ hexpand: true }),
accessPoint.active ? MaterialIcon('check', 'large') : null,
],
}),
setup: accessPoint.active ? () => { } : setupCursorHover,
})
}
const CurrentNetwork = () => {
let authLock = false;
// console.log(Network.wifi);
const bottomSeparator = Box({
className: 'separator-line',
});
const networkName = Box({
vertical: true,
hexpand: true,
children: [
Label({
hpack: 'start',
className: 'txt-smaller txt-subtext',
label: getString("Current network"),
}),
Label({
hpack: 'start',
label: Network.wifi?.ssid,
setup: (self) => self.hook(Network, (self) => {
if (authLock) return;
self.label = Network.wifi?.ssid;
}),
}),
]
});
const networkStatus = Box({
children: [Label({
vpack: 'center',
className: 'txt-subtext',
setup: (self) => self.hook(Network, (self) => {
if (authLock) return;
self.label = Network.wifi.state;
}),
})]
})
const networkAuth = Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: Box({
className: 'margin-top-10 spacing-v-5',
vertical: true,
children: [
Label({
className: 'margin-left-5',
hpack: 'start',
label: getString("Authentication"),
}),
Entry({
className: 'sidebar-wifinetworks-auth-entry',
visibility: false, // Password dots
onAccept: (self) => {
authLock = false;
networkAuth.revealChild = false;
execAsync(`nmcli device wifi connect '${connectAttempt}' password '${self.text}'`)
.catch(print);
}
})
]
}),
setup: (self) => self.hook(Network, (self) => {
if (Network.wifi.state == 'failed' || Network.wifi.state == 'need_auth') {
authLock = true;
connectAttempt = Network.wifi.ssid;
self.revealChild = true;
}
}),
});
const actualContent = Box({
vertical: true,
className: 'spacing-v-10',
children: [
Box({
className: 'sidebar-wifinetworks-network',
vertical: true,
children: [
Box({
className: 'spacing-h-10',
children: [
MaterialIcon('language', 'hugerass'),
networkName,
networkStatus,
]
}),
networkAuth,
]
}),
bottomSeparator,
]
});
return Box({
vertical: true,
children: [Revealer({
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
revealChild: Network.wifi,
child: actualContent,
})]
})
}
export default (props) => {
const networkList = Box({
vertical: true,
className: 'spacing-v-10',
children: [Overlay({
passThrough: true,
child: Scrollable({
vexpand: true,
child: Box({
attribute: {
'updateNetworks': (self) => {
const accessPoints = Network.wifi?.access_points || [];
self.children = Object.values(accessPoints.reduce((a, accessPoint) => {
// Only keep max strength networks by ssid
if (!a[accessPoint.ssid] || a[accessPoint.ssid].strength < accessPoint.strength) {
a[accessPoint.ssid] = accessPoint;
a[accessPoint.ssid].active |= accessPoint.active;
}
return a;
}, {})).map(n => WifiNetwork(n));
},
},
vertical: true,
className: 'spacing-v-5 margin-bottom-15',
setup: (self) => self.hook(Network, self.attribute.updateNetworks),
})
}),
overlays: [Box({
className: 'sidebar-centermodules-scrollgradient-bottom'
})]
})]
});
const bottomBar = Box({
homogeneous: true,
children: [Button({
hpack: 'center',
className: 'txt-small txt sidebar-centermodules-bottombar-button',
onClicked: () => {
execAsync(['bash', '-c', userOptions.apps.network]).catch(print);
closeEverything();
},
label: getString('More'),
setup: setupCursorHover,
})],
})
return Box({
...props,
className: 'spacing-v-10',
vertical: true,
children: [
CurrentNetwork(),
networkList,
bottomBar,
]
});
}

View file

@ -0,0 +1,18 @@
import PopupWindow from '../.widgethacks/popupwindow.js';
import SidebarRight from "./sideright.js";
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box } = Widget;
import clickCloseRegion from '../.commonwidgets/clickcloseregion.js';
export default () => PopupWindow({
keymode: 'on-demand',
anchor: ['right', 'top', 'bottom'],
name: 'sideright',
layer: 'top',
child: Box({
children: [
clickCloseRegion({ name: 'sideright', multimonitor: false, fillMonitor: 'horizontal' }),
SidebarRight(),
]
})
});

View file

@ -0,0 +1,267 @@
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 * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
const { execAsync, exec } = Utils;
import { BluetoothIndicator, NetworkIndicator } from '../.commonwidgets/statusicons.js';
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { sidebarOptionsStack } from './sideright.js';
export const ToggleIconWifi = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Wifi | Right-click to configure'),
onClicked: () => Network.toggleWifi(),
onSecondaryClickRelease: () => {
execAsync(['bash', '-c', `${userOptions.apps.network}`]).catch(print);
closeEverything();
},
child: NetworkIndicator(),
setup: (self) => {
setupCursorHover(self);
self.hook(Network, button => {
button.toggleClassName('sidebar-button-active', [Network.wifi?.internet, Network.wired?.internet].includes('connected'))
button.tooltipText = (`${Network.wifi?.ssid} | ${getString("Right-click to configure")}` || getString('Unknown'));
});
},
...props,
});
export const ToggleIconBluetooth = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Bluetooth | Right-click to configure'),
onClicked: () => {
const status = Bluetooth?.enabled;
if (status)
exec('rfkill block bluetooth');
else
exec('rfkill unblock bluetooth');
},
onSecondaryClickRelease: () => {
execAsync(['bash', '-c', `${userOptions.apps.bluetooth}`]).catch(print);
closeEverything();
},
child: BluetoothIndicator(),
setup: (self) => {
setupCursorHover(self);
self.hook(Bluetooth, button => {
button.toggleClassName('sidebar-button-active', Bluetooth?.enabled)
});
},
...props,
});
export const HyprToggleIcon = async (icon, name, hyprlandConfigValue, props = {}) => {
try {
return Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: `${name}`,
onClicked: (button) => {
// Set the value to 1 - value
Utils.execAsync(`hyprctl -j getoption ${hyprlandConfigValue}`).then((result) => {
const currentOption = JSON.parse(result).int;
execAsync(['bash', '-c', `hyprctl keyword ${hyprlandConfigValue} ${1 - currentOption} &`]).catch(print);
button.toggleClassName('sidebar-button-active', currentOption == 0);
}).catch(print);
},
child: MaterialIcon(icon, 'norm', { hpack: 'center' }),
setup: button => {
button.toggleClassName('sidebar-button-active', JSON.parse(Utils.exec(`hyprctl -j getoption ${hyprlandConfigValue}`)).int == 1);
setupCursorHover(button);
},
...props,
})
} catch {
return null;
}
}
export const ModuleNightLight = async (props = {}) => {
if (!exec(`bash -c 'command -v gammastep'`)) return null;
return Widget.Button({
attribute: {
enabled: false,
},
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Night Light'),
onClicked: (self) => {
self.attribute.enabled = !self.attribute.enabled;
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
if (self.attribute.enabled) Utils.execAsync('gammastep').catch(print)
else Utils.execAsync('pkill gammastep')
.then(() => {
// disable the button until fully terminated to avoid race
self.sensitive = false;
const source = setInterval(() => {
Utils.execAsync('pkill -0 gammastep')
.catch(() => {
self.sensitive = true;
source.destroy();
});
}, 500);
})
.catch(print);
},
child: MaterialIcon('nightlight', 'norm'),
setup: (self) => {
setupCursorHover(self);
self.attribute.enabled = !!exec('pidof gammastep');
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
},
...props,
});
}
export const ModuleCloudflareWarp = async (props = {}) => {
if (!exec(`bash -c 'command -v warp-cli'`)) return null;
return Widget.Button({
attribute: {
enabled: false,
},
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Cloudflare WARP'),
onClicked: (self) => {
self.attribute.enabled = !self.attribute.enabled;
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
if (self.attribute.enabled) Utils.execAsync('warp-cli connect').catch(print)
else Utils.execAsync('warp-cli disconnect').catch(print);
},
child: Widget.Icon({
icon: 'cloudflare-dns-symbolic',
className: 'txt-norm',
}),
setup: (self) => {
setupCursorHover(self);
self.attribute.enabled = !exec(`bash -c 'warp-cli status | grep Disconnected'`);
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
},
...props,
});
}
export const ModuleInvertColors = async (props = {}) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
return Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Color inversion'),
onClicked: (button) => {
// const shaderPath = JSON.parse(exec('hyprctl -j getoption decoration:screen_shader')).str;
Hyprland.messageAsync('j/getoption decoration:screen_shader')
.then((output) => {
const shaderPath = JSON.parse(output)["str"].trim();
if (shaderPath != "[[EMPTY]]" && shaderPath != "") {
execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader '[[EMPTY]]'`]).catch(print);
button.toggleClassName('sidebar-button-active', false);
}
else {
Hyprland.messageAsync(`j/keyword decoration:screen_shader ${GLib.get_user_config_dir()}/hypr/shaders/invert.frag`)
.catch(print);
button.toggleClassName('sidebar-button-active', true);
}
})
},
child: MaterialIcon('invert_colors', 'norm'),
setup: setupCursorHover,
...props,
})
} catch {
return null;
};
}
export const ModuleRawInput = async (props = {}) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
return Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Raw input',
onClicked: (button) => {
Hyprland.messageAsync('j/getoption input:accel_profile')
.then((output) => {
const value = JSON.parse(output)["str"].trim();
if (value != "[[EMPTY]]" && value != "") {
execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print);
button.toggleClassName('sidebar-button-active', false);
}
else {
Hyprland.messageAsync(`j/keyword input:accel_profile flat`)
.catch(print);
button.toggleClassName('sidebar-button-active', true);
}
})
},
child: MaterialIcon('mouse', 'norm'),
setup: setupCursorHover,
...props,
})
} catch {
return null;
};
}
export const ModuleIdleInhibitor = (props = {}) => Widget.Button({ // TODO: Make this work
attribute: {
enabled: false,
},
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Keep system awake'),
onClicked: (self) => {
self.attribute.enabled = !self.attribute.enabled;
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
if (self.attribute.enabled) Utils.execAsync(['bash', '-c', `pidof wayland-idle-inhibitor.py || ${App.configDir}/scripts/wayland-idle-inhibitor.py`]).catch(print)
else Utils.execAsync('pkill -f wayland-idle-inhibitor.py').catch(print);
},
child: MaterialIcon('coffee', 'norm'),
setup: (self) => {
setupCursorHover(self);
self.attribute.enabled = !!exec('pidof wayland-idle-inhibitor.py');
self.toggleClassName('sidebar-button-active', self.attribute.enabled);
},
...props,
});
export const ModuleReloadIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Reload Environment config'),
onClicked: () => {
execAsync(['bash', '-c', 'hyprctl reload || swaymsg reload &']);
App.closeWindow('sideright');
},
child: MaterialIcon('refresh', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
export const ModuleSettingsIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Open Settings'),
onClicked: () => {
execAsync(['bash', '-c', `${userOptions.apps.settings}`, '&']);
App.closeWindow('sideright');
},
child: MaterialIcon('settings', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
export const ModulePowerIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: getString('Session'),
onClicked: () => {
closeEverything();
Utils.timeout(1, () => openWindowOnAllMonitors('session'));
},
child: MaterialIcon('power_settings_new', 'norm'),
setup: button => {
setupCursorHover(button);
}
})

View file

@ -0,0 +1,188 @@
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 } = Widget;
import {
ToggleIconBluetooth,
ToggleIconWifi,
HyprToggleIcon,
ModuleNightLight,
ModuleInvertColors,
ModuleIdleInhibitor,
ModuleReloadIcon,
ModuleSettingsIcon,
ModulePowerIcon,
ModuleRawInput,
ModuleCloudflareWarp
} from "./quicktoggles.js";
import ModuleNotificationList from "./centermodules/notificationlist.js";
import ModuleAudioControls from "./centermodules/audiocontrols.js";
import ModuleWifiNetworks from "./centermodules/wifinetworks.js";
import ModuleBluetooth from "./centermodules/bluetooth.js";
import ModuleConfigure from "./centermodules/configure.js";
import { ModuleCalendar } from "./calendar.js";
import { getDistroIcon } from '../.miscutils/system.js';
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { ExpandingIconTabContainer } from '../.commonwidgets/tabcontainer.js';
import { checkKeybind } from '../.widgetutils/keybind.js';
const centerWidgets = [
{
name: getString('Notifications'),
materialIcon: 'notifications',
contentWidget: ModuleNotificationList,
},
{
name: getString('Audio controls'),
materialIcon: 'volume_up',
contentWidget: ModuleAudioControls,
},
{
name: getString('Bluetooth'),
materialIcon: 'bluetooth',
contentWidget: ModuleBluetooth,
},
{
name: getString('Wifi networks'),
materialIcon: 'wifi',
contentWidget: ModuleWifiNetworks,
onFocus: () => execAsync('nmcli dev wifi list').catch(print),
},
{
name: getString('Live config'),
materialIcon: 'tune',
contentWidget: ModuleConfigure,
},
];
const timeRow = Box({
className: 'spacing-h-10 sidebar-group-invisible-morehorizpad',
children: [
Widget.Icon({
icon: getDistroIcon(),
className: 'txt txt-larger',
}),
Widget.Label({
hpack: 'center',
className: 'txt-small txt',
setup: (self) => {
const getUptime = async () => {
try {
await execAsync(['bash', '-c', 'uptime -p']);
return execAsync(['bash', '-c', `uptime -p | sed -e 's/...//;s/ day\\| days/d/;s/ hour\\| hours/h/;s/ minute\\| minutes/m/;s/,[^,]*//2'`]);
} catch {
return execAsync(['bash', '-c', 'uptime']).then(output => {
const uptimeRegex = /up\s+((\d+)\s+days?,\s+)?((\d+):(\d+)),/;
const matches = uptimeRegex.exec(output);
if (matches) {
const days = matches[2] ? parseInt(matches[2]) : 0;
const hours = matches[4] ? parseInt(matches[4]) : 0;
const minutes = matches[5] ? parseInt(matches[5]) : 0;
let formattedUptime = '';
if (days > 0) {
formattedUptime += `${days} d `;
}
if (hours > 0) {
formattedUptime += `${hours} h `;
}
formattedUptime += `${minutes} m`;
return formattedUptime;
} else {
throw new Error('Failed to parse uptime output');
}
});
}
};
self.poll(5000, label => {
getUptime().then(upTimeString => {
label.label = `${getString("Uptime:"
)} ${upTimeString}`;
}).catch(err => {
console.error(`Failed to fetch uptime: ${err}`);
});
});
},
}),
Widget.Box({ hexpand: true }),
ModuleReloadIcon({ hpack: 'end' }),
// ModuleSettingsIcon({ hpack: 'end' }), // Button does work, gnome-control-center is kinda broken
ModulePowerIcon({ hpack: 'end' }),
]
});
const togglesBox = Widget.Box({
hpack: 'center',
className: 'sidebar-togglesbox spacing-h-5',
children: [
ToggleIconWifi(),
ToggleIconBluetooth(),
// await ModuleRawInput(),
// await HyprToggleIcon('touchpad_mouse', 'No touchpad while typing', 'input:touchpad:disable_while_typing', {}),
await ModuleNightLight(),
await ModuleInvertColors(),
ModuleIdleInhibitor(),
await ModuleCloudflareWarp(),
]
})
export const sidebarOptionsStack = ExpandingIconTabContainer({
tabsHpack: 'center',
tabSwitcherClassName: 'sidebar-icontabswitcher',
icons: centerWidgets.map((api) => api.materialIcon),
names: centerWidgets.map((api) => api.name),
children: centerWidgets.map((api) => api.contentWidget()),
onChange: (self, id) => {
self.shown = centerWidgets[id].name;
if (centerWidgets[id].onFocus) centerWidgets[id].onFocus();
}
});
export default () => Box({
vexpand: true,
hexpand: true,
css: 'min-width: 2px;',
children: [
EventBox({
onPrimaryClick: () => App.closeWindow('sideright'),
onSecondaryClick: () => App.closeWindow('sideright'),
onMiddleClick: () => App.closeWindow('sideright'),
}),
Box({
vertical: true,
vexpand: true,
className: 'sidebar-right spacing-v-15',
children: [
Box({
vertical: true,
className: 'spacing-v-5',
children: [
timeRow,
togglesBox,
]
}),
Box({
className: 'sidebar-group',
children: [
sidebarOptionsStack,
],
}),
ModuleCalendar(),
]
}),
],
setup: (self) => self
.on('key-press-event', (widget, event) => { // Handle keybinds
if (checkKeybind(event, userOptions.keybinds.sidebar.options.nextTab)) {
sidebarOptionsStack.nextTab();
}
else if (checkKeybind(event, userOptions.keybinds.sidebar.options.prevTab)) {
sidebarOptionsStack.prevTab();
}
})
,
});

View file

@ -0,0 +1,224 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
const { Box, Button, Label, Revealer } = Widget;
import { MaterialIcon } from '../.commonwidgets/materialicon.js';
import { TabContainer } from '../.commonwidgets/tabcontainer.js';
import Todo from "../../services/todo.js";
import { setupCursorHover } from '../.widgetutils/cursorhover.js';
const TodoListItem = (task, id, isDone, isEven = false) => {
const taskName = Widget.Label({
hexpand: true,
xalign: 0,
wrap: true,
className: 'txt txt-small sidebar-todo-txt',
label: task.content,
selectable: true,
});
const actions = Box({
hpack: 'end',
className: 'spacing-h-5 sidebar-todo-actions',
children: [
Widget.Button({ // Check/Uncheck
vpack: 'center',
className: 'txt sidebar-todo-item-action',
child: MaterialIcon(`${isDone ? 'remove_done' : 'check'}`, 'norm', { vpack: 'center' }),
onClicked: (self) => {
const contentWidth = todoContent.get_allocated_width();
crosser.toggleClassName('sidebar-todo-crosser-crossed', true);
crosser.css = `margin-left: -${contentWidth}px;`;
Utils.timeout(200, () => {
widgetRevealer.revealChild = false;
})
Utils.timeout(350, () => {
if (isDone)
Todo.uncheck(id);
else
Todo.check(id);
})
},
setup: setupCursorHover,
}),
Widget.Button({ // Remove
vpack: 'center',
className: 'txt sidebar-todo-item-action',
child: MaterialIcon('delete_forever', 'norm', { vpack: 'center' }),
onClicked: () => {
const contentWidth = todoContent.get_allocated_width();
crosser.toggleClassName('sidebar-todo-crosser-removed', true);
crosser.css = `margin-left: -${contentWidth}px;`;
Utils.timeout(200, () => {
widgetRevealer.revealChild = false;
})
Utils.timeout(350, () => {
Todo.remove(id);
})
},
setup: setupCursorHover,
}),
]
})
const crosser = Widget.Box({
className: 'sidebar-todo-crosser',
});
const todoContent = Widget.Box({
className: 'sidebar-todo-item spacing-h-5',
children: [
Widget.Box({
vertical: true,
children: [
taskName,
actions,
]
}),
crosser,
]
});
const widgetRevealer = Widget.Revealer({
revealChild: true,
transition: 'slide_down',
transitionDuration: userOptions.animations.durationLarge,
child: todoContent,
})
return Box({
homogeneous: true,
children: [widgetRevealer]
});
}
const todoItems = (isDone) => Widget.Scrollable({
hscroll: 'never',
vscroll: 'automatic',
child: Widget.Box({
vertical: true,
className: 'spacing-v-5',
setup: (self) => self
.hook(Todo, (self) => {
self.children = Todo.todo_json.map((task, i) => {
if (task.done != isDone) return null;
return TodoListItem(task, i, isDone);
})
if (self.children.length == 0) {
self.homogeneous = true;
self.children = [
Widget.Box({
hexpand: true,
vertical: true,
vpack: 'center',
className: 'txt txt-subtext',
children: [
MaterialIcon(`${isDone ? 'checklist' : 'check_circle'}`, 'gigantic'),
Label({ label: `${isDone ? getString('Finished tasks will go here') : getString('Nothing here!')}` })
]
})
]
}
else self.homogeneous = false;
}, 'updated')
,
}),
setup: (listContents) => {
const vScrollbar = listContents.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
}
});
const UndoneTodoList = () => {
const newTaskButton = Revealer({
transition: 'slide_left',
transitionDuration: userOptions.animations.durationLarge,
revealChild: true,
child: Button({
className: 'txt-small sidebar-todo-new',
halign: 'end',
vpack: 'center',
label: getString('+ New task'),
setup: setupCursorHover,
onClicked: (self) => {
newTaskButton.revealChild = false;
newTaskEntryRevealer.revealChild = true;
confirmAddTask.revealChild = true;
cancelAddTask.revealChild = true;
newTaskEntry.grab_focus();
}
})
});
const cancelAddTask = Revealer({
transition: 'slide_right',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: Button({
className: 'txt-norm icon-material sidebar-todo-add',
halign: 'end',
vpack: 'center',
label: 'close',
setup: setupCursorHover,
onClicked: (self) => {
newTaskEntryRevealer.revealChild = false;
confirmAddTask.revealChild = false;
cancelAddTask.revealChild = false;
newTaskButton.revealChild = true;
newTaskEntry.text = '';
}
})
});
const newTaskEntry = Widget.Entry({
// hexpand: true,
vpack: 'center',
className: 'txt-small sidebar-todo-entry',
placeholderText: getString('Add a task...'),
onAccept: ({ text }) => {
if (text == '') return;
Todo.add(text)
newTaskEntry.text = '';
},
onChange: ({ text }) => confirmAddTask.child.toggleClassName('sidebar-todo-add-available', text != ''),
});
const newTaskEntryRevealer = Revealer({
transition: 'slide_right',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: newTaskEntry,
});
const confirmAddTask = Revealer({
transition: 'slide_right',
transitionDuration: userOptions.animations.durationLarge,
revealChild: false,
child: Button({
className: 'txt-norm icon-material sidebar-todo-add',
halign: 'end',
vpack: 'center',
label: 'arrow_upward',
setup: setupCursorHover,
onClicked: (self) => {
if (newTaskEntry.text == '') return;
Todo.add(newTaskEntry.text);
newTaskEntry.text = '';
}
})
});
return Box({ // The list, with a New button
vertical: true,
className: 'spacing-v-5',
setup: (box) => {
box.pack_start(todoItems(false), true, true, 0);
box.pack_start(Box({
setup: (self) => {
self.pack_start(cancelAddTask, false, false, 0);
self.pack_start(newTaskEntryRevealer, true, true, 0);
self.pack_start(confirmAddTask, false, false, 0);
self.pack_start(newTaskButton, false, false, 0);
}
}), false, false, 0);
},
});
}
export const TodoWidget = () => TabContainer({
icons: ['format_list_bulleted', 'task_alt'],
names: [getString('Unfinished'), getString('Done')],
children: [
UndoneTodoList(),
todoItems(true),
]
})