Initial commit

This commit is contained in:
end-4
2023-12-25 18:16:14 +07:00
commit 16b0d77075
174 changed files with 18893 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
import { App, Service, Utils, Widget } from '../../imports.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const { CONFIG_DIR, exec, execAsync } = Utils;
import { setupCursorHover } from "../../lib/cursorhover.js";
import { RoundedCorner } from "../../lib/roundedcorner.js";
import Brightness from '../../services/brightness.js';
import Indicator from '../../services/indicator.js';
// Removes everything after the last
// em dash, en dash, minus, vertical bar, or middle dot (note: maybe add open parenthesis?)
// For example:
// • Discord | #ricing-theming | r/unixporn — Mozilla Firefox --> • Discord | #ricing-theming
// GJS Error · Issue #112 · Aylur/ags — Mozilla Firefox --> GJS Error · Issue #112
function truncateTitle(str) {
let lastDash = -1;
let found = -1; // 0: em dash, 1: en dash, 2: minus, 3: vertical bar, 4: middle dot
for (let i = str.length - 1; i >= 0; i--) {
if (str[i] === '—') {
found = 0;
lastDash = i;
}
else if (str[i] === '' && found < 1) {
found = 1;
lastDash = i;
}
else if (str[i] === '-' && found < 2) {
found = 2;
lastDash = i;
}
else if (str[i] === '|' && found < 3) {
found = 3;
lastDash = i;
}
else if (str[i] === '·' && found < 4) {
found = 4;
lastDash = i;
}
}
if (lastDash === -1) return str;
return str.substring(0, lastDash);
}
export const ModuleLeftSpace = () => Widget.EventBox({
onScrollUp: () => {
Indicator.popup(1); // Since the brightness and speaker are both on the same window
Brightness.screen_value += 0.05;
},
onScrollDown: () => {
Indicator.popup(1); // Since the brightness and speaker are both on the same window
Brightness.screen_value -= 0.05;
},
onPrimaryClick: () => {
App.toggleWindow('sideleft');
},
child: Widget.Box({
homogeneous: false,
children: [
RoundedCorner('topleft', { className: 'corner-black' }),
Widget.Overlay({
overlays: [
Widget.Box({ hexpand: true }),
Widget.Box({
className: 'bar-sidemodule', hexpand: true,
children: [Widget.Box({
vertical: true,
className: 'bar-space-button',
children: [
Widget.Scrollable({
hexpand: true, vexpand: true,
hscroll: 'automatic', vscroll: 'never',
child: Widget.Box({
vertical: true,
children: [
Widget.Label({
xalign: 0,
className: 'txt-smaller bar-topdesc txt',
connections: [[Hyprland.active.client, label => { // Hyprland.active.client
label.label = Hyprland.active.client._class.length === 0 ? 'Desktop' : Hyprland.active.client._class;
}]],
}),
Widget.Label({
xalign: 0,
className: 'txt txt-smallie',
connections: [
[Hyprland.active.client, label => { // Hyprland.active.client
label.label = Hyprland.active.client._title.length === 0 ? `Workspace ${Hyprland.active.workspace.id}` : truncateTitle(Hyprland.active.client._title);
}]
],
})
]
})
})
]
})]
}),
]
})
]
})
});
+53
View File
@@ -0,0 +1,53 @@
const { Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
import { ModuleLeftSpace } from "./leftspace.js";
import { ModuleMusic } from "./music.js";
import { ModuleRightSpace } from "./rightspace.js";
import { ModuleSystem } from "./system.js";
import { ModuleWorkspaces } from "./workspaces.js";
import { RoundedCorner } from "../../lib/roundedcorner.js";
const left = Widget.Box({
className: 'bar-sidemodule',
children: [ModuleMusic()],
});
const center = Widget.Box({
children: [
RoundedCorner('topright', { className: 'corner-bar-group' }),
ModuleWorkspaces(),
RoundedCorner('topleft', { className: 'corner-bar-group' }),
],
});
const right = Widget.Box({
className: 'bar-sidemodule',
children: [ModuleSystem()],
});
export default () => Widget.Window({
name: 'bar',
anchor: ['top', 'left', 'right'],
exclusivity: 'exclusive',
visible: true,
child: Widget.CenterBox({
className: 'bar-bg',
startWidget: ModuleLeftSpace(),
centerWidget: Widget.Box({
className: 'spacing-h--20',
children: [
left,
center,
right,
]
}),
endWidget: ModuleRightSpace(),
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);
}
}),
});
+82
View File
@@ -0,0 +1,82 @@
import { Service, Utils, Widget } from '../../imports.js';
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
const { execAsync, exec } = Utils;
import { AnimatedCircProg } from "../../lib/animatedcircularprogress.js";
import { showMusicControls } from '../../variables.js';
const TrackProgress = () => {
const _updateProgress = (circprog) => {
const mpris = Mpris.getPlayer('');
if (!mpris) return;
// Set circular progress (font size cuz that's how this hacky circprog works)
circprog.css = `font-size: ${Math.max(mpris.position / mpris.length * 100, 0)}px;`
}
return AnimatedCircProg({
className: 'bar-music-circprog',
vpack: 'center',
connections: [ // Update on change/once every 3 seconds
[Mpris, _updateProgress],
[3000, _updateProgress]
]
})
}
export const ModuleMusic = () => Widget.EventBox({
onScrollUp: () => execAsync('hyprctl dispatch workspace -1'),
onScrollDown: () => execAsync('hyprctl dispatch workspace +1'),
onPrimaryClickRelease: () => showMusicControls.setValue(!showMusicControls.value),
onSecondaryClickRelease: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']),
onMiddleClickRelease: () => Mpris.getPlayer('')?.playPause(),
child: Widget.Box({
className: 'bar-group-margin bar-sides',
children: [
Widget.Box({
className: 'bar-group bar-group-standalone bar-group-pad-music spacing-h-10',
children: [
Widget.Box({ // Wrap a box cuz overlay can't have margins itself
homogeneous: true,
children: [Widget.Overlay({
child: Widget.Box({
vpack: 'center',
className: 'bar-music-playstate',
homogeneous: true,
children: [Widget.Label({
vpack: 'center',
className: 'bar-music-playstate-txt',
justification: 'center',
connections: [[Mpris, label => {
const mpris = Mpris.getPlayer('');
label.label = `${mpris !== null && mpris.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
}]],
})],
connections: [[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(),
]
})]
}),
Widget.Scrollable({
hexpand: true,
child: Widget.Label({
className: 'txt txt-smallie',
connections: [[Mpris, label => {
const mpris = Mpris.getPlayer('');
if (mpris)
label.label = `${mpris.trackTitle}${mpris.trackArtists.join(', ')}`;
else
label.label = 'No media';
}]],
})
})
]
})
]
})
});
+59
View File
@@ -0,0 +1,59 @@
import { App, Utils, Widget } from '../../imports.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
const { execAsync } = Utils;
import Indicator from '../../services/indicator.js';
import { StatusIcons } from "../../lib/statusicons.js";
import { RoundedCorner } from "../../lib/roundedcorner.js";
import { Tray } from "./tray.js";
export const ModuleRightSpace = () => {
const barTray = Tray();
const barStatusIcons = StatusIcons({
className: 'bar-statusicons',
connections: [[App, (self, currentName, visible) => {
if (currentName === 'sideright') {
self.toggleClassName('bar-statusicons-active', visible);
}
}]],
});
return Widget.EventBox({
onScrollUp: () => {
if (!Audio.speaker) return;
Audio.speaker.volume += 0.03;
Indicator.popup(1);
},
onScrollDown: () => {
if (!Audio.speaker) return;
Audio.speaker.volume -= 0.03;
Indicator.popup(1);
},
// onHover: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', true) },
// onHoverLost: () => { barStatusIcons.toggleClassName('bar-statusicons-hover', false) },
onPrimaryClick: () => App.toggleWindow('sideright'),
onSecondaryClickRelease: () => execAsync(['bash', '-c', 'playerctl next || playerctl position `bc <<< "100 * $(playerctl metadata mpris:length) / 1000000 / 100"` &']),
onMiddleClickRelease: () => Mpris.getPlayer('')?.playPause(),
child: Widget.Box({
homogeneous: false,
children: [
Widget.Box({
hexpand: true,
className: 'spacing-h-5 txt',
children: [
Widget.Box({
hexpand: true,
className: 'spacing-h-15 txt',
children: [
Widget.Box({ hexpand: true, }),
barTray,
barStatusIcons,
],
}),
]
}),
RoundedCorner('topright', { className: 'corner-black' })
]
})
});
}
+149
View File
@@ -0,0 +1,149 @@
// This is for the right pill of the bar.
// For the cool memory indicator on the sidebar, see sysinfo.js
import { Service, Utils, Widget } from '../../imports.js';
const { exec, execAsync } = Utils;
const { GLib } = imports.gi;
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
import { MaterialIcon } from '../../lib/materialicon.js';
const BATTERY_LOW = 20;
const BarClock = () => Widget.Box({
vpack: 'center',
className: 'spacing-h-5',
children: [
Widget.Label({
className: 'bar-clock',
connections: [[5000, label => {
label.label = GLib.DateTime.new_now_local().format("%H:%M");
}]],
}),
Widget.Label({
className: 'txt-norm txt',
label: '•',
}),
Widget.Label({
className: 'txt-smallie txt',
connections: [[5000, label => {
label.label = GLib.DateTime.new_now_local().format("%A, %d/%m");
}]],
}),
],
});
const BarBattery = () => {
const BarResourceValue = (name, icon, command) => Widget.Box({
vpack: 'center',
className: 'bar-batt spacing-h-5',
children: [
MaterialIcon(icon, 'small'),
Widget.ProgressBar({ // Progress
vpack: 'center', hexpand: true,
className: 'bar-prog-batt',
connections: [[5000, (progress) => execAsync(['bash', '-c', command])
.then((output) => {
progress.value = Number(output) / 100;
progress.tooltipText = `${name}: ${Number(output)}%`
})
.catch(print)
]],
}),
]
});
const batteryWidget = Widget.Box({
vpack: 'center',
hexpand: true,
className: 'spacing-h-5 bar-batt',
connections: [[Battery, box => {
box.toggleClassName('bar-batt-low', Battery.percent <= BATTERY_LOW);
box.toggleClassName('bar-batt-full', Battery.charged);
}]],
children: [
MaterialIcon('settings_heart', 'small'),
Widget.Label({ // Percentage
className: 'bar-batt-percentage',
connections: [[Battery, label => {
label.label = `${Battery.percent}`;
}]],
}),
Widget.ProgressBar({ // Progress
vpack: 'center',
hexpand: true,
className: 'bar-prog-batt',
connections: [[Battery, progress => {
progress.value = Math.abs(Battery.percent / 100); // battery could be initially negative wtf
progress.toggleClassName('bar-prog-batt-low', Battery.percent <= BATTERY_LOW);
progress.toggleClassName('bar-prog-batt-full', Battery.charged);
batteryWidget.tooltipText = `Battery: ${Battery.percent}%`
}]],
}),
Widget.Revealer({ // A dot for charging state
transitionDuration: 150,
revealChild: false,
transition: 'slide_left',
child: Widget.Box({
className: 'spacing-h-3',
children: [
Widget.Box({
vpack: 'center',
className: 'bar-batt-chargestate-charging-smaller',
connections: [[Battery, box => {
box.toggleClassName('bar-batt-chargestate-low', Battery.percent <= BATTERY_LOW);
box.toggleClassName('bar-batt-chargestate-full', Battery.charged);
}]],
}),
Widget.Box({
vpack: 'center',
className: 'bar-batt-chargestate-charging',
connections: [[Battery, box => {
box.toggleClassName('bar-batt-chargestate-low', Battery.percent <= BATTERY_LOW);
box.toggleClassName('bar-batt-chargestate-full', Battery.charged);
}]],
}),
]
}),
connections: [[Battery, revealer => {
revealer.revealChild = Battery.charging;
}]],
}),
],
});
const memUsage = Widget.Box({
className: 'spacing-h-5',
children: [
BarResourceValue('RAM usage', 'memory', `free | awk '/^Mem/ {printf("%.2f\\n", ($3/$2) * 100)}'`),
BarResourceValue('Swap usage', 'swap_horiz', `free | awk '/^Swap/ {printf("%.2f\\n", ($3/$2) * 100)}'`),
]
})
const widgetStack = Widget.Stack({
transition: 'slide_up_down',
vpack: 'center',
hexpand: true,
items: [
['fallback', memUsage],
['battery', batteryWidget],
],
setup: (stack) => Utils.timeout(1, () => {
if (Battery.available) stack.shown = 'battery';
else stack.shown = 'fallback';
})
})
return widgetStack;
}
export const ModuleSystem = () => Widget.EventBox({
onScrollUp: () => execAsync('hyprctl dispatch workspace -1'),
onScrollDown: () => execAsync('hyprctl dispatch workspace +1'),
child: Widget.Box({
className: 'bar-group-margin bar-sides',
children: [
Widget.Box({
className: 'bar-group bar-group-standalone bar-group-pad-system spacing-h-15',
children: [
BarClock(),
BarBattery(),
],
}),
]
})
});
+66
View File
@@ -0,0 +1,66 @@
const { GLib, Gdk, Gtk } = imports.gi;
import { Service, Widget } from '../../imports.js';
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
const { Box, Icon, Button, Revealer } = Widget;
const { Gravity } = imports.gi.Gdk;
const revealerDuration = 200;
const SysTrayItem = item => Button({
className: 'bar-systray-item',
child: Icon({
hpack: 'center',
binds: [['icon', item, 'icon']]
}),
binds: [['tooltipMarkup', item, 'tooltip-markup']],
onClicked: btn => item.menu.popup_at_widget(btn, Gravity.SOUTH, Gravity.NORTH, null),
onSecondaryClick: btn => item.menu.popup_at_widget(btn, Gravity.SOUTH, Gravity.NORTH, null),
});
export const Tray = (props = {}) => {
const trayContent = Box({
vpack: 'center',
className: 'bar-systray bar-group',
properties: [
['items', new Map()],
['onAdded', (box, id) => {
const item = SystemTray.getItem(id);
if (!item) return;
item.menu.className = 'menu';
if (box._items.has(id) || !item)
return;
const widget = SysTrayItem(item);
box._items.set(id, widget);
box.pack_start(widget, false, false, 0);
box.show_all();
if (box._items.size === 1)
trayRevealer.revealChild = true;
}],
['onRemoved', (box, id) => {
if (!box._items.has(id))
return;
box._items.get(id).destroy();
box._items.delete(id);
if (box._items.size === 0)
trayRevealer.revealChild = false;
}],
],
connections: [
[SystemTray, (box, id) => box._onAdded(box, id), 'added'],
[SystemTray, (box, id) => box._onRemoved(box, id), 'removed'],
],
});
const trayRevealer = Widget.Revealer({
revealChild: false,
transition: 'slide_left',
transitionDuration: revealerDuration,
child: trayContent,
});
return Box({
...props,
children: [
trayRevealer,
]
});
}
+100
View File
@@ -0,0 +1,100 @@
const { GLib, Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const WORKSPACE_SIDE_PAD = 0.546; // rem
const NUM_OF_WORKSPACES = 10;
let lastWorkspace = 0;
const activeWorkspaceIndicator = Widget.Box({
css: `
padding: 0rem ${WORKSPACE_SIDE_PAD}rem;
`,
children: [
Widget.Box({
vpack: 'center',
hpack: 'start',
className: 'bar-ws-active-box',
connections: [
[Hyprland.active.workspace, (box) => {
const ws = Hyprland.active.workspace.id;
box.setCss(`
margin-left: ${1.774 * (ws - 1) + 0.068}rem;
`);
lastWorkspace = ws;
}],
],
children: [
Widget.Label({
vpack: 'center',
className: 'bar-ws-active',
label: ``,
})
]
})
]
});
export const ModuleWorkspaces = () => Widget.EventBox({
onScrollUp: () => Utils.execAsync(['bash', '-c', 'hyprctl dispatch workspace -1 &']),
onScrollDown: () => Utils.execAsync(['bash', '-c', 'hyprctl dispatch workspace +1 &']),
onMiddleClickRelease: () => App.toggleWindow('overview'),
onSecondaryClickRelease: () => App.toggleWindow('osk'),
child: Widget.Box({
homogeneous: true,
className: 'bar-ws-width',
children: [
Widget.Overlay({
passThrough: true,
child: Widget.Box({
homogeneous: true,
className: 'bar-group-center',
children: [Widget.Box({
className: 'bar-group-standalone bar-group-pad',
})]
}),
overlays: [
Widget.Overlay({
setup: (self) => self.set_overlay_pass_through(self.get_children()[1], true),
child: Widget.Box({
hpack: 'center',
css: `
padding: 0rem ${WORKSPACE_SIDE_PAD}rem;
`,
// homogeneous: true,
children: Array.from({ length: NUM_OF_WORKSPACES }, (_, i) => i + 1).map(i => Widget.Button({
onPrimaryClick: () => Utils.execAsync(['bash', '-c', `hyprctl dispatch workspace ${i} &`]).catch(print),
child: Widget.Label({
vpack: 'center',
label: `${i}`,
className: 'bar-ws txt',
}),
})),
connections: [
[Hyprland, (box) => {
// console.log('update');
const kids = box.children;
kids.forEach((child, i) => {
child.child.toggleClassName('bar-ws-occupied', false);
child.child.toggleClassName('bar-ws-occupied-left', false);
child.child.toggleClassName('bar-ws-occupied-right', false);
child.child.toggleClassName('bar-ws-occupied-left-right', false);
});
const occupied = Array.from({ length: NUM_OF_WORKSPACES }, (_, i) => Hyprland.getWorkspace(i + 1)?.windows > 0);
for (let i = 0; i < occupied.length; i++) {
if (!occupied[i]) continue;
const child = kids[i];
child.child.toggleClassName(`bar-ws-occupied${!occupied[i - 1] ? '-left' : ''}${!occupied[i + 1] ? '-right' : ''}`, true);
}
}, 'notify::workspaces'],
],
}),
overlays: [
activeWorkspaceIndicator,
]
})
],
})
]
})
});
@@ -0,0 +1,60 @@
import { Widget } from '../../imports.js';
import { keybindList } from "../../data/keybinds.js";
export const Keybinds = () => Widget.Box({
vertical: false,
className: "spacing-h-15",
homogeneous: true,
children: keybindList.map((group, i) => Widget.Box({ // Columns
vertical: true,
className: "spacing-v-15",
children: group.map((category, i) => Widget.Box({ // Categories
vertical: true,
className: "spacing-v-15",
children: [
Widget.Box({ // Category header
vertical: false,
className: "spacing-h-10",
children: [
Widget.Label({
xalign: 0,
className: "icon-material txt txt-larger",
label: category.icon,
}),
Widget.Label({
xalign: 0,
className: "cheatsheet-category-title txt",
label: category.name,
}),
]
}),
Widget.Box({
vertical: false,
className: "spacing-h-10",
children: [
Widget.Box({ // Keys
vertical: true,
homogeneous: true,
children: category.binds.map((keybinds, i) => Widget.Box({ // Binds
vertical: false,
children: keybinds.keys.map((key, i) => Widget.Label({ // Specific keys
className: `${key == 'OR' || key == '+' ? 'cheatsheet-key-notkey' : 'cheatsheet-key'} txt-small`,
label: key,
}))
}))
}),
Widget.Box({ // Actions
vertical: true,
homogeneous: true,
children: category.binds.map((keybinds, i) => Widget.Label({ // Binds
xalign: 0,
label: keybinds.action,
className: "txt chearsheet-action txt-small",
}))
})
]
})
]
}))
})),
});
+90
View File
@@ -0,0 +1,90 @@
const { Gdk, Gtk } = imports.gi;
import { Service, Widget } from '../../imports.js';
import { Keybinds } from "./keybinds.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
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',
children: [
Widget.Label({
hpack: 'center',
css: 'margin-right: 0.682rem;',
className: 'txt-title txt',
label: 'Cheat sheet',
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key txt-small",
label: "",
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key-notkey txt-small",
label: "+",
}),
Widget.Label({
vpack: 'center',
className: "cheatsheet-key txt-small",
label: "/",
})
]
}),
Widget.Label({
useMarkup: true,
selectable: true,
justify: Gtk.Justification.CENTER,
className: 'txt-small txt',
label: 'Sheet data stored in <tt>~/.config/ags/data/keybinds.js</tt>\nChange keybinds in <tt>~/.config/hypr/keybinds.conf</tt>'
}),
]
}),
endWidget: Widget.Button({
vpack: 'start',
hpack: 'end',
className: "cheatsheet-closebtn icon-material txt txt-hugeass",
onClicked: () => {
App.toggleWindow('cheatsheet');
},
child: Widget.Label({
className: 'icon-material txt txt-hugeass',
label: 'close'
}),
setup: setupCursorHover,
}),
});
const clickOutsideToClose = Widget.EventBox({
onPrimaryClick: () => App.closeWindow('cheatsheet'),
onSecondaryClick: () => App.closeWindow('cheatsheet'),
onMiddleClick: () => App.closeWindow('cheatsheet'),
});
export default () => Widget.Window({
name: 'cheatsheet',
exclusivity: 'normal',
focusable: true,
popup: true,
visible: false,
child: Widget.Box({
vertical: true,
children: [
clickOutsideToClose,
Widget.Box({
vertical: true,
className: "cheatsheet-bg spacing-v-15",
children: [
cheatsheetHeader(),
Keybinds(),
]
}),
],
})
});
@@ -0,0 +1,83 @@
const { Gdk, Gtk } = imports.gi;
const Lang = imports.lang;
import { App, Service, Utils, Widget, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../imports.js';
const { execAsync, exec } = Utils;
const { Box, Label } = Widget;
const NUM_OF_VERTICES = 30;
const NUM_OF_EDGES = 29;
// Vertices
var vertices = [];
for (var i = 0; i < NUM_OF_VERTICES; i++) {
vertices.push([
Math.floor(Math.random() * SCREEN_WIDTH),
Math.floor(Math.random() * SCREEN_HEIGHT)
]);
}
// Edges
function generateRandomEdges(numVertices, numEdges) { // TODO: make sure whole graph is connected
var edges = new Set();
var vertices = [];
// Generate vertices
for (var i = 0; i < numVertices; i++) {
vertices.push(i);
}
// Generate random distinct edges
while (edges.size < numEdges) {
var randomVertex1 = vertices[Math.floor(Math.random() * numVertices)];
var randomVertex2 = vertices[Math.floor(Math.random() * numVertices)];
// Ensure the two vertices are distinct and the edge doesn't already exist
if (randomVertex1 !== randomVertex2) {
var edge = [randomVertex1, randomVertex2].sort();
edges.add(edge.join(','));
}
}
return Array.from(edges).map(edge => edge.split(',').map(Number));
}
var edges = generateRandomEdges(NUM_OF_VERTICES, NUM_OF_EDGES);
export default () => Box({
hpack: 'fill',
vpack: 'fill',
homogeneous: true,
children: [
Widget.DrawingArea({
className: 'bg-graph',
setup: (area) => {
area.connect('draw', Lang.bind(area, (area, cr) => {
// area.set_size_request(SCREEN_WIDTH, SCREEN_HEIGHT);
// console.log('allocated width/height:', area.get_allocated_width(), '/', area.get_allocated_height())
const styleContext = area.get_style_context();
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
const backgroundColor = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const radius = area.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
const borderWidth = area.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
cr.setSourceRGBA(backgroundColor.red, backgroundColor.green, backgroundColor.blue, backgroundColor.alpha);
cr.rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
cr.fill();
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
// Draw edges
cr.setLineWidth(borderWidth);
console.log("line width:", borderWidth);
for (var i = 0; i < NUM_OF_EDGES; i++) {
console.log(vertices[edges[i][0]][0], vertices[edges[i][0]][1], '->', vertices[edges[i][1]][0], vertices[edges[i][1]][1])
cr.moveTo(vertices[edges[i][0]][0], vertices[edges[i][0]][1]);
cr.lineTo(vertices[edges[i][1]][0], vertices[edges[i][1]][1]);
cr.stroke();
}
// Draw vertices
for (var i = 0; i < NUM_OF_VERTICES; i++) {
cr.arc(vertices[i][0], vertices[i][1], radius, 0, 2 * Math.PI)
cr.fill()
}
}))
}
})
]
})
@@ -0,0 +1,27 @@
const { Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
import TimeAndLaunchesWidget from './timeandlaunches.js'
import SystemWidget from './system.js'
import GraphWidget from './graph.js'
export default () => Widget.Window({
name: 'desktopbackground',
anchor: ['top', 'bottom', 'left', 'right'],
layer: 'background',
exclusivity: 'normal',
visible: true,
child: Widget.Overlay({
child: Widget.Box({
hexpand: true,
vexpand: true,
}),
overlays: [
// GraphWidget(),
TimeAndLaunchesWidget(),
SystemWidget(),
],
setup: (self) => self.set_overlay_pass_through(self.get_children()[1], true),
}),
});
@@ -0,0 +1,151 @@
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
const { Box, EventBox, Label, Revealer, Overlay } = Widget;
import { AnimatedCircProg } from '../../lib/animatedcircularprogress.js'
import { MaterialIcon } from '../../lib/materialicon.js';
const ResourceValue = (name, icon, interval, valueUpdateCmd, displayFunc, props = {}) => Box({
...props,
className: 'bg-system-bg txt',
children: [
Revealer({
transition: 'slide_left',
transitionDuration: 200,
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',
connections: [[interval, (label) => displayFunc(label)]]
})
]
})
}),
Overlay({
child: AnimatedCircProg({
className: 'bg-system-circprog',
connections: [[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/ {printf("%.2f\\n", ($3/$2) * 100)}'`,
(label) => {
execAsync(['bash', '-c', `free -h | awk '/^Swap/ {print $3 " / " $2}' | 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: '<version>',
setup: (label) => {
execAsync([`bash`, `-c`, `hyprctl version | grep -oP "Tag: v\\K\\d+\\.\\d+\\.\\d+"`]).then(distro => {
label.label = `Hyprland ${distro}`;
}).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();
kids.forEach((child, i) => {
child.get_children()[0].revealChild = !child.get_children()[0].revealChild;
});
},
})
],
})
@@ -0,0 +1,69 @@
const { GLib, Gio } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.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 '../../lib/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,
connections: [[5000, label => {
label.label = GLib.DateTime.new_now_local().format("%H:%M");
}]],
}),
Label({
className: 'bg-time-date',
xalign: 0,
connections: [[5000, label => {
label.label = GLib.DateTime.new_now_local().format("%A, %d/%m/%Y");
}]],
}),
]
})
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-v-20',
children: [
TimeAndDate(),
// QuickLaunches(),
],
})
+264
View File
@@ -0,0 +1,264 @@
const { Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../imports.js';
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, EventBox, Label, Revealer, Overlay } = Widget;
import { AnimatedCircProg } from '../../lib/animatedcircularprogress.js'
import { MaterialIcon } from '../../lib/materialicon.js';
import { setupCursorHover, setupCursorHoverAim } from "../../lib/cursorhover.js";
const ANIMATION_TIME = 150;
const pinnedApps = [
'firefox',
'org.gnome.Nautilus',
];
function substitute(str) {
const subs = [
{ from: 'code-url-handler', to: 'visual-studio-code' },
{ from: 'Code', to: 'visual-studio-code' },
{ from: 'GitHub Desktop', to: 'github-desktop' },
{ from: 'wpsoffice', to: 'wps-office2019-kprometheus' },
{ from: 'gnome-tweaks', to: 'org.gnome.tweaks' },
{ from: 'Minecraft* 1.20.1', to: 'minecraft' },
{ from: '', to: 'image-missing' },
];
for (const { from, to } of subs) {
if (from === str)
return to;
}
return str;
}
const focus = ({ address }) => Utils.execAsync(`hyprctl dispatch focuswindow address:${address}`);
const DockSeparator = (props = {}) => Box({
...props,
className: 'dock-separator',
})
const AppButton = ({ icon, ...rest }) => Widget.Revealer({
properties: [
['workspace', 0],
],
revealChild: false,
transition: 'slide_right',
transitionDuration: ANIMATION_TIME,
child: Widget.Button({
...rest,
className: 'dock-app-btn',
child: Widget.Box({
child: Widget.Overlay({
child: Widget.Box({
homogeneous: true,
className: 'dock-app-icon',
child: Widget.Icon({
icon: icon,
setup: (self) => Utils.timeout(1, () => {
const styleContext = self.get_parent().get_style_context();
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
self.size = Math.max(width, height, 1);
})
}),
}),
overlays: [Widget.Box({
class_name: 'indicator',
vpack: 'end',
hpack: 'center',
})],
}),
}),
setup: (button) => {
setupCursorHover(button);
}
})
});
const Taskbar = () => Widget.Box({
className: 'dock-apps',
properties: [
['map', new Map()],
['clientSortFunc', (a, b) => {
return a._workspace > b._workspace;
}],
['update', (box) => {
Hyprland.clients.forEach(client => {
if (client["pid"] == -1) return;
const appClass = substitute(client.class);
for (const appName of pinnedApps) {
if (appClass.includes(appName.toLowerCase()))
return null;
}
const newButton = AppButton({
icon: appClass,
tooltipText: `${client.title} (${appClass})`,
onClicked: () => focus(client),
});
newButton._workspace = client.workspace.id;
newButton.revealChild = true;
box._map.set(client.address, newButton);
})
box.children = Array.from(box._map.values());
}],
['add', (box, address) => {
if (!address) { // Since the first active emit is undefined
box._update(box);
return;
}
const newClient = Hyprland.clients.find(client => {
return client.address == address;
});
const appClass = substitute(newClient.class);
const newButton = AppButton({
icon: appClass,
tooltipText: `${newClient.title} (${appClass})`,
onClicked: () => focus(newClient),
})
newButton._workspace = newClient.workspace.id;
box._map.set(address, newButton);
box.children = Array.from(box._map.values());
newButton.revealChild = true;
}],
['remove', (box, address) => {
if (!address) return;
const removedButton = box._map.get(address);
removedButton.revealChild = false;
Utils.timeout(ANIMATION_TIME, () => {
removedButton.destroy();
box._map.delete(address);
box.children = Array.from(box._map.values());
})
}],
],
connections: [
// [Hyprland, (box) => box._update(box)],
[Hyprland, (box, address) => box._add(box, address), 'client-added'],
[Hyprland, (box, address) => box._remove(box, address), 'client-removed'],
],
setup: (self) => {
Utils.timeout(100, () => self._update(self));
}
});
const PinnedApps = () => Widget.Box({
class_name: 'dock-apps',
homogeneous: true,
children: pinnedApps
.map(term => ({ app: Applications.query(term)?.[0], term }))
.filter(({ app }) => app)
.map(({ app, term = true }) => {
const newButton = AppButton({
icon: app.icon_name,
onClicked: () => {
for (const client of Hyprland.clients) {
if (client.class.toLowerCase().includes(term))
return focus(client);
}
app.launch();
},
onMiddleClick: () => app.launch(),
setup: (self) => {
self.revealChild = true;
},
tooltipText: app.name,
connections: [[Hyprland, button => {
const running = Hyprland.clients
.find(client => client.class.toLowerCase().includes(term)) || false;
button.toggleClassName('nonrunning', !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 () => {
const dockContent = Box({
className: 'dock-bg spacing-h-5',
children: [
PinnedApps(),
DockSeparator(),
Taskbar(),
]
})
const dockRevealer = Revealer({
properties: [
['updateShow', self => { // I only use mouse to resize. I don't care about keyboard resize if that's a thing
const dockSize = [
dockContent.get_allocated_width(),
dockContent.get_allocated_height()
]
const dockAt = [
SCREEN_WIDTH / 2 - dockSize[0] / 2,
SCREEN_HEIGHT - dockSize[1],
];
const dockLeft = dockAt[0];
const dockRight = dockAt[0] + dockSize[0];
const dockTop = dockAt[1];
const dockBottom = dockAt[1] + dockSize[1];
const currentWorkspace = Hyprland.active.workspace.id;
var toReveal = true;
const hyprlandClients = JSON.parse(exec('hyprctl clients -j'));
for (const index in hyprlandClients) {
const client = hyprlandClients[index];
const clientLeft = client.at[0];
const clientRight = client.at[0] + client.size[0];
const clientTop = client.at[1];
const clientBottom = client.at[1] + client.size[1];
if (client.workspace.id == currentWorkspace) {
if (
clientLeft < dockRight &&
clientRight > dockLeft &&
clientTop < dockBottom &&
clientBottom > dockTop
) {
self.revealChild = false;
return;
}
}
}
self.revealChild = true;
}]
],
revealChild: false,
transition: 'slide_up',
transitionDuration: 200,
child: dockContent,
connections: [
// [Hyprland, (self) => self._updateShow(self)],
// [Hyprland.active.workspace, (self) => self._updateShow(self)],
// [Hyprland.active.client, (self) => self._updateShow(self)],
// [Hyprland, (self) => self._updateShow(self), 'client-added'],
// [Hyprland, (self) => self._updateShow(self), 'client-removed'],
],
})
return EventBox({
onHover: () => {
dockRevealer.revealChild = true;
},
onHoverLost: () => {
if (Hyprland.active.client._class.length === 0) return;
dockRevealer.revealChild = false;
},
child: Box({
homogeneous: true,
css: 'min-height: 2px;',
children: [
dockRevealer,
]
}),
})
}
+11
View File
@@ -0,0 +1,11 @@
import { App, Widget } from '../../imports.js';
import Dock from './dock.js';
export default () => Widget.Window({
name: 'dock',
layer: 'bottom',
anchor: ['bottom'],
exclusivity: 'normal',
visible: true,
child: Dock(),
});
@@ -0,0 +1,58 @@
const { Gio, GLib, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { exec, execAsync } = Utils;
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
import { AnimatedCircProg } from "../../lib/animatedcircularprogress.js";
import { MaterialIcon } from '../../lib/materialicon.js';
import { showColorScheme } from '../../variables.js';
const ColorBox = ({
name = 'Color',
...rest
}) => Box({
...rest,
homogeneous: true,
children: [
Label({
label: `${name}`,
})
]
})
const colorschemeContent = Box({
className: 'osd-colorscheme spacing-v-5',
vertical: true,
hpack: 'center',
children: [
Label({
xalign: 0,
className: 'txt-norm titlefont txt',
label: 'Colorscheme',
}),
Box({
className: 'spacing-h-5',
children: [
ColorBox({ name: 'P', className: 'osd-color osd-color-primary' }),
ColorBox({ name: 'P-c', className: 'osd-color osd-color-primaryContainer' }),
ColorBox({ name: 'S', className: 'osd-color osd-color-secondary' }),
ColorBox({ name: 'S-c', className: 'osd-color osd-color-secondaryContainer' }),
ColorBox({ name: 'Sf-v', className: 'osd-color osd-color-surfaceVariant' }),
ColorBox({ name: 'Sf', className: 'osd-color osd-color-surface' }),
ColorBox({ name: 'Bg', className: 'osd-color osd-color-background' }),
]
})
]
});
export default () => Widget.Revealer({
transition: 'slide_down',
transitionDuration: 200,
child: colorschemeContent,
connections: [
[showColorScheme, (revealer) => {
revealer.revealChild = showColorScheme.value;
}],
],
})
@@ -0,0 +1,76 @@
// This file is for brightness/volume indicators
const { GLib, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
import Brightness from '../../services/brightness.js';
import Indicator from '../../services/indicator.js';
import Notification from '../../lib/notification.js';
const OsdValue = (name, labelConnections, progressConnections, props = {}) => Widget.Box({ // Volume
...props,
vertical: true,
className: 'osd-bg osd-value',
hexpand: true,
children: [
Widget.Box({
vexpand: true,
children: [
Widget.Label({
xalign: 0, yalign: 0, hexpand: true,
className: 'osd-label',
label: `${name}`,
}),
Widget.Label({
hexpand: false, className: 'osd-value-txt',
label: '100',
connections: labelConnections,
}),
]
}),
Widget.ProgressBar({
className: 'osd-progress',
hexpand: true,
vertical: false,
connections: progressConnections,
})
],
});
const brightnessIndicator = OsdValue('Brightness',
[[Brightness, self => {
self.label = `${Math.round(Brightness.screen_value * 100)}`;
}, 'notify::screen-value']],
[[Brightness, (progress) => {
const updateValue = Brightness.screen_value;
progress.value = updateValue;
}, 'notify::screen-value']],
)
const volumeIndicator = OsdValue('Volume',
[[Audio, (label) => {
label.label = `${Math.round(Audio.speaker?.volume * 100)}`;
}]],
[[Audio, (progress) => {
const updateValue = Audio.speaker?.volume;
if (!isNaN(updateValue)) progress.value = updateValue;
}]],
);
export default () => Widget.Revealer({
transition: 'slide_down',
connections: [
[Indicator, (revealer, value) => {
revealer.revealChild = (value > -1);
}, 'popup'],
],
child: Widget.Box({
hpack: 'center',
vertical: false,
children: [
brightnessIndicator,
volumeIndicator,
]
})
});
+30
View File
@@ -0,0 +1,30 @@
import { Widget } from '../../imports.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) => Widget.Window({
name: `indicator${monitor}`,
monitor,
className: 'indicator',
layer: 'overlay',
visible: true,
anchor: ['top'],
child: Widget.EventBox({
onHover: () => { //make the widget hide when hovering
Indicator.popup(-1);
},
child: Widget.Box({
vertical: true,
css: 'min-height: 2px;',
children: [
IndicatorValues(),
MusicControls(),
NotificationPopups(),
ColorScheme(),
]
})
}),
});
@@ -0,0 +1,373 @@
const { Gio, GLib, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { exec, execAsync } = Utils;
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
import { AnimatedCircProg } from "../../lib/animatedcircularprogress.js";
import { MaterialIcon } from '../../lib/materialicon.js';
import { showMusicControls } from '../../variables.js';
function expandTilde(path) {
if (path.startsWith('~')) {
return GLib.get_home_dir() + path.slice(1);
} else {
return path;
}
}
const LIGHTDARK_FILE_LOCATION = '~/.cache/ags/user/colormode.txt'
const lightDark = Utils.readFile(expandTilde(LIGHTDARK_FILE_LOCATION)).trim();
const COVER_COLORSCHEME_SUFFIX = '_colorscheme.css';
const PREFERRED_PLAYER = 'plasma-browser-integration';
var lastCoverPath = '';
function isRealPlayer(player) {
return (
!player.busName.startsWith('org.mpris.MediaPlayer2.firefox') &&
!player.busName.startsWith('org.mpris.MediaPlayer2.playerctld')
);
}
export const getPlayer = (name = PREFERRED_PLAYER) => {
return 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 fileExists(filePath) {
let file = Gio.File.new_for_path(filePath);
return file.query_exists(null);
}
function detectMediaSource(link) {
if (link.startsWith("file://")) {
if (link.includes('firefox-mpris'))
return '󰈹 Firefox'
return "󰈣 File";
}
// Remove protocol if present
let url = link.replace(/(^\w+:|^)\/\//, '');
// Extract the domain name
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;
}
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',
connections: [ // Update on change/once every 3 seconds
[Mpris, _updateProgress],
[3000, _updateProgress]
],
})
}
const TrackTitle = ({ player, ...rest }) => Label({
...rest,
label: 'No music playing',
xalign: 0,
truncate: 'end',
// wrap: true,
className: 'osd-music-title',
connections: [[player, (self) => {
// Player name
self.label = player.trackTitle.length > 0 ? 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',
connections: [[player, (self) => {
self.label = player.trackArtists.length > 0 ? player.trackArtists.join(', ') : '';
}, 'notify::track-artists']]
})
const CoverArt = ({ player, ...rest }) => Box({
...rest,
className: 'osd-music-cover',
children: [
Widget.Overlay({
child: Box({ // Fallback
className: 'osd-music-cover-fallback',
homogeneous: true,
children: [Label({
className: 'icon-material txt-hugeass',
label: 'music_note',
})]
}),
overlays: [ // Real
Box({
properties: [
['updateCover', (self) => {
const player = Mpris.getPlayer();
// Player closed
// Note that cover path still remains, so we're checking title
if (!player || player.trackTitle == "") {
self.css = `background-image: none;`;
App.applyCss(`${App.configDir}/style.css`);
return;
}
const coverPath = player.coverPath;
const stylePath = `${player.coverPath}${lightDark}${COVER_COLORSCHEME_SUFFIX}`;
if (player.coverPath == lastCoverPath) { // Since 'notify::cover-path' emits on cover download complete
self.css = `background-image: url('${coverPath}');`;
}
lastCoverPath = player.coverPath;
// If a colorscheme has already been generated, skip generation
if (fileExists(stylePath)) {
self.css = `background-image: url('${coverPath}');`;
App.applyCss(stylePath);
return;
}
// Generate colors
execAsync(['bash', '-c',
`${App.configDir}/scripts/color_generation/generate_colors_material.py --path '${coverPath}' > ${App.configDir}/scss/_musicmaterial.scss ${lightDark}`])
.then(() => {
exec(`wal -i "${player.coverPath}" -n -t -s -e -q ${lightDark}`)
exec(`bash -c "cp ~/.cache/wal/colors.scss ${App.configDir}/scss/_musicwal.scss"`)
exec(`sassc ${App.configDir}/scss/_music.scss ${stylePath}`);
self.css = `background-image: url('${coverPath}');`;
App.applyCss(`${stylePath}`);
})
.catch(print);
}],
],
className: 'osd-music-cover-art',
connections: [
[player, (self) => self._updateCover(self), 'notify::cover-path']
],
})
]
})
],
})
const TrackControls = ({ player, ...rest }) => Widget.Revealer({
revealChild: false,
transition: 'slide_right',
transitionDuration: 200,
child: Widget.Box({
...rest,
vpack: 'center',
className: 'osd-music-controls spacing-h-3',
children: [
Button({
className: 'osd-music-controlbtn',
child: Label({
className: 'icon-material osd-music-controlbtn-txt',
label: 'skip_previous',
})
}),
Button({
className: 'osd-music-controlbtn',
child: Label({
className: 'icon-material osd-music-controlbtn-txt',
label: 'skip_next',
})
}),
],
}),
connections: [[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: 200,
child: Widget.Box({
...rest,
className: 'osd-music-pill spacing-h-5',
homogeneous: true,
children: [
Label({
hpack: 'fill',
justification: 'center',
className: 'icon-nerd',
connections: [[player, (self) => {
self.label = detectMediaSource(player.trackCoverUrl);
}, 'notify::cover-path']]
}),
],
}),
connections: [[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: 200,
child: Widget.Box({
...rest,
vpack: 'center',
className: 'osd-music-pill spacing-h-5',
children: [
Label({
connections: [[1000, (self) => {
const player = Mpris.getPlayer();
if (!player) return;
self.label = lengthStr(player.position);
}]]
}),
Label({ label: '/' }),
Label({
connections: [[Mpris, (self) => {
const player = Mpris.getPlayer();
if (!player) return;
self.label = lengthStr(player.length);
}]]
}),
],
}),
connections: [[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: () => {
Mpris.getPlayer().playPause()
},
child: Widget.Label({
justification: 'center',
hpack: 'fill',
vpack: 'center',
connections: [[player, (label) => {
label.label = `${player.playBackStatus == 'Playing' ? 'pause' : 'play_arrow'}`;
}, 'notify::play-back-status']],
}),
}),
],
// setup: self => Utils.timeout(1, () => {
// self.set_overlay_pass_through(self.get_children()[1], true);
// }),
passThrough: true,
})
});
}
const MusicControlsWidget = (player) => Box({
className: 'osd-music spacing-h-20',
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);
box.pack_end(TrackTime({ player: player }), false, false, 0)
// box.pack_end(TrackSource({ vpack: 'center', player: player }), false, false, 0);
}
})
]
})
]
})
export default () => Widget.Revealer({
transition: 'slide_down',
transitionDuration: 170,
child: Box({
connections: [[Mpris, box => {
let foundPlayer = false;
Mpris.players.forEach((player, i) => {
if (isRealPlayer(player)) {
foundPlayer = true;
box._player = player;
box.children = [MusicControlsWidget(player)];
}
});
if (!foundPlayer) {
box._player = null;
box.get_children().forEach(ch => ch.destroy());
return;
}
}, 'notify::players']],
}),
connections: [
[showMusicControls, (revealer) => {
revealer.revealChild = showMusicControls.value;
}],
],
})
@@ -0,0 +1,82 @@
// This file is for popup notifications
const { GLib, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
const { Box, EventBox, Icon, Scrollable, Label, Button, Revealer } = Widget;
import Notification from '../../lib/notification.js';
const PopupNotification = (notifObject) => Widget.Box({
homogeneous: true,
children: [
Widget.EventBox({
onHoverLost: () => {
notifObject.dismiss();
},
child: Widget.Revealer({
revealChild: true,
child: Widget.Box({
children: [Notification({
notifObject: notifObject,
isPopup: true,
props: { hpack: 'fill' },
})],
}),
})
})
]
})
const naiveNotifPopupList = Widget.Box({
vertical: true,
className: 'spacing-v-5',
connections: [
[Notifications, (box) => {
box.children = Notifications.popups.reverse()
.map(notifItem => PopupNotification(notifItem));
}],
],
})
const notifPopupList = Box({
vertical: true,
className: 'osd-notifs spacing-v-5-revealer',
properties: [
['map', new Map()],
['dismiss', (box, id, force = false) => {
if (!id || !box._map.has(id) || box._map.get(id)._hovered && !force)
return;
const notif = box._map.get(id);
notif.revealChild = false;
notif._destroyWithAnims();
}],
['notify', (box, id) => {
// console.log('new notiffy', id, Notifications.getNotification(id))
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
box._map.delete(id);
const notif = Notifications.getNotification(id);
const newNotif = Notification({
notifObject: notif,
isPopup: true,
});
box._map.set(id, newNotif);
box.pack_end(box._map.get(id), false, false, 0);
box.show_all();
// box.children = Array.from(box._map.values()).reverse();
}],
],
connections: [
[Notifications, (box, id) => box._notify(box, id), 'notified'],
[Notifications, (box, id) => box._dismiss(box, id), 'dismissed'],
[Notifications, (box, id) => box._dismiss(box, id, true), 'closed'],
],
});
export default () => notifPopupList;
@@ -0,0 +1,10 @@
import PopupWindow from '../../lib/popupwindow.js';
import OnScreenKeyboard from "./onscreenkeyboard.js";
export default () => PopupWindow({
anchor: ['bottom'],
name: 'osk',
showClassName: 'osk-show',
hideClassName: 'osk-hide',
child: OnScreenKeyboard(),
});
@@ -0,0 +1,175 @@
const { GLib, Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const { Box, EventBox, Button, Revealer } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from '../../lib/materialicon.js';
import { separatorLine } from '../../lib/separator.js';
import { defaultOskLayout, oskLayouts } from '../../data/keyboardlayouts.js';
import { setupCursorHoverGrab } from '../../lib/cursorhover.js';
const keyboardLayout = defaultOskLayout;
const keyboardJson = oskLayouts[keyboardLayout];
execAsync(`ydotoold`).catch(print); // Start ydotool daemon
function releaseAllKeys() {
const keycodes = Array.from(Array(249).keys());
execAsync([`ydotool`, `key`, ...keycodes.map(keycode => `${keycode}:0`)])
.then(console.log('Released all keys'))
.catch(print);
}
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();
App.toggleWindow('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 --no-fuzzy --dmenu | cliphist decode | wl-copy"]).catch(print);
},
label: 'assignment',
}),
]
})
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: (key.shape == "space" || key.shape == "expand"),
label: key.label,
setup: (button) => {
let pressed = false;
if (key.keytype == "normal") {
button.connect('pressed', () => { // mouse down
execAsync(`ydotool key ${key.keycode}:1`);
});
button.connect('clicked', () => { // release
execAsync(`ydotool key ${key.keycode}:0`);
});
}
else if (key.keytype == "modkey") {
button.connect('pressed', () => { // release
if (pressed) {
execAsync(`ydotool key ${key.keycode}:0`);
button.toggleClassName('osk-key-active', false);
pressed = false;
}
else {
execAsync(`ydotool key ${key.keycode}:1`);
button.toggleClassName('osk-key-active', true);
pressed = true;
modsPressed = true;
}
});
}
}
})
})
}))
})
}
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,
separatorLine,
keyboardItself(keyboardJson),
],
})
],
connections: [[App, (box, name, visible) => { // Update on open
if (name == 'osk' && visible) {
keyboardWindow.setCss(`margin-bottom: -0px;`);
}
}],],
});
const gestureEvBox = EventBox({ child: keyboardWindow })
const gesture = Gtk.GestureDrag.new(gestureEvBox);
gesture.connect('drag-begin', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
gesture.startY = JSON.parse(out).y;
}).catch(print);
});
gesture.connect('drag-update', () => {
Hyprland.sendMessage('j/cursorpos').then((out) => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset > 0) return;
keyboardWindow.setCss(`
margin-bottom: ${offset}px;
`);
}).catch(print);
});
gesture.connect('drag-end', () => {
var offset = gesture.get_offset()[2];
if (offset > 50) {
App.closeWindow('osk');
}
else {
keyboardWindow.setCss(`
transition: margin-bottom 170ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-bottom: 0px;
`);
}
})
export default () => gestureEvBox;
+31
View File
@@ -0,0 +1,31 @@
const { Gio, GLib } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
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) {
console.log('dump', 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));
}
+18
View File
@@ -0,0 +1,18 @@
import { Widget } from '../../imports.js';
import { SearchAndWindows } from "./overview.js";
export default () => Widget.Window({
name: 'overview',
exclusivity: 'normal',
focusable: true,
popup: true,
visible: false,
anchor: ['top'],
layer: 'overlay',
child: Widget.Box({
vertical: true,
children: [
SearchAndWindows(),
]
}),
})
@@ -0,0 +1,136 @@
const { Gio, GLib } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
import Todo from "../../services/todo.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.split(' ');
if (args[0] == '>raw') { // Mouse raw input
execAsync([`bash`, `-c`, `hyprctl keyword input:force_no_accel $(( 1 - $(hyprctl getoption input:force_no_accel -j | gojq ".int") ))`, `&`]).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
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh`, `&`]).catch(print);
}
else if (args[0] == '>light') { // Light mode
execAsync([`bash`, `-c`, `mkdir -p ~/.cache/ags/user && echo "-l" > ~/.cache/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
.catch(print);
}
else if (args[0] == '>dark') { // Dark mode
execAsync([`bash`, `-c`, `mkdir -p ~/.cache/ags/user && echo "" > ~/.cache/ags/user/colormode.txt`])
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print))
.catch(print);
}
else if (args[0] == '>badapple') { // Light mode
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/applycolor.sh --bad-apple`]).catch(print);
}
else if (args[0] == '>material') { // Light mode
execAsync([`bash`, `-c`, `mkdir -p ~/.cache/ags/user && echo "material" > ~/.cache/ags/user/colorbackend.txt`]).catch(print);
}
else if (args[0] == '>pywal') { // Dark mode
execAsync([`bash`, `-c`, `mkdir -p ~/.cache/ags/user && echo "pywal" > ~/.cache/ags/user/colorbackend.txt`]).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`]).catch(print);
}
else if (args[0] == '>reboot') { // Reboot
execAsync([`bash`, `-c`, `systemctl reboot`]).catch(print);
}
else if (args[0] == '>sleep') { // Sleep
execAsync([`bash`, `-c`, `systemctl suspend`]).catch(print);
}
else if (args[0] == '>logout') { // Log out
execAsync([`bash`, `-c`, `loginctl terminate-user $USER`]).catch(print);
}
}
export function execAndClose(command, terminal) {
App.closeWindow('overview');
if (terminal) {
execAsync([`bash`, `-c`, `foot fish -C "${command}"`, `&`]).catch(print);
}
else
execAsync(command).catch(print);
}
export function startsWithNumber(str) {
var pattern = /^\d/;
return pattern.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;
}
+520
View File
@@ -0,0 +1,520 @@
const { Gdk, Gio, Gtk } = imports.gi;
import { App, Service, Utils, Variable, Widget, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../imports.js';
import Applications from 'resource:///com/github/Aylur/ags/service/applications.js';
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
const { execAsync, exec } = Utils;
import { setupCursorHover, setupCursorHoverGrab } from "../../lib/cursorhover.js";
import { DoubleRevealer } from "../../lib/doublerevealer.js";
import { execAndClose, expandTilde, hasUnterminatedBackslash, startsWithNumber, launchCustomCommand, ls } from './miscfunctions.js';
import {
CalculationResultButton, CustomCommandButton, DirectoryButton,
DesktopEntryButton, ExecuteCommandButton, SearchButton
} from './searchbuttons.js';
import { dumpToWorkspace, swapWorkspace } from "./actions.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)];
const searchPromptTexts = [
'Try "~/.config"',
'Try "Files"',
'Try "6*cos(pi)"',
'Try "sudo pacman -Syu"',
'Try "How to basic"',
'Drag n\' drop to move windows',
'Type to search',
]
const overviewTick = Variable(false);
function truncateTitle(str) {
let lastDash = -1;
let found = -1; // 0: em dash, 1: en dash, 2: minus, 3: vertical bar, 4: middle dot
for (let i = str.length - 1; i >= 0; i--) {
if (str[i] === '—') {
found = 0;
lastDash = i;
}
else if (str[i] === '' && found < 1) {
found = 1;
lastDash = i;
}
else if (str[i] === '-' && found < 2) {
found = 2;
lastDash = i;
}
else if (str[i] === '|' && found < 3) {
found = 3;
lastDash = i;
}
else if (str[i] === '·' && found < 4) {
found = 4;
lastDash = i;
}
}
if (lastDash === -1) return str;
return str.substring(0, lastDash);
}
function iconExists(iconName) {
let iconTheme = Gtk.IconTheme.get_default();
return iconTheme.has_icon(iconName);
}
function substitute(str) {
const subs = [
{ from: 'code-url-handler', to: 'visual-studio-code' },
{ from: 'Code', to: 'visual-studio-code' },
{ from: 'GitHub Desktop', to: 'github-desktop' },
{ from: 'wpsoffice', to: 'wps-office2019-kprometheus' },
{ from: 'gnome-tweaks', to: 'org.gnome.tweaks' },
{ from: 'Minecraft* 1.20.1', to: 'minecraft' },
{ from: '', to: 'image-missing' },
];
for (const { from, to } of subs) {
if (from === str)
return to;
}
if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, '-'); // Turn into kebab-case
return str;
}
const ContextWorkspaceArray = ({ label, actionFunc, thisWorkspace }) => Widget.MenuItem({
label: `${label}`,
setup: (menuItem) => {
let submenu = new Gtk.Menu();
submenu.className = 'menu';
for (let i = 1; i <= 10; i++) {
let button = new Gtk.MenuItem({
label: `Workspace ${i}`
});
button.connect("activate", () => {
// execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print);
actionFunc(thisWorkspace, i);
overviewTick.value = !overviewTick.value;
});
submenu.append(button);
}
menuItem.set_reserve_indicator(true);
menuItem.set_submenu(submenu);
}
})
const client = ({ address, size: [w, h], workspace: { id, name }, class: c, title, xwayland }) => {
const revealInfoCondition = (Math.min(w, h) * OVERVIEW_SCALE > 70);
if (w <= 0 || h <= 0) return null;
title = truncateTitle(title);
return Widget.Button({
className: 'overview-tasks-window',
hpack: 'center',
vpack: 'center',
onClicked: () => {
execAsync([`bash`, `-c`, `hyprctl dispatch focuswindow address:${address}`, `&`]).catch(print);
App.closeWindow('overview');
},
onMiddleClickRelease: () => execAsync([`bash`, `-c`, `hyprctl dispatch closewindow address:${address}`, `&`]).catch(print),
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: () => {
execAsync([`bash`, `-c`, `hyprctl dispatch closewindow address:${address}`, `&`])
.catch(print);
}
}),
ContextWorkspaceArray({
label: "Dump windows to workspace",
actionFunc: dumpToWorkspace,
thisWorkspace: Number(id)
}),
ContextWorkspaceArray({
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_pointer(null); // Show the menu at the pointer's position
},
child: Widget.Box({
css: `
min-width: ${Math.max(w * OVERVIEW_SCALE - 4, 1)}px;
min-height: ${Math.max(h * OVERVIEW_SCALE - 4, 1)}px;
`,
homogeneous: true,
child: Widget.Box({
vertical: true,
vpack: 'center',
className: 'spacing-v-5',
children: [
Widget.Icon({
icon: substitute(c),
size: Math.min(w, h) * OVERVIEW_SCALE / 2.5,
}),
// TODO: Add xwayland tag instead of just having italics
DoubleRevealer({
transition1: 'slide_right',
transition2: 'slide_down',
revealChild: revealInfoCondition,
child: Widget.Scrollable({
hexpand: true,
vscroll: 'never',
hscroll: 'automatic',
child: Widget.Label({
truncate: 'end',
className: `${xwayland ? 'txt txt-italic' : 'txt'}`,
css: `
font-size: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * OVERVIEW_SCALE / 14.6}px;
margin: 0px ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 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.drag_source_set_icon_gicon(icon);
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 = Gtk.Fixed.new();
const WorkspaceNumber = (index) => Widget.Label({
className: 'overview-tasks-workspace-number',
label: `${index}`,
css: `
margin: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * OVERVIEW_SCALE * OVERVIEW_WS_NUM_MARGIN_SCALE}px;
font-size: ${SCREEN_HEIGHT * OVERVIEW_SCALE * OVERVIEW_WS_NUM_SCALE}px;
`,
})
const widget = Widget.Box({
className: 'overview-tasks-workspace',
vpack: 'center',
css: `
min-width: ${SCREEN_WIDTH * OVERVIEW_SCALE}px;
min-height: ${SCREEN_HEIGHT * OVERVIEW_SCALE}px;
`,
children: [Widget.EventBox({
hexpand: true,
vexpand: true,
onPrimaryClickRelease: () => {
execAsync([`bash`, `-c`, `hyprctl dispatch workspace ${index}`, `&`]).catch(print);
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) => {
overviewTick.value = !overviewTick.value;
execAsync([`bash`, `-c`, `hyprctl dispatch movetoworkspacesilent ${index},address:${data.get_text()}`, `&`]).catch(print);
});
},
child: fixed,
})],
});
widget.update = clients => {
clients = clients.filter(({ workspace: { id } }) => id === index);
// this is for my monitor layout
// shifts clients back by SCREEN_WIDTHpx if necessary
clients = clients.map(client => {
const [x, y] = client.at;
if (x > SCREEN_WIDTH)
client.at = [x - SCREEN_WIDTH, y];
return client;
});
fixed.get_children().forEach(ch => ch.destroy());
fixed.put(WorkspaceNumber(index), 0, 0);
clients.forEach(c => c.mapped && fixed.put(client(c), c.at[0] * OVERVIEW_SCALE, c.at[1] * OVERVIEW_SCALE));
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),
properties: [['update', box => {
execAsync('hyprctl -j clients').then(clients => {
const json = JSON.parse(clients);
box.get_children().forEach(ch => ch.update(json));
}).catch(print);
}]],
setup: (box) => box._update(box),
connections: [
// Update on change
[overviewTick, box => { if (!App.getWindow(windowName).visible) return; Utils.timeout(2, () => box._update(box)); }],
[Hyprland, box => { if (!App.getWindow(windowName).visible) return; box._update(box); }, 'client-added'],
[Hyprland, box => { if (!App.getWindow(windowName).visible) return; box._update(box); }, 'client-removed'],
// Update on show
[App, (box, name, visible) => { // Update on open
if (name == 'overview' && visible) {
box._update(box);
}
}],
],
});
export const SearchAndWindows = () => {
var _appSearchResults = [];
const clickOutsideToClose = Widget.EventBox({
onPrimaryClick: () => App.closeWindow('overview'),
onSecondaryClick: () => App.closeWindow('overview'),
onMiddleClick: () => App.closeWindow('overview'),
});
const resultsBox = Widget.Box({
className: 'overview-search-results',
vertical: true,
vexpand: true,
});
const resultsRevealer = Widget.Revealer({
transitionDuration: 200,
revealChild: false,
transition: 'slide_down',
// duration: 200,
hpack: 'center',
child: resultsBox,
});
const overviewRevealer = Widget.Revealer({
revealChild: true,
transition: 'slide_down',
transitionDuration: 200,
child: Widget.Box({
vertical: true,
className: 'overview-tasks',
children: [
OverviewRow({ startWorkspace: 1, workspaces: 5 }),
OverviewRow({ startWorkspace: 6, workspaces: 5 }),
]
}),
});
const entryPromptRevealer = Widget.Revealer({
transition: 'crossfade',
transitionDuration: 150,
revealChild: true,
hpack: 'center',
child: Widget.Label({
className: 'overview-search-prompt txt-small txt',
label: searchPromptTexts[Math.floor(Math.random() * searchPromptTexts.length)],
})
});
const entryIconRevealer = Widget.Revealer({
transition: 'crossfade',
transitionDuration: 150,
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
const text = self.text;
if(text.length == 0) return;
const isAction = text.startsWith('>');
const isDir = (entry.text[0] == '/' || entry.text[0] == '~');
if (startsWithNumber(text)) { // Eval on typing is dangerous, this is a workaround
try {
const fullResult = eval(text);
// copy
execAsync(['wl-copy', `${fullResult}`]).catch(print);
App.closeWindow('overview');
return;
} catch (e) {
// console.log(e);
}
}
if (isDir) {
App.closeWindow('overview');
execAsync(['bash', '-c', `xdg-open "${expandTilde(text)}"`, `&`]).catch(print);
return;
}
if (_appSearchResults.length > 0) {
App.closeWindow('overview');
_appSearchResults[0].launch();
return;
}
else if (text[0] == '>') { // Custom commands
App.closeWindow('overview');
launchCustomCommand(text);
return;
}
// Fallback: Execute command
if (!isAction && exec(`bash -c "command -v ${text.split(' ')[0]}"`) != '') {
if (text.startsWith('sudo'))
execAndClose(text, true);
else
execAndClose(text, false);
}
else {
App.closeWindow('overview');
execAsync(['bash', '-c', `xdg-open 'https://www.google.com/search?q=${text} -site:quora.com' &`]).catch(print); // fuck quora
}
},
// Actually onChange but this is ta workaround for a bug
connections: [
['notify::text', (entry) => { // This is when you type
const isAction = entry.text[0] == '>';
const isDir = (entry.text[0] == '/' || entry.text[0] == '~');
resultsBox.get_children().forEach(ch => ch.destroy());
// check empty if so then dont do stuff
if (entry.text == '') {
resultsRevealer.set_reveal_child(false);
overviewRevealer.set_reveal_child(true);
entryPromptRevealer.set_reveal_child(true);
entryIconRevealer.set_reveal_child(false);
entry.toggleClassName('overview-search-box-extended', false);
}
else {
const text = entry.text;
resultsRevealer.set_reveal_child(true);
overviewRevealer.set_reveal_child(false);
entryPromptRevealer.set_reveal_child(false);
entryIconRevealer.set_reveal_child(true);
entry.toggleClassName('overview-search-box-extended', true);
_appSearchResults = Applications.query(text);
// Calculate
if (startsWithNumber(text)) { // Eval on typing is dangerous, this is a small workaround.
try {
const fullResult = eval(text);
resultsBox.add(CalculationResultButton({ result: fullResult, text: text }));
} catch (e) {
// console.log(e);
}
}
if (isDir) {
var contents = [];
contents = ls({ path: text, silent: true });
contents.forEach((item) => {
resultsBox.add(DirectoryButton(item));
})
}
if (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 (!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
resultsBox.add(SearchButton({ text: entry.text }));
resultsBox.show_all();
}
}]
],
});
return Widget.Box({
vertical: true,
children: [
clickOutsideToClose,
Widget.Box({
hpack: 'center',
children: [
entry,
Widget.Box({
className: 'overview-search-icon-box',
setup: box => box.pack_start(entryPromptRevealer, true, true, 0),
}),
entryIcon,
]
}),
overviewRevealer,
resultsRevealer,
],
connections: [
[App, (_b, name, visible) => {
if (name == 'overview' && !visible) {
entryPromptRevealer.child.label = searchPromptTexts[Math.floor(Math.random() * searchPromptTexts.length)];
resultsBox.children = [];
entry.set_text('');
}
}],
['key-press-event', (widget, event) => { // Typing
if (event.get_keyval()[1] >= 32 && event.get_keyval()[1] <= 126 && widget != entry) {
Utils.timeout(1, () => entry.grab_focus());
entry.set_text(entry.text + String.fromCharCode(event.get_keyval()[1]));
entry.set_position(-1);
}
}],
],
});
};
@@ -0,0 +1,172 @@
const { Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
import { searchItem } from './searchitem.js';
import { execAndClose, startsWithNumber, launchCustomCommand } from './miscfunctions.js';
export const DirectoryButton = ({ parentPath, name, type, icon }) => {
const actionText = Widget.Revealer({
revealChild: false,
transition: "crossfade",
transitionDuration: 200,
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: 300,
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,
setup: (self) => Utils.timeout(1, () => {
const styleContext = self.get_parent().get_style_context();
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
self.size = Math.max(width, height, 1);
})
}),
}),
Widget.Label({
className: 'overview-search-results-txt txt txt-norm',
label: name,
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
]
})
]
}),
connections: [
['focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
}],
['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: 200,
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: 300,
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,
setup: (self) => Utils.timeout(1, () => {
const styleContext = self.get_parent().get_style_context();
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
self.size = Math.max(width, height, 1);
})
}),
}),
Widget.Label({
className: 'overview-search-results-txt txt txt-norm',
label: app.name,
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
]
})
]
}),
connections: [
['focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
}],
['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),
})
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 Google',
actionName: 'Go',
content: `${text}`,
onActivate: () => {
App.closeWindow('overview');
execAsync(['bash', '-c', `xdg-open 'https://www.google.com/search?q=${text} -site:quora.com' &`]).catch(print); // fuck quora
},
});
@@ -0,0 +1,69 @@
const { Gdk, Gtk } = imports.gi;
import { App, Service, Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
import { setupCursorHover, setupCursorHoverAim } from "../../lib/cursorhover.js";
import { MaterialIcon } from '../../lib/materialicon.js';
export const searchItem = ({ materialIconName, name, actionName, content, onActivate }) => {
const actionText = Widget.Revealer({
revealChild: false,
transition: "crossfade",
transitionDuration: 200,
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: 300,
child: actionText,
})
return Widget.Button({
className: 'overview-search-result-btn',
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 txt-smallie txt-subtext',
label: `${name}`,
truncate: "end",
}),
Widget.Label({
hpack: 'start',
className: 'overview-search-results-txt txt txt-norm',
label: `${content}`,
truncate: "end",
}),
]
}),
Widget.Box({ hexpand: true }),
actionTextRevealer,
],
})
]
}),
connections: [
['focus-in-event', (button) => {
actionText.revealChild = true;
actionTextRevealer.revealChild = true;
}],
['focus-out-event', (button) => {
actionText.revealChild = false;
actionTextRevealer.revealChild = false;
}],
]
});
}
+36
View File
@@ -0,0 +1,36 @@
import { Widget } from '../../imports.js';
import { RoundedCorner } from "../../lib/roundedcorner.js";
export const CornerTopleft = () => Widget.Window({
name: 'cornertl',
layer: 'top',
anchor: ['top', 'left'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('topleft', { className: 'corner', }),
});
export const CornerTopright = () => Widget.Window({
name: 'cornertr',
layer: 'top',
anchor: ['top', 'right'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('topright', { className: 'corner', }),
});
export const CornerBottomleft = () => Widget.Window({
name: 'cornerbl',
layer: 'top',
anchor: ['bottom', 'left'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('bottomleft', { className: 'corner-black', }),
});
export const CornerBottomright = () => Widget.Window({
name: 'cornerbr',
layer: 'top',
anchor: ['bottom', 'right'],
exclusivity: 'normal',
visible: true,
child: RoundedCorner('bottomright', { className: 'corner-black', }),
});
+14
View File
@@ -0,0 +1,14 @@
const { Gdk, Gtk } = imports.gi;
import { Widget } from '../../imports.js';
import SessionScreen from "./sessionscreen.js";
export default () => Widget.Window({ // On-screen keyboard
name: 'session',
popup: true,
visible: false,
focusable: true,
layer: 'overlay',
exclusivity: 'ignore',
// anchor: ['top', 'bottom', 'left', 'right'],
child: SessionScreen(),
})
@@ -0,0 +1,142 @@
// 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, Service, Utils, Widget, SCREEN_HEIGHT, SCREEN_WIDTH } from '../../imports.js';
const { exec, execAsync } = Utils;
const SessionButton = (name, icon, command, props = {}) => {
const buttonDescription = Widget.Revealer({
vpack: 'end',
transitionDuration: 200,
transition: 'slide_down',
revealChild: false,
child: Widget.Label({
className: 'txt-smaller session-button-desc',
label: name,
}),
});
return Widget.Button({
onClicked: command,
className: 'session-button',
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;
},
connections: [
['focus-in-event', (self) => {
buttonDescription.revealChild = true;
self.toggleClassName('session-button-focused', true);
}],
['focus-out-event', (self) => {
buttonDescription.revealChild = false;
self.toggleClassName('session-button-focused', false);
}],
],
...props,
});
}
export default () => {
// lock, logout, sleep
const lockButton = SessionButton('Lock', 'lock', () => { App.closeWindow('session'); execAsync('gtklock') });
const logoutButton = SessionButton('Logout', 'logout', () => { App.closeWindow('session'); execAsync(['bash', '-c', 'loginctl terminate-user $USER']) });
const sleepButton = SessionButton('Sleep', 'sleep', () => { App.closeWindow('session'); execAsync('systemctl suspend') });
// hibernate, shutdown, reboot
const hibernateButton = SessionButton('Hibernate', 'downloading', () => { App.closeWindow('session'); execAsync('systemctl hibernate') });
const shutdownButton = SessionButton('Shutdown', 'power_settings_new', () => { App.closeWindow('session'); execAsync('systemctl poweroff') });
const rebootButton = SessionButton('Reboot', 'restart_alt', () => { App.closeWindow('session'); execAsync('systemctl reboot') });
const cancelButton = SessionButton('Cancel', 'close', () => App.closeWindow('session'), { className: 'session-button-cancel' });
return Widget.Box({
className: 'session-bg',
css: `
min-width: ${SCREEN_WIDTH * 1.5}px;
min-height: ${SCREEN_HEIGHT * 1.5}px;
`, // idk why but height = screen height doesn't fill
vertical: true,
children: [
Widget.EventBox({
onPrimaryClick: () => App.closeWindow('session'),
onSecondaryClick: () => App.closeWindow('session'),
onMiddleClick: () => App.closeWindow('session'),
}),
Widget.Box({
hpack: 'center',
vexpand: true,
vertical: true,
children: [
Widget.Box({
vpack: 'center',
vertical: true,
className: 'spacing-v-15',
children: [
Widget.Box({
vertical: true,
css: 'margin-bottom: 0.682rem;',
children: [
Widget.Label({
className: 'txt-title txt',
label: 'Session',
}),
Widget.Label({
justify: Gtk.Justification.CENTER,
className: 'txt-small txt',
label: 'Use arrow keys to navigate.\nEnter to select, Esc to cancel.'
}),
]
}),
Widget.Box({
hpack: 'center',
className: 'spacing-h-15',
children: [ // lock, logout, sleep
lockButton,
logoutButton,
sleepButton,
]
}),
Widget.Box({
hpack: 'center',
className: 'spacing-h-15',
children: [ // hibernate, shutdown, reboot
hibernateButton,
shutdownButton,
rebootButton,
]
}),
Widget.Box({
hpack: 'center',
className: 'spacing-h-15',
children: [ // hibernate, shutdown, reboot
cancelButton,
]
}),
]
})
]
})
],
connections: [
[App, (_b, name, visible) => {
if (visible) lockButton.grab_focus(); // Lock is the default option
}],
],
});
}
@@ -0,0 +1,237 @@
const { Gdk, GLib, Gtk, Pango } = imports.gi;
import { App, Utils, Widget } from '../../../imports.js';
const { Box, Button, Entry, EventBox, Icon, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import ChatGPT from '../../../services/chatgpt.js';
import { MaterialIcon } from "../../../lib/materialicon.js";
import { setupCursorHover, setupCursorHoverInfo } from "../../../lib/cursorhover.js";
import { SystemMessage, ChatMessage } from "./chatgpt_chatmessage.js";
import { ConfigToggle } from '../../../lib/configwidgets.js';
import { markdownTest } from '../../../lib/md2pango.js';
const chatGPTTabIcon = Icon({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
icon: `${App.configDir}/assets/openai-logomark.svg`,
setup: (self) => Utils.timeout(1, () => {
const styleContext = self.get_style_context();
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
self.size = Math.max(width, height, 1) * 116 / 180; // Why such a specific proportion? See https://openai.com/brand#logos
})
});
export const chatGPTInfo = Box({
vertical: true,
className: 'spacing-v-15',
children: [
Icon({
hpack: 'center',
className: 'sidebar-chat-welcome-logo',
icon: `${App.configDir}/assets/openai-logomark.svg`,
setup: (self) => Utils.timeout(1, () => {
const styleContext = self.get_style_context();
const width = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
self.size = Math.max(width, height, 1) * 116 / 180; // Why such a specific proportion? See https://openai.com/brand#logos
})
}),
Label({
className: 'txt txt-title-small sidebar-chat-welcome-txt',
wrap: true,
justify: Gtk.Justification.CENTER,
label: 'ChatGPT',
}),
Box({
className: 'spacing-h-5',
hpack: 'center',
children: [
Label({
className: 'txt-smallie txt-subtext',
wrap: true,
justify: Gtk.Justification.CENTER,
label: 'Powered by OpenAI',
}),
Button({
className: 'txt-subtext txt-norm icon-material',
label: 'info',
tooltipText: 'Uses gpt-3.5-turbo.\nNot affiliated, endorsed, or sponsored by OpenAI.',
setup: setupCursorHoverInfo,
}),
]
}),
]
})
export const chatGPTSettings = Revealer({
transition: 'slide_down',
transitionDuration: 150,
revealChild: true,
connections: [
[ChatGPT, (self) => {
self.revealChild = false;
}, 'newMsg'],
[ChatGPT, (self) => {
self.revealChild = true;
}, 'clear'],
],
child: Box({
vertical: true,
hpack: 'fill',
className: 'sidebar-chat-settings',
children: [
ConfigToggle({
icon: 'cycle',
name: 'Cycle models',
desc: 'Helps avoid exceeding the API rate of 3 messages per minute.\nTurn this on if you message rapidly.',
initValue: ChatGPT.cycleModels,
onChange: (self, newValue) => {
ChatGPT.cycleModels = newValue;
},
}),
ConfigToggle({
icon: 'description',
name: 'Assistant prompt',
desc: 'Tells ChatGPT\n 1. It\'s a sidebar assistant on Linux\n 2. Be short and concise\n 3. Use markdown features extensively\nLeave this off for a vanilla ChatGPT experience.',
initValue: ChatGPT.assistantPrompt,
onChange: (self, newValue) => {
ChatGPT.assistantPrompt = newValue;
},
}),
]
})
});
export const openaiApiKeyInstructions = Box({
homogeneous: true,
children: [Revealer({
transition: 'slide_down',
transitionDuration: 150,
connections: [[ChatGPT, (self, hasKey) => {
self.revealChild = (ChatGPT.key.length == 0);
}, 'hasKey']],
child: Button({
child: Label({
useMarkup: true,
wrap: true,
className: 'txt sidebar-chat-welcome-txt',
justify: Gtk.Justification.CENTER,
label: 'An OpenAI API key is required\nYou can grab one <u>here</u>, then enter it below'
}),
setup: setupCursorHover,
onClicked: () => {
Utils.execAsync(['bash', '-c', `xdg-open https://platform.openai.com/api-keys &`]);
}
})
})]
});
export const chatGPTWelcome = Box({
vexpand: true,
homogeneous: true,
child: Box({
className: 'spacing-v-15',
vpack: 'center',
vertical: true,
children: [
chatGPTInfo,
openaiApiKeyInstructions,
chatGPTSettings,
]
})
});
export const chatContent = Box({
className: 'spacing-v-15',
vertical: true,
connections: [
[ChatGPT, (box, id) => {
const message = ChatGPT.messages[id];
if (!message) return;
box.add(ChatMessage(message))
}, 'newMsg'],
[ChatGPT, (box) => {
box.children = [chatGPTWelcome];
}, 'clear'],
[ChatGPT, (box) => {
box.children = [chatGPTWelcome];
}, 'initialized'],
]
});
export const chatGPTView = Scrollable({
className: 'sidebar-chat-viewport',
vexpand: true,
child: chatContent,
setup: (scrolledWindow) => {
scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = scrolledWindow.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
Utils.timeout(1, () => { // Fix click-to-scroll-widget-to-view behavior
const viewport = scrolledWindow.child;
viewport.set_focus_vadjustment(new Gtk.Adjustment(undefined));
})
}
});
export const chatGPTCommands = Box({
className: 'spacing-h-5',
children: [
Box({ hexpand: true }),
Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => chatContent.add(SystemMessage(
`Key stored in:\n\`${ChatGPT.keyPath}\`\nTo update this key, type \`/key YOUR_API_KEY\``,
'/key')),
setup: setupCursorHover,
label: '/key',
}),
Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => chatContent.add(SystemMessage(
`Currently using \`${ChatGPT.modelName}\``,
'/model'
)),
setup: setupCursorHover,
label: '/model',
}),
Button({
className: 'sidebar-chat-chip sidebar-chat-chip-action txt txt-small',
onClicked: () => ChatGPT.clear(),
setup: setupCursorHover,
label: '/clear',
}),
]
});
export const chatGPTSendMessage = (text) => {
// Check if text or API key is empty
if (text.length == 0) return;
if (ChatGPT.key.length == 0) {
ChatGPT.key = text;
chatContent.add(SystemMessage(`Key saved to\n\`${ChatGPT.keyPath}\``, 'API Key'));
text = '';
return;
}
// Commands
if (text.startsWith('/')) {
if (text.startsWith('/clear')) ChatGPT.clear();
else if (text.startsWith('/model')) chatContent.add(SystemMessage(`Currently using \`${ChatGPT.modelName}\``, '/model'))
else if (text.startsWith('/key')) {
const parts = text.split(' ');
if (parts.length == 1) chatContent.add(SystemMessage(`Key stored in:\n\`${ChatGPT.keyPath}\`\nTo update this key, type \`/key YOUR_API_KEY\``, '/key'));
else {
ChatGPT.key = parts[1];
chatContent.add(SystemMessage(`Updated API Key at\n\`${ChatGPT.keyPath}\``, '/key'));
}
}
else if (text.startsWith('/test'))
chatContent.add(SystemMessage(markdownTest, `Markdown test`));
else
chatContent.add(SystemMessage(`Invalid command.`, 'Error'))
}
else {
ChatGPT.send(text);
}
}
@@ -0,0 +1,291 @@
const { Gdk, Gio, GLib, Gtk, Pango } = imports.gi;
import { App, Utils, Widget } from '../../../imports.js';
const { Box, Button, Entry, EventBox, Icon, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from "../../../lib/materialicon.js";
import { convert } from "../../../lib/md2pango.js";
import GtkSource from "gi://GtkSource?version=3.0";
const CUSTOM_SOURCEVIEW_SCHEME_PATH = `${App.configDir}/data/sourceviewtheme.xml`;
const CUSTOM_SCHEME_ID = 'custom';
const USERNAME = GLib.get_user_name();
const CHATGPT_CURSOR = ' >> ';
const MESSAGE_SCROLL_DELAY = 13; // In milliseconds, the time before an updated message scrolls to bottom
/////////////////////// 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 copyToClipboard(text) {
const clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
const textVariant = new GLib.Variant('s', text);
clipboard.set_text(textVariant, -1);
clipboard.store();
}
function substituteLang(str) {
const subs = [
{ from: 'javascript', to: 'js' },
];
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.WORD
});
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,
});
const CodeBlock = (content = '', lang = 'txt') => {
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',
onClicked: (self) => {
// execAsync(['bash', '-c', `wl-copy '${content}'`, `&`]).catch(print);
execAsync([`wl-copy`, `${sourceView.label}`]).catch(print);
},
child: Box({
className: 'spacing-h-5',
children: [
MaterialIcon('content_copy', 'small'),
Label({
label: 'Copy',
})
]
})
})
]
})
// Source view
const sourceView = HighlightedCode(content, lang);
const codeBlock = Box({
properties: [
['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: [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,
properties: [
['fullUpdate', (self, content, useCursor = false) => {
// Clear and add first text widget
contentBox.get_children().forEach(ch => ch.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)) {
// console.log(`code at line ${index}`);
const kids = self.get_children();
const lastLabel = kids[kids.length - 1];
const blockContent = lines.slice(lastProcessed, index).join('\n');
if (!inCode) {
lastLabel.label = convert(blockContent);
contentBox.add(CodeBlock('', codeBlockRegex.exec(line)[1]));
}
else {
lastLabel._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 = convert(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 = `${convert(blockContent)}${useCursor ? CHATGPT_CURSOR : ''}`;
else
lastLabel._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' + convert(content),
// }))
contentBox.show_all();
}]
]
});
contentBox._fullUpdate(contentBox, content, false);
return contentBox;
}
export const ChatMessage = (message) => {
const messageContentBox = MessageContent(message.content);
const thisMessage = Box({
className: 'sidebar-chat-message',
children: [
Box({
className: `sidebar-chat-indicator ${message.role == 'user' ? 'sidebar-chat-indicator-user' : 'sidebar-chat-indicator-bot'}`,
}),
Box({
vertical: true,
hpack: 'fill',
hexpand: true,
children: [
Label({
hpack: 'fill',
xalign: 0,
className: 'txt txt-bold sidebar-chat-name',
wrap: true,
label: (message.role == 'user' ? USERNAME : 'ChatGPT'),
}),
messageContentBox,
],
connections: [
[message, (self, isThinking) => {
messageContentBox.toggleClassName('thinking', message.thinking);
}, 'notify::thinking'],
[message, (self) => { // Message update
messageContentBox._fullUpdate(messageContentBox, message.content, message.role != 'user');
Utils.timeout(MESSAGE_SCROLL_DELAY, () => {
const scrolledWindow = thisMessage.get_parent().get_parent();
var adjustment = scrolledWindow.get_vadjustment();
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
});
}, 'notify::content'],
[message, (label, isDone) => { // Remove the cursor
messageContentBox._fullUpdate(messageContentBox, message.content, false);
}, 'notify::done'],
]
})
]
});
return thisMessage;
}
export const SystemMessage = (content, commandName) => {
const messageContentBox = MessageContent(content);
const thisMessage = Box({
className: 'sidebar-chat-message',
children: [
Box({
className: `sidebar-chat-indicator sidebar-chat-indicator-System`,
}),
Box({
vertical: true,
hpack: 'fill',
hexpand: true,
children: [
Label({
xalign: 0,
className: 'txt txt-bold sidebar-chat-name',
wrap: true,
label: `System • ${commandName}`,
}),
messageContentBox,
],
})
],
setup: (self) => Utils.timeout(MESSAGE_SCROLL_DELAY, () => {
const scrolledWindow = thisMessage.get_parent().get_parent();
var adjustment = scrolledWindow.get_vadjustment();
adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size());
})
});
return thisMessage;
}
@@ -0,0 +1,88 @@
import { App, Utils, Widget } from '../../imports.js';
const { Box, Button, Entry, EventBox, Icon, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { setupCursorHover, setupCursorHoverInfo } from "../../lib/cursorhover.js";
// APIs
import ChatGPT from '../../services/chatgpt.js';
import { chatGPTView, chatGPTCommands, chatGPTSendMessage } from './apis/chatgpt.js';
const APIS = [
{
name: 'ChatGPT',
sendCommand: chatGPTSendMessage,
contentWidget: chatGPTView,
commandBar: chatGPTCommands,
tabIcon: Box({}),
}
];
let currentApiId = 0;
const apiSwitcher = Box({
vertical: true,
children: [
Box({
homogeneous: true,
children: APIS.map(api => api.tabIcon),
}),
]
})
export const chatEntry = Entry({
className: 'sidebar-chat-entry',
hexpand: true,
connections: [
[ChatGPT, (self) => {
if (APIS[currentApiId].name != 'ChatGPT') return;
self.placeholderText = (ChatGPT.key.length > 0 ? 'Ask a question...' : 'Enter OpenAI API Key...');
}, 'hasKey']
],
onChange: (entry) => {
chatSendButton.toggleClassName('sidebar-chat-send-available', entry.text.length > 0);
},
onAccept: (entry) => {
APIS[currentApiId].sendCommand(entry.text)
entry.text = '';
},
});
const chatSendButton = Button({
className: 'txt-norm icon-material sidebar-chat-send',
vpack: 'center',
label: 'arrow_upward',
setup: setupCursorHover,
onClicked: (self) => {
APIS[currentApiId].sendCommand(chatEntry.text);
chatEntry.text = '';
},
});
const textboxArea = Box({ // Entry area
className: 'sidebar-chat-textarea spacing-h-10',
children: [
chatEntry,
chatSendButton,
]
});
const apiContentStack = Stack({
vexpand: true,
transition: 'slide_left_right',
items: APIS.map(api => [api.name, api.contentWidget]),
})
const apiCommandStack = Stack({
transition: 'slide_up_down',
items: APIS.map(api => [api.name, api.commandBar]),
})
export default Widget.Box({
vertical: true,
className: 'spacing-v-10',
homogeneous: false,
children: [
apiSwitcher,
apiContentStack,
apiCommandStack,
textboxArea,
]
});
+11
View File
@@ -0,0 +1,11 @@
import PopupWindow from '../../lib/popupwindow.js';
import SidebarLeft from "./sideleft.js";
export default () => PopupWindow({
focusable: true,
anchor: ['left', 'top', 'bottom'],
name: 'sideleft',
showClassName: 'sideleft-show',
hideClassName: 'sideleft-hide',
child: SidebarLeft(),
});
+29
View File
@@ -0,0 +1,29 @@
const { Gdk, Gtk } = imports.gi;
import { Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
const { Box, Button, EventBox, Label, Scrollable } = Widget;
export const SidebarModule = ({
name,
child
}) => {
return Box({
className: 'sidebar-module',
vertical: true,
children: [
Button({
child: Box({
children: [
Label({
className: 'txt-small txt',
label: `${name}`,
}),
Label({
className: 'sidebar-module-btn-arrow',
})
]
})
})
]
});
}
@@ -0,0 +1,12 @@
const { Gdk, Gtk } = imports.gi;
import { Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
const { Box, Button, EventBox, Label, Scrollable } = Widget;
import { SidebarModule } from './module.js';
export const QuickScripts = () => SidebarModule({
name: 'Quick scripts',
child: Box({
})
})
+109
View File
@@ -0,0 +1,109 @@
const { Gdk, Gtk } = imports.gi;
import { Utils, Widget } from '../../imports.js';
const { Box, Button, EventBox, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { MaterialIcon } from "../../lib/materialicon.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
import { NavigationIndicator } from "../../lib/navigationindicator.js";
import toolBox from './toolbox.js';
import apiWidgets from './apiwidgets.js';
import { chatEntry } from './apiwidgets.js';
const SidebarTabButton = (stack, stackItem, navIndicator, navIndex, icon, label) => Widget.Button({
// hexpand: true,
className: 'sidebar-selector-tab',
onClicked: (self) => {
stack.shown = stackItem;
// Add active class to self and remove for others
const allTabs = self.get_parent().get_children();
for (let i = 0; i < allTabs.length; i++) {
if (allTabs[i] != self) allTabs[i].toggleClassName('sidebar-selector-tab-active', false);
else self.toggleClassName('sidebar-selector-tab-active', true);
}
// Fancy highlighter line width
const buttonWidth = self.get_allocated_width();
const highlightWidth = self.get_children()[0].get_allocated_width();
navIndicator.css = `
font-size: ${navIndex}px;
padding: 0px ${(buttonWidth - highlightWidth) / 2}px;
`;
},
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
MaterialIcon(icon, 'larger'),
Label({
className: 'txt txt-smallie',
label: label,
})
]
}),
setup: (button) => Utils.timeout(1, () => {
setupCursorHover(button);
button.toggleClassName('sidebar-selector-tab-active', defaultTab === stackItem);
}),
});
const defaultTab = 'apis';
const contentStack = Stack({
vexpand: true,
transition: 'slide_left_right',
items: [
['apis', apiWidgets],
['tools', toolBox],
],
})
const navIndicator = NavigationIndicator(2, false, { // The line thing
className: 'sidebar-selector-highlight',
css: 'font-size: 0px; padding: 0rem 4.773rem;', // Shushhhh
})
const navBar = Box({
vertical: true,
children: [
Box({
homogeneous: true,
children: [
SidebarTabButton(contentStack, 'apis', navIndicator, 0, 'api', 'APIs'),
SidebarTabButton(contentStack, 'tools', navIndicator, 1, 'home_repair_service', 'Tools'),
]
}),
navIndicator,
]
})
export default () => Box({
// vertical: true,
vexpand: true,
hexpand: true,
children: [
EventBox({
onPrimaryClick: () => App.closeWindow('sideleft'),
onSecondaryClick: () => App.closeWindow('sideleft'),
onMiddleClick: () => App.closeWindow('sideleft'),
}),
Box({
vertical: true,
vexpand: true,
className: 'sidebar-left',
children: [
navBar,
contentStack,
]
}),
],
connections: [
['key-press-event', (widget, event) => { // Typing
if (event.get_keyval()[1] >= 32 && event.get_keyval()[1] <= 126 &&
widget != chatEntry && event.get_keyval()[1] != Gdk.KEY_space) {
if (contentStack.shown == 'apis') {
chatEntry.grab_focus();
chatEntry.set_text(chatEntry.text + String.fromCharCode(event.get_keyval()[1]));
chatEntry.set_position(-1);
}
}
}],
],
});
+16
View File
@@ -0,0 +1,16 @@
const { Gdk, Gtk } = imports.gi;
import { Utils, Widget } from '../../imports.js';
const { Box, Button, EventBox, Label, Revealer, Scrollable, Stack } = Widget;
const { execAsync, exec } = Utils;
import { QuickScripts } from './quickscripts.js';
export default Scrollable({
hscroll: "never",
vscroll: "automatic",
child: Box({
vertical: true,
children: [
QuickScripts(),
]
})
});
+204
View File
@@ -0,0 +1,204 @@
const { Gio, Gdk, GLib, Gtk } = imports.gi;
import { App, Widget, Utils } from '../../imports.js';
const { Box, Button, CenterBox, Label, Revealer } = Widget;
import { MaterialIcon } from "../../lib/materialicon.js";
import { getCalendarLayout } from "../../lib/calendarlayout.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
import { TodoWidget } from "./todolist.js";
let calendarJson = getCalendarLayout(undefined, true);
let monthshift = 0;
function fileExists(filePath) {
let file = Gio.File.new_for_path(filePath);
return file.query_exists(null);
}
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: 'Mo', today: 0 },
{ day: 'Tu', today: 0 },
{ day: 'We', today: 0 },
{ day: 'Th', today: 0 },
{ day: 'Fr', today: 0 },
{ day: 'Sa', today: 0 },
{ day: '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) => {
box.children = calendarJson.map((row, i) => Widget.Box({
// homogeneous: true,
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,
items: [
['calendar', CalendarWidget()],
['todo', TodoWidget()],
// ['stars', Widget.Label({ label: 'GitHub feed will be here' })],
],
transition: 'slide_up_down',
transitionDuration: 180,
setup: (stack) => Utils.timeout(1, () => {
stack.shown = defaultShown;
})
})
const StackButton = (stackItemName, icon, name) => Widget.Button({
className: 'button-minsize sidebar-navrail-btn sidebar-button-alone 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', 'Calendar'),
StackButton('todo', 'lists', 'To Do'),
// StackButton(box, 'stars', 'star', 'GitHub'),
]
}), false, false, 0);
box.pack_end(contentStack, false, false, 0);
}
})
+11
View File
@@ -0,0 +1,11 @@
import PopupWindow from '../../lib/popupwindow.js';
import SidebarRight from "./sideright.js";
export default () => PopupWindow({
focusable: true,
anchor: ['right', 'top', 'bottom'],
name: 'sideright',
showClassName: 'sideright-show',
hideClassName: 'sideright-hide',
child: SidebarRight(),
});
@@ -0,0 +1,117 @@
// This file is for the notification widget on the sidebar
// For the popup notifications, see onscreendisplay.js
// The actual widget for each single notification is in lib/notification.js
const { GLib, Gtk } = imports.gi;
import { Service, Utils, Widget } from '../../imports.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
const { lookUpIcon, timeout } = Utils;
const { Box, Icon, Scrollable, Label, Button, Revealer } = Widget;
import { MaterialIcon } from "../../lib/materialicon.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
import Notification from "../../lib/notification.js";
const NotificationList = Box({
vertical: true,
vpack: 'start',
className: 'spacing-v-5-revealer',
connections: [
[Notifications, (box, id) => {
if (box.children.length == 0) {
Notifications.notifications
.forEach(n => {
box.pack_end(Notification({
notifObject: n,
isPopup: false,
}), false, false, 0)
});
box.show_all();
}
else if (id) {
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'],
[Notifications, (box, id) => {
if (!id) return;
for (const ch of box.children) {
if (ch._id === id) {
ch._destroyWithAnims();
}
}
}, 'closed'],
[Notifications, box => box.visible = Notifications.notifications.length > 0],
],
});
export default (props) => {
const listTitle = Revealer({
revealChild: false,
connections: [[Notifications, (revealer) => {
revealer.revealChild = (Notifications.notifications.length > 0);
}]],
child: Box({
vpack: 'start',
className: 'sidebar-group-invisible txt',
children: [
Label({
hexpand: true,
xalign: 0,
className: 'txt-title-small',
label: 'Notifications',
}),
Button({
className: 'notif-closeall-btn',
onClicked: () => {
Notifications.clear();
},
child: Box({
className: 'spacing-h-5',
children: [
MaterialIcon('clear_all', 'norm'),
Label({
className: 'txt-small',
label: 'Clear',
})
]
}),
setup: button => {
setupCursorHover(button);
},
})
]
})
});
const listContents = Scrollable({
hexpand: true,
hscroll: 'never',
vscroll: 'automatic',
child: Box({
vexpand: true,
children: [NotificationList],
})
});
listContents.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = listContents.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
return Box({
...props,
className: 'sidebar-group-invisible spacing-v-5',
vertical: true,
children: [
listTitle,
listContents,
]
});
}
@@ -0,0 +1,194 @@
import { Widget, Utils, Service } from '../../imports.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 "../../lib/statusicons.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
import { MaterialIcon } from '../../lib/materialicon.js';
export const ToggleIconWifi = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Wifi | Right-click to configure',
onClicked: Network.toggleWifi,
onSecondaryClickRelease: () => {
execAsync(['bash', '-c', 'XDG_CURRENT_DESKTOP="gnome" gnome-control-center wifi', '&']);
},
child: NetworkIndicator(),
connections: [
[Network, button => {
button.toggleClassName('sidebar-button-active', Network.wifi?.internet == 'connected' || Network.wired?.internet == 'connected')
}],
[Network, button => {
button.tooltipText = (`${Network.wifi?.ssid} | Right-click to configure` || 'Unknown');
}],
],
setup: setupCursorHover,
...props,
});
export const ToggleIconBluetooth = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Bluetooth | Right-click to configure',
onClicked: () => { // Provided service doesn't work hmmm
const status = Bluetooth?.enabled;
if (status) {
exec('rfkill block bluetooth');
}
else {
exec('rfkill unblock bluetooth');
}
},
onSecondaryClickRelease: () => {
execAsync(['bash', '-c', 'XDG_CURRENT_DESKTOP="gnome" gnome-control-center bluetooth', '&']);
},
child: BluetoothIndicator(),
connections: [
[Bluetooth, button => {
button.toggleClassName('sidebar-button-active', Bluetooth?.enabled)
}],
],
setup: setupCursorHover,
...props,
});
export const HyprToggleIcon = (icon, name, hyprlandConfigValue, props = {}) => 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,
})
export const ModuleNightLight = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Night Light',
onClicked: (button) => {
const shaderPath = JSON.parse(exec('hyprctl -j getoption decoration:screen_shader')).str;
if (shaderPath != "[[EMPTY]]" && shaderPath != "") {
execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader ''`]).catch(print);
button.toggleClassName('sidebar-button-active', false);
}
else {
execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader ~/.config/hypr/shaders/extradark.frag`]).catch(print);
button.toggleClassName('sidebar-button-active', true);
}
},
child: MaterialIcon('nightlight', 'norm'),
setup: setupCursorHover,
...props,
})
export const ModuleInvertColors = (props = {}) => Widget.Button({
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Color inversion',
onClicked: (button) => {
const shaderPath = JSON.parse(exec('hyprctl -j getoption decoration:screen_shader')).str;
if (shaderPath != "[[EMPTY]]" && shaderPath != "") {
execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader ''`]).catch(print);
button.toggleClassName('sidebar-button-active', false);
}
else {
execAsync(['bash', '-c', `hyprctl keyword decoration:screen_shader ~/.config/hypr/shaders/invert.frag`]).catch(print);
button.toggleClassName('sidebar-button-active', true);
}
},
child: MaterialIcon('invert_colors', 'norm'),
setup: setupCursorHover,
...props,
})
export const ModuleIdleInhibitor = (props = {}) => Widget.Button({ // TODO: Make this work
properties: [
['enabled', false],
['inhibitor', undefined],
],
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Keep system awake',
onClicked: (self) => {
self._enabled = !self._enabled;
self.toggleClassName('sidebar-button-active', self._enabled);
if (self._enabled) {
self._inhibitor = Utils.subprocess(
['wayland-idle-inhibitor.py'],
(output) => print(output),
(err) => logError(err),
self,
);
}
else {
self._inhibitor.force_exit();
}
},
child: MaterialIcon('coffee', 'norm'),
setup: setupCursorHover,
...props,
});
export const ModuleEditIcon = (props = {}) => Widget.Button({ // TODO: Make this work
...props,
className: 'txt-small sidebar-iconbutton',
onClicked: () => {
execAsync(['bash', '-c', 'XDG_CURRENT_DESKTOP="gnome" gnome-control-center', '&']);
App.toggleWindow('sideright');
},
child: MaterialIcon('edit', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
export const ModuleReloadIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Reload Hyprland',
onClicked: () => {
execAsync(['bash', '-c', 'hyprctl reload &']);
App.toggleWindow('sideright');
},
child: MaterialIcon('refresh', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
export const ModuleSettingsIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Open Settings',
onClicked: () => {
execAsync(['bash', '-c', 'XDG_CURRENT_DESKTOP="gnome" gnome-control-center', '&']);
App.toggleWindow('sideright');
},
child: MaterialIcon('settings', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
export const ModulePowerIcon = (props = {}) => Widget.Button({
...props,
className: 'txt-small sidebar-iconbutton',
tooltipText: 'Session',
onClicked: () => {
App.toggleWindow('session');
App.closeWindow('sideright');
},
child: MaterialIcon('power_settings_new', 'norm'),
setup: button => {
setupCursorHover(button);
}
})
+109
View File
@@ -0,0 +1,109 @@
const { GLib, Gdk, Gtk } = imports.gi;
import { Utils, Widget } from '../../imports.js';
const { execAsync, exec } = Utils;
const { Box, EventBox } = Widget;
import {
ToggleIconBluetooth,
ToggleIconWifi,
HyprToggleIcon,
ModuleNightLight,
ModuleInvertColors,
ModuleIdleInhibitor,
ModuleEditIcon,
ModuleReloadIcon,
ModuleSettingsIcon,
ModulePowerIcon
} from "./quicktoggles.js";
import ModuleNotificationList from "./notificationlist.js";
import { ModuleCalendar } from "./calendar.js";
// const NUM_OF_TOGGLES_PER_LINE = 5;
// const togglesFlowBox = Widget.FlowBox({
// className: 'sidebar-group spacing-h-10',
// setup: (self) => {
// self.set_max_children_per_line(NUM_OF_TOGGLES_PER_LINE);
// self.add(ToggleIconWifi({ hexpand: true }));
// self.add(ToggleIconBluetooth({ hexpand: true }));
// self.add(HyprToggleIcon('mouse', 'Raw input', 'input:force_no_accel', { hexpand: true }));
// self.add(HyprToggleIcon('front_hand', 'No touchpad while typing', 'input:touchpad:disable_while_typing', { hexpand: true }));
// self.add(ModuleNightLight({ hexpand: true }));
// // Setup flowbox rearrange
// self.connect('child-activated', (self, child) => {
// if (child.get_index() === 0) {
// self.reorder_child(child, self.get_children().length - 1);
// } else {
// self.reorder_child(child, 0);
// }
// });
// }
// })
const timeRow = Box({
className: 'spacing-h-5 sidebar-group-invisible-morehorizpad',
children: [
Widget.Label({
className: 'txt-title txt',
connections: [[5000, label => {
label.label = GLib.DateTime.new_now_local().format("%H:%M");
}]],
}),
Widget.Label({
hpack: 'center',
className: 'txt-small txt',
connections: [[5000, label => {
execAsync(['bash', '-c', `uptime -p | sed -e 's/up //;s/ hours,/h/;s/ minutes/m/'`]).then(upTimeString => {
label.label = `• up: ${upTimeString}`;
}).catch(print);
}]],
}),
Widget.Box({ hexpand: true }),
// ModuleEditIcon({ hpack: 'end' }), // TODO: Make this work
ModuleReloadIcon({ hpack: 'end' }),
ModuleSettingsIcon({ hpack: 'end' }),
ModulePowerIcon({ hpack: 'end' }),
]
});
const togglesBox = Widget.Box({
hpack: 'center',
className: 'sidebar-togglesbox spacing-h-10',
children: [
ToggleIconWifi(),
ToggleIconBluetooth(),
HyprToggleIcon('mouse', 'Raw input', 'input:force_no_accel', {}),
HyprToggleIcon('front_hand', 'No touchpad while typing', 'input:touchpad:disable_while_typing', {}),
ModuleNightLight(),
ModuleInvertColors(),
ModuleIdleInhibitor(),
]
})
export default () => Box({
vexpand: true,
hexpand: true,
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,
// togglesFlowBox,
togglesBox,
]
}),
ModuleNotificationList({ vexpand: true, }),
ModuleCalendar(),
]
}),
]
});
+276
View File
@@ -0,0 +1,276 @@
const { Gio, Gdk, GLib, Gtk } = imports.gi;
import { App, Widget, Utils } from '../../imports.js';
const { Box, Button, CenterBox, Label, Revealer } = Widget;
import { MaterialIcon } from "../../lib/materialicon.js";
import Todo from "../../services/todo.js";
import { setupCursorHover } from "../../lib/cursorhover.js";
import { NavigationIndicator } from "../../lib/navigationindicator.js";
const defaultTodoSelected = 'undone';
const todoListItem = (task, id, isDone, isEven = false) => {
const crosser = Widget.Box({
className: 'sidebar-todo-crosser',
});
const todoContent = Widget.Box({
className: 'sidebar-todo-item spacing-h-5',
children: [
Widget.Label({
hexpand: true,
xalign: 0,
wrap: true,
className: 'txt txt-small sidebar-todo-txt',
label: task.content,
selectable: true,
}),
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,
}),
crosser,
]
});
const widgetRevealer = Widget.Revealer({
revealChild: true,
transition: 'slide_down',
transitionDuration: 150,
child: todoContent,
})
return widgetRevealer;
}
const todoItems = (isDone) => Widget.Scrollable({
child: Widget.Box({
vertical: true,
connections: [[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',
children: [
MaterialIcon(`${isDone ? 'checklist' : 'check_circle'}`, 'badonkers'),
Label({ label: `${isDone ? 'Finished tasks will go here' : 'Nothing here!'}` })
]
})
]
}
else self.homogeneous = false;
}, 'updated']]
}),
setup: (listContents) => {
listContents.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
const vScrollbar = listContents.get_vscrollbar();
vScrollbar.get_style_context().add_class('sidebar-scrollbar');
}
});
const UndoneTodoList = () => {
const newTaskButton = Revealer({
transition: 'slide_left',
transitionDuration: 200,
revealChild: true,
child: Button({
className: 'txt-small sidebar-todo-new',
halign: 'end',
vpack: 'center',
label: '+ 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: 200,
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: '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: 200,
revealChild: false,
child: newTaskEntry,
});
const confirmAddTask = Revealer({
transition: 'slide_right',
transitionDuration: 200,
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);
},
});
}
const todoItemsBox = Widget.Stack({
vpack: 'fill',
transition: 'slide_left_right',
items: [
['undone', UndoneTodoList()],
['done', todoItems(true)],
],
});
export const TodoWidget = () => {
const TodoTabButton = (isDone, navIndex) => Widget.Button({
hexpand: true,
className: 'sidebar-selector-tab',
onClicked: (button) => {
todoItemsBox.shown = `${isDone ? 'done' : 'undone'}`;
const kids = button.get_parent().get_children();
for (let i = 0; i < kids.length; i++) {
if (kids[i] != button) kids[i].toggleClassName('sidebar-selector-tab-active', false);
else button.toggleClassName('sidebar-selector-tab-active', true);
}
// Fancy highlighter line width
const buttonWidth = button.get_allocated_width();
const highlightWidth = button.get_children()[0].get_allocated_width();
navIndicator.css = `
font-size: ${navIndex}px;
padding: 0px ${(buttonWidth - highlightWidth) / 2}px;
`;
},
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
MaterialIcon(`${isDone ? 'task_alt' : 'format_list_bulleted'}`, 'larger'),
Label({
className: 'txt txt-smallie',
label: `${isDone ? 'Done' : 'Unfinished'}`,
})
]
}),
setup: (button) => Utils.timeout(1, () => {
setupCursorHover(button);
button.toggleClassName('sidebar-selector-tab-active', defaultTodoSelected === `${isDone ? 'done' : 'undone'}`);
}),
});
const undoneButton = TodoTabButton(false, 0);
const doneButton = TodoTabButton(true, 1);
const navIndicator = NavigationIndicator(2, false, { // The line thing
className: 'sidebar-selector-highlight',
css: 'font-size: 0px; padding: 0rem 1.636rem;', // Shush
})
return Widget.Box({
hexpand: true,
vertical: true,
className: 'spacing-v-10',
setup: (box) => { // undone/done selector rail
box.pack_start(Widget.Box({
vertical: true,
children: [
Widget.Box({
className: 'sidebar-selectors spacing-h-5',
homogeneous: true,
setup: (box) => {
box.pack_start(undoneButton, false, true, 0);
box.pack_start(doneButton, false, true, 0);
}
}),
Widget.Box({
className: 'sidebar-selector-highlight-offset',
homogeneous: true,
children: [navIndicator]
})
]
}), false, false, 0);
box.pack_end(todoItemsBox, true, true, 0);
},
});
};