update ags
This commit is contained in:
1 parent
88822cb8e8
commit
a9c19bd2e1
134 files changed
+4732
-5813
No files matched your search
@@ -0,0 +1,179 @@
|
||||
|
||||
const { query } = await Service.import("applications")
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
const Gio = imports.gi.Gio;
|
||||
const { Gtk } = imports.gi;
|
||||
const GLib = imports.gi.GLib;
|
||||
const WINDOW_NAME = "applauncher"
|
||||
import Box from 'types/widgets/box';
|
||||
import popupwindow from './misc/popupwindow.ts';
|
||||
import { Application } from 'types/service/applications';
|
||||
|
||||
|
||||
const LAUNCH_COUNT_FILE = Gio.File.new_for_path(
|
||||
GLib.build_filenamev([GLib.get_home_dir(), '.cache', 'launch_counts.json'])
|
||||
);
|
||||
|
||||
function readLaunchCounts() {
|
||||
try {
|
||||
if (!LAUNCH_COUNT_FILE.query_exists(null)) {
|
||||
return {};
|
||||
}
|
||||
const [, contents] = LAUNCH_COUNT_FILE.load_contents(null);
|
||||
return JSON.parse(decoder.decode(contents));
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLaunchCounts(launchCounts: number) {
|
||||
const data = JSON.stringify(launchCounts, null, 2);
|
||||
LAUNCH_COUNT_FILE.replace_contents(
|
||||
encoder.encode(data),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.REPLACE_DESTINATION,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function incrementLaunchCount(appName: string) {
|
||||
const launchCounts = readLaunchCounts();
|
||||
launchCounts[appName] = (launchCounts[appName] || 0) + 1;
|
||||
writeLaunchCounts(launchCounts);
|
||||
}
|
||||
|
||||
function sortApplicationsByLaunchCount(applications: Box<any, any>[]) {
|
||||
const launchCounts = readLaunchCounts()
|
||||
return applications.sort((a: Box<any, any>, b: Box<any, any>) => {
|
||||
const countA = launchCounts[a.attribute.app.name] || 0
|
||||
const countB = launchCounts[b.attribute.app.name] || 0
|
||||
return countB - countA
|
||||
})
|
||||
}
|
||||
|
||||
function AppItem(app: Application): Box<any, any> {
|
||||
let clickCount = 0;
|
||||
const button = Widget.Button({
|
||||
class_name: "application_container",
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: "application_icon",
|
||||
// @ts-ignore
|
||||
icon: Utils.lookUpIcon(app.icon_name) ? app.icon_name : "image-missing",
|
||||
size: 42,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "application_label",
|
||||
label: app.name,
|
||||
xalign: 0,
|
||||
vpack: "center",
|
||||
truncate: "end",
|
||||
}),
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
incrementLaunchCount(app.name);
|
||||
App.closeWindow(WINDOW_NAME);
|
||||
app.launch();
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return Widget.Box({
|
||||
attribute: { app },
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
button,
|
||||
Widget.Separator({
|
||||
class_name: "application_divider",
|
||||
orientation: Gtk.Orientation.HORIZONTAL
|
||||
})
|
||||
],
|
||||
})
|
||||
};
|
||||
|
||||
const Applauncher = ({ width = 500, height = 500, spacing = 12 }) => {
|
||||
let applications: Box<any, any>[];
|
||||
|
||||
const list = Widget.Box({
|
||||
vertical: true,
|
||||
spacing,
|
||||
})
|
||||
|
||||
function repopulate() {
|
||||
applications = query("").map(AppItem)
|
||||
applications = sortApplicationsByLaunchCount(applications)
|
||||
list.children = applications
|
||||
}
|
||||
repopulate()
|
||||
|
||||
const entry = Widget.Entry({
|
||||
hexpand: true,
|
||||
class_name: "applauncher_entry",
|
||||
placeholder_text: "Search",
|
||||
|
||||
on_accept: () => {
|
||||
const results = applications.filter((item) => item.visible)
|
||||
if (results[0]) {
|
||||
App.toggleWindow(WINDOW_NAME)
|
||||
results[0].attribute.app.launch()
|
||||
}
|
||||
},
|
||||
|
||||
on_change: ({ text }) => applications.forEach(item => {
|
||||
item.visible = item.attribute.app.match(text ?? "")
|
||||
}),
|
||||
})
|
||||
|
||||
return Widget.Box({
|
||||
vertical: true,
|
||||
css: `margin: ${spacing * 2}px;`,
|
||||
class_name: "applauncher_box",
|
||||
margin_top: 14,
|
||||
margin_left: 14,
|
||||
children: [
|
||||
entry,
|
||||
|
||||
Widget.Scrollable({
|
||||
hscroll: "never",
|
||||
css: `min-width: ${width}px;`
|
||||
+ `min-height: ${height}px;`,
|
||||
child: list,
|
||||
}),
|
||||
],
|
||||
setup: self => self.hook(App, (_, windowName, visible) => {
|
||||
if (windowName !== WINDOW_NAME)
|
||||
return
|
||||
|
||||
if (visible) {
|
||||
repopulate()
|
||||
entry.text = ""
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export const applauncher = popupwindow({
|
||||
name: WINDOW_NAME,
|
||||
|
||||
class_name: "applauncher",
|
||||
visible: false,
|
||||
keymode: "exclusive",
|
||||
child: Applauncher({
|
||||
width: 500,
|
||||
height: 500,
|
||||
spacing: 0,
|
||||
}),
|
||||
anchor: ["bottom", "left"]
|
||||
})
|
||||
@@ -0,0 +1,451 @@
|
||||
const hyprland = await Service.import("hyprland")
|
||||
const battery = await Service.import("battery")
|
||||
const systemtray = await Service.import("systemtray")
|
||||
const audio = await Service.import("audio")
|
||||
const network = await Service.import("network")
|
||||
const { Gtk, GLib, Gio } = imports.gi;
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
import { OpenSettings } from "apps/settings/main.ts";
|
||||
import { enableClickThrough } from "./misc/clickthrough.js";
|
||||
import { RoundedCorner } from "./misc/cairo_roundedcorner.js";
|
||||
import { Client, Workspace } from "types/service/hyprland.js"
|
||||
import Button from "types/widgets/button.js"
|
||||
import Icon from "types/widgets/icon.js"
|
||||
import { FileEnumerator, FileInfo } from "types/@girs/gio-2.0/gio-2.0.cjs"
|
||||
const mpris = await Service.import("mpris")
|
||||
|
||||
|
||||
function checkKeymap() {
|
||||
const layout = Utils.execAsync(`hyprctl devices -j | jq -r '.keyboards[] | select(.main == true) | .active_keymap' | tail -n1 | cut -c1-2 | tr 'A-Z' 'a-z'`)
|
||||
.then(out => { return out })
|
||||
.catch(err => { print(err); return "en" });
|
||||
return layout;
|
||||
}
|
||||
|
||||
const keyboard_layout = Variable("en")
|
||||
keyboard_layout.setValue(await checkKeymap())
|
||||
hyprland.connect("keyboard-layout", (hyprland, keyboardname, layoutname) => {
|
||||
keyboard_layout.setValue(layoutname.trim().toLowerCase().substr(0, 2))
|
||||
})
|
||||
|
||||
const time = Variable("", {
|
||||
poll: [1000, 'date "+%I:%M %p"'],
|
||||
})
|
||||
|
||||
const date = Variable("", {
|
||||
poll: [1000, 'date "+%Y-%m-%d"'],
|
||||
})
|
||||
|
||||
|
||||
function getIconNameFromClass(windowClass: string) {
|
||||
let formattedClass = windowClass.replace(/\s+/g, '-').toLowerCase();
|
||||
let homeDir = GLib.get_home_dir();
|
||||
let systemDataDirs = GLib.get_system_data_dirs().map(dir => dir + '/applications');
|
||||
let dataDirs = systemDataDirs.concat([homeDir + '/.local/share/applications']);
|
||||
let icon: string | undefined;
|
||||
|
||||
for (let dir of dataDirs) {
|
||||
let applicationsGFile = Gio.File.new_for_path(dir);
|
||||
|
||||
let enumerator: FileEnumerator;
|
||||
try {
|
||||
enumerator = applicationsGFile.enumerate_children('standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fileInfo: FileInfo | null;
|
||||
while ((fileInfo = enumerator.next_file(null)) !== null) {
|
||||
let desktopFile = fileInfo.get_name();
|
||||
if (desktopFile.endsWith('.desktop')) {
|
||||
let fileContents = GLib.file_get_contents(dir + '/' + desktopFile);
|
||||
let matches = /Icon=(\S+)/.exec(decoder.decode(fileContents[1]));
|
||||
if (matches && matches[1]) {
|
||||
if (desktopFile.toLowerCase().includes(formattedClass)) {
|
||||
icon = matches[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enumerator.close(null);
|
||||
if (icon) break;
|
||||
}
|
||||
return Utils.lookUpIcon(icon) ? icon : "image-missing";
|
||||
}
|
||||
|
||||
const dispatch = (ws: string) => hyprland.messageAsync(`dispatch workspace ${ws}`).catch(print);
|
||||
|
||||
|
||||
function Workspaces() {
|
||||
const activeId = hyprland.active.workspace.bind("id");
|
||||
let workspaceButtons = new Map();
|
||||
|
||||
function createWorkspaceButton(id: Number) {
|
||||
const button = Widget.Button({
|
||||
on_clicked: () => dispatch(`${id}`),
|
||||
child: Widget.Label(`${id}`),
|
||||
class_name: activeId.as(i => `${i === id ? "active" : ""}`),
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function updateWorkspaceButtons(workspaces: Workspace[]): Array<Button<any, any>> {
|
||||
workspaces.sort((a, b) => a.id - b.id);
|
||||
|
||||
const updatedButtons = new Map();
|
||||
|
||||
workspaces.forEach(({ id }) => {
|
||||
if (workspaceButtons.has(id)) {
|
||||
updatedButtons.set(id, workspaceButtons.get(id));
|
||||
} else {
|
||||
updatedButtons.set(id, createWorkspaceButton(id));
|
||||
}
|
||||
});
|
||||
if (workspaceButtons != updatedButtons)
|
||||
workspaceButtons = updatedButtons;
|
||||
|
||||
return Array.from(workspaceButtons.values());
|
||||
}
|
||||
|
||||
const workspaceButtonsArray = hyprland.bind("workspaces").as(updateWorkspaceButtons);
|
||||
|
||||
return Widget.EventBox({
|
||||
onScrollUp: () => dispatch('+1'),
|
||||
onScrollDown: () => dispatch('-1'),
|
||||
child: Widget.Box({
|
||||
children: workspaceButtonsArray,
|
||||
class_name: "workspaces",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function Clock() {
|
||||
return Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "clock",
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: "time",
|
||||
label: time.bind(),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "date",
|
||||
label: date.bind(),
|
||||
})
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function BatteryLabel() {
|
||||
const icon = battery.bind("icon_name");
|
||||
|
||||
const isVisible = battery.bind("percent").as(p => p < 100);
|
||||
|
||||
return Widget.Box({
|
||||
class_name: "battery",
|
||||
visible: isVisible,
|
||||
children: [
|
||||
Widget.Icon({ icon }),
|
||||
Widget.Label({
|
||||
label: battery.bind("percent").as(p => `${p > 0 ? p : 0}%`),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function SysTray() {
|
||||
const items = systemtray.bind("items")
|
||||
.as(items => items.map(item => {
|
||||
if (item.id.trim() != "nm-applet") {
|
||||
return Widget.Button({
|
||||
child: Widget.Icon({ icon: item.bind("icon") }),
|
||||
on_primary_click: (_, event) => item.activate(event),
|
||||
on_secondary_click: (_, event) => item.openMenu(event),
|
||||
tooltip_markup: item.bind("tooltip_markup"),
|
||||
})
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}))
|
||||
|
||||
// @ts-ignore
|
||||
return Widget.Box({
|
||||
class_name: "tray",
|
||||
spacing: 5,
|
||||
children: items,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function AppLauncher() {
|
||||
const button = Widget.Button({
|
||||
class_name: "filled_tonal_button awesome_icon",
|
||||
on_clicked: () => {
|
||||
App.toggleWindow("applauncher")
|
||||
},
|
||||
child: Widget.Label(
|
||||
"\uf002"
|
||||
)
|
||||
})
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function Wifi() {
|
||||
const button = Widget.Button({
|
||||
class_name: "bar_wifi awesome_icon",
|
||||
on_primary_click: () => {
|
||||
OpenSettings("network")
|
||||
},
|
||||
on_secondary_click: (_, event) => {
|
||||
const nm_applet = systemtray.items.find(item => item.id == "nm-applet")
|
||||
if (nm_applet) {
|
||||
nm_applet.openMenu(event)
|
||||
} else {
|
||||
Utils.execAsync("nm-connection-editor").catch(print)
|
||||
}
|
||||
},
|
||||
child: Widget.Icon({
|
||||
icon: "network-wireless-offline-symbolic"
|
||||
}),
|
||||
tooltip_text: 'Disabled'
|
||||
}).hook(network, self => {
|
||||
if (network.wifi.enabled) {
|
||||
self.tooltip_text = network.wifi.ssid || 'Unknown';
|
||||
self.child.icon = network.wifi.icon_name;
|
||||
} else {
|
||||
self.tooltip_text = 'Disabled'
|
||||
self.child.icon = "network-wireless-offline-symbolic";
|
||||
}
|
||||
})
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function MediaPlayer() {
|
||||
let metadata = mpris.players[0]?.metadata;
|
||||
const button = Widget.Button({
|
||||
class_name: "filled_tonal_button awesome_icon",
|
||||
on_primary_click: () => {
|
||||
App.toggleWindow("media")
|
||||
// Utils.execAsync(["ags", "-t", "media"])
|
||||
},
|
||||
on_secondary_click: () => {
|
||||
Utils.execAsync(["playerctl", "play-pause"]).catch(print)
|
||||
},
|
||||
child: Widget.Label(
|
||||
""
|
||||
),
|
||||
visible: false,
|
||||
tooltip_text: "Unknown"
|
||||
}).hook(mpris, self => {
|
||||
if (mpris.players.length > 0) {
|
||||
self.visible = true;
|
||||
metadata = mpris.players[0]?.metadata;
|
||||
if (metadata)
|
||||
self.tooltip_text = `${metadata["xesam:artist"]} - ${metadata["xesam:title"]}`;
|
||||
} else {
|
||||
self.visible = false;
|
||||
self.tooltip_text = "Unknown"
|
||||
App.closeWindow("media")
|
||||
}
|
||||
})
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function KeyboardLayout() {
|
||||
const widget = Widget.Label({
|
||||
class_name: "keyboard",
|
||||
label: keyboard_layout.bind()
|
||||
})
|
||||
return widget
|
||||
}
|
||||
|
||||
|
||||
function OpenSideBar() {
|
||||
const button = Widget.Button({
|
||||
class_name: "filled_tonal_button awesome_icon",
|
||||
on_clicked: () => {
|
||||
App.toggleWindow("sidebar")
|
||||
// Utils.execAsync(["ags", "-t", "sidebar"])
|
||||
},
|
||||
label: ""
|
||||
})
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
function TaskBar() {
|
||||
let globalWidgets: Button<Icon<any>, any>[] = [];
|
||||
|
||||
function Clients(clients: Client[]) {
|
||||
const currentClientIds = clients.map(client => client.pid);
|
||||
globalWidgets = globalWidgets.filter(widget => currentClientIds.includes(widget.attribute.pid));
|
||||
|
||||
clients.forEach(item => {
|
||||
let widget = globalWidgets.find(w => w.attribute.pid === item.pid);
|
||||
//if (item.class == "kitty") {
|
||||
// return;
|
||||
//}
|
||||
if (widget) {
|
||||
widget.tooltip_markup = item.title;
|
||||
} else {
|
||||
let icon: string | undefined;
|
||||
if (item.class == "com.github.Aylur.ags") {
|
||||
if (item.initialTitle == "Settings") {
|
||||
icon = "emblem-system-symbolic"
|
||||
}
|
||||
}
|
||||
else
|
||||
icon = getIconNameFromClass(item.class)
|
||||
widget = Widget.Button({
|
||||
attribute: { pid: item.pid },
|
||||
child: Widget.Icon({ icon: icon }),
|
||||
tooltip_markup: item.title,
|
||||
});
|
||||
globalWidgets.push(widget);
|
||||
}
|
||||
});
|
||||
return globalWidgets;
|
||||
}
|
||||
|
||||
return Widget.Box({
|
||||
class_name: "tray",
|
||||
spacing: 5,
|
||||
children: hyprland.bind("clients").as(Clients),
|
||||
})
|
||||
}
|
||||
|
||||
function volumeIndicator() {
|
||||
return Widget.EventBox({
|
||||
onScrollUp: () => audio.speaker.volume += 0.01,
|
||||
onScrollDown: () => audio.speaker.volume -= 0.01,
|
||||
class_name: "filled_tonal_button volume_box",
|
||||
child: Widget.Button({
|
||||
on_primary_click: () => Utils.execAsync("pavucontrol").catch(print),
|
||||
on_secondary_click: () => audio.speaker.is_muted = !audio.speaker.is_muted,
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Icon().hook(audio.speaker, self => {
|
||||
const vol = audio.speaker.volume * 100;
|
||||
const icon = [
|
||||
[101, 'overamplified'],
|
||||
[67, 'high'],
|
||||
[34, 'medium'],
|
||||
[1, 'low'],
|
||||
[0, 'muted'],
|
||||
].find(([threshold]) => Number(threshold) <= vol)?.[1];
|
||||
if (audio.speaker.is_muted)
|
||||
self.icon = `audio-volume-muted-symbolic`;
|
||||
else
|
||||
self.icon = `audio-volume-${icon}-symbolic`;
|
||||
self.tooltip_text = `Volume ${Math.floor(vol)}%`;
|
||||
}),
|
||||
Widget.Label({
|
||||
label: audio.speaker.bind("volume").as(volume => `${Math.floor(volume * 100)}%`),
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Left() {
|
||||
return Widget.Box({
|
||||
// margin_left: 15,
|
||||
class_name: "modules-left",
|
||||
hpack: "start",
|
||||
spacing: 8,
|
||||
children: [
|
||||
AppLauncher(),
|
||||
MediaPlayer(),
|
||||
TaskBar()
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Center() {
|
||||
return Widget.Box({
|
||||
class_name: "modules-center",
|
||||
hpack: "center",
|
||||
spacing: 8,
|
||||
children: [
|
||||
Workspaces(),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Right() {
|
||||
return Widget.Box({
|
||||
// margin_right: 15,
|
||||
class_name: "modules-right",
|
||||
hpack: "end",
|
||||
spacing: 8,
|
||||
children: [
|
||||
volumeIndicator(),
|
||||
KeyboardLayout(),
|
||||
BatteryLabel(),
|
||||
SysTray(),
|
||||
Wifi(),
|
||||
Clock(),
|
||||
OpenSideBar()
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const Bar = async (monitor = 0) => {
|
||||
return Widget.Window({
|
||||
name: `bar-${monitor}`,
|
||||
class_name: "bar",
|
||||
monitor,
|
||||
//anchor: ["top", "left", "right"],
|
||||
anchor: ["bottom", "left", "right"],
|
||||
exclusivity: "exclusive",
|
||||
child: Widget.CenterBox({
|
||||
start_widget: Left(),
|
||||
center_widget: Center(),
|
||||
end_widget: Right(),
|
||||
}),
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
export const BarCornerTopLeft = (monitor = 0) => Widget.Window({
|
||||
monitor,
|
||||
name: `bar_corner_tl${monitor}`,
|
||||
class_name: "transparent",
|
||||
layer: 'top',
|
||||
anchor: ['top', 'left'],
|
||||
exclusivity: 'normal',
|
||||
visible: true,
|
||||
child: RoundedCorner('top_left', { className: 'corner', }),
|
||||
setup: enableClickThrough,
|
||||
});
|
||||
|
||||
export const BarCornerTopRight = (monitor = 0) => Widget.Window({
|
||||
monitor,
|
||||
name: `bar_corner_tr${monitor}`,
|
||||
class_name: "transparent",
|
||||
layer: 'top',
|
||||
anchor: ['top', 'right'],
|
||||
exclusivity: 'normal',
|
||||
visible: true,
|
||||
child: RoundedCorner('top_right', { className: 'corner', }),
|
||||
setup: enableClickThrough,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
const { GLib } = imports.gi;
|
||||
const WINDOW_NAME = "cliphist"
|
||||
import popupwindow from './misc/popupwindow.ts';
|
||||
import Box from 'types/widgets/box.js';
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
type EntryObject = {
|
||||
id: string;
|
||||
content: string;
|
||||
entry: string;
|
||||
};
|
||||
|
||||
function ClipHistItem(entry: string) {
|
||||
let [id, ...content] = entry.split('\t');
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
class_name: "entry_container",
|
||||
child: Widget.Label({
|
||||
label: entry,
|
||||
class_name: "entry_label",
|
||||
xalign: 0,
|
||||
vpack: "center",
|
||||
truncate: "end",
|
||||
})
|
||||
});
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 1) {
|
||||
App.closeWindow(WINDOW_NAME);
|
||||
Utils.execAsync(`${App.configDir}/scripts/cliphist.sh --copy-by-id ${id}`);
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return Widget.Box({
|
||||
attribute: { content: content.join(' ').trim() },
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
button,
|
||||
Widget.Separator({
|
||||
class_name: "entry_divider",
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function ClipHistWidget({ width = 500, height = 500, spacing = 12 }) {
|
||||
let output: string;
|
||||
let entries: string[];
|
||||
let clipHistItems: EntryObject[];
|
||||
let widgets: Box<any, any>[];
|
||||
|
||||
const list = Widget.Box({
|
||||
vertical: true,
|
||||
spacing,
|
||||
});
|
||||
|
||||
async function repopulate() {
|
||||
output = await Utils.execAsync(`${App.configDir}/scripts/cliphist.sh --get`)
|
||||
.then(str => str)
|
||||
.catch(err => {
|
||||
print(err);
|
||||
return "";
|
||||
});
|
||||
entries = output.split('\n').filter(line => line.trim() !== '');
|
||||
clipHistItems = entries.map(entry => {
|
||||
let [id, ...content] = entry.split('\t');
|
||||
return { id: id.trim(), content: content.join(' ').trim(), entry: entry };
|
||||
});
|
||||
widgets = clipHistItems.map(item => ClipHistItem(item.entry));
|
||||
list.children = widgets;
|
||||
}
|
||||
repopulate();
|
||||
|
||||
const entry = Widget.Entry({
|
||||
hexpand: true,
|
||||
class_name: "cliphistory_entry",
|
||||
placeholder_text: "Search",
|
||||
on_change: ({ text }) => {
|
||||
const lowerCaseText = (text ?? "").toLowerCase();
|
||||
widgets.forEach(item => {
|
||||
item.visible = item.attribute.content.toLowerCase().includes(lowerCaseText);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Widget.Box({
|
||||
vertical: true,
|
||||
class_name: "cliphistory_box",
|
||||
margin_top: 14,
|
||||
margin_right: 14,
|
||||
children: [
|
||||
entry,
|
||||
Widget.Scrollable({
|
||||
hscroll: "never",
|
||||
css: `min-width: ${width}px; min-height: ${height}px;`,
|
||||
child: list,
|
||||
}),
|
||||
],
|
||||
setup: self => self.hook(App, (_, windowName, visible) => {
|
||||
if (windowName !== WINDOW_NAME) return;
|
||||
if (visible) {
|
||||
repopulate();
|
||||
entry.text = "";
|
||||
}
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export const cliphist = popupwindow({
|
||||
name: WINDOW_NAME,
|
||||
class_name: "cliphistory",
|
||||
visible: false,
|
||||
keymode: "exclusive",
|
||||
child: ClipHistWidget({
|
||||
width: 500,
|
||||
height: 500,
|
||||
spacing: 0,
|
||||
}),
|
||||
anchor: ["bottom", "right"],
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
import { MprisPlayer } from 'types/service/mpris.js';
|
||||
import popupwindow from './misc/popupwindow.ts';
|
||||
import Label from 'types/widgets/label.js';
|
||||
const mpris = await Service.import("mpris")
|
||||
const players = mpris.bind("players")
|
||||
|
||||
const FALLBACK_ICON = "audio-x-generic-symbolic"
|
||||
const PLAY_ICON = "media-playback-start-symbolic"
|
||||
const PAUSE_ICON = "media-playback-pause-symbolic"
|
||||
const PREV_ICON = "media-skip-backward-symbolic"
|
||||
const NEXT_ICON = "media-skip-forward-symbolic"
|
||||
const WINDOW_NAME = "media"
|
||||
|
||||
function lengthStr(length: number): string {
|
||||
const min = Math.floor(length / 60)
|
||||
const sec = Math.floor(length % 60)
|
||||
const sec0 = sec < 10 ? "0" : ""
|
||||
return `${min}:${sec0}${sec}`
|
||||
}
|
||||
|
||||
function Player(player: MprisPlayer) {
|
||||
const img = Widget.Box({
|
||||
class_name: "img",
|
||||
vpack: "start",
|
||||
css: player.bind("cover_path").transform(p => `
|
||||
background-image: url('${p}');
|
||||
`),
|
||||
})
|
||||
|
||||
const title = Widget.Label({
|
||||
class_name: "title",
|
||||
wrap: true,
|
||||
hpack: "start",
|
||||
label: player.bind("track_title"),
|
||||
})
|
||||
|
||||
const artist = Widget.Label({
|
||||
class_name: "artist",
|
||||
wrap: true,
|
||||
hpack: "start",
|
||||
label: player.bind("track_artists").transform(a => a.join(", ")),
|
||||
})
|
||||
|
||||
const positionSlider = Widget.Slider({
|
||||
class_name: "position",
|
||||
draw_value: false,
|
||||
on_change: ({ value }) => player.position = value * player.length,
|
||||
visible: player.bind("length").as(l => l > 0),
|
||||
setup: self => {
|
||||
function update() {
|
||||
const value = player.position / player.length
|
||||
self.value = value > 0 ? value : 0
|
||||
}
|
||||
self.hook(player, update)
|
||||
self.hook(player, update, "position")
|
||||
self.poll(1000, update)
|
||||
},
|
||||
})
|
||||
|
||||
const positionLabel = Widget.Label({
|
||||
class_name: "position",
|
||||
hpack: "start",
|
||||
setup: self => {
|
||||
const update = (self: Label<any>) => {
|
||||
self.label = lengthStr(player.position)
|
||||
self.visible = player.length > 0
|
||||
}
|
||||
|
||||
self.hook(player, update, "position")
|
||||
self.poll(1000, update)
|
||||
},
|
||||
})
|
||||
|
||||
const lengthLabel = Widget.Label({
|
||||
class_name: "length",
|
||||
hpack: "end",
|
||||
visible: player.bind("length").transform(l => l > 0),
|
||||
label: player.bind("length").transform(lengthStr),
|
||||
})
|
||||
|
||||
const icon = Widget.Icon({
|
||||
class_name: "icon",
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
vpack: "start",
|
||||
tooltip_text: player.identity || "",
|
||||
icon: player.bind("entry").transform(entry => {
|
||||
const name = `${entry}-symbolic`
|
||||
return Utils.lookUpIcon(name) ? name : FALLBACK_ICON
|
||||
}),
|
||||
})
|
||||
|
||||
const playPause = Widget.Button({
|
||||
class_name: "play-pause",
|
||||
on_clicked: () => player.playPause(),
|
||||
visible: player.bind("can_play"),
|
||||
child: Widget.Icon({
|
||||
icon: player.bind("play_back_status").transform(s => {
|
||||
switch (s) {
|
||||
case "Playing": return PAUSE_ICON
|
||||
case "Paused":
|
||||
case "Stopped": return PLAY_ICON
|
||||
}
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
const prev = Widget.Button({
|
||||
on_clicked: () => player.previous(),
|
||||
visible: player.bind("can_go_prev"),
|
||||
child: Widget.Icon(PREV_ICON),
|
||||
})
|
||||
|
||||
const next = Widget.Button({
|
||||
on_clicked: () => player.next(),
|
||||
visible: player.bind("can_go_next"),
|
||||
child: Widget.Icon(NEXT_ICON),
|
||||
})
|
||||
|
||||
return Widget.Box(
|
||||
{
|
||||
class_name: "player",
|
||||
margin_left: 14,
|
||||
margin_top: 14,
|
||||
},
|
||||
img,
|
||||
Widget.Box(
|
||||
{
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
},
|
||||
Widget.Box([
|
||||
title,
|
||||
icon,
|
||||
]),
|
||||
artist,
|
||||
Widget.Box({ vexpand: true }),
|
||||
positionSlider,
|
||||
Widget.CenterBox({
|
||||
start_widget: positionLabel,
|
||||
center_widget: Widget.Box({
|
||||
class_name: "player_buttons",
|
||||
children: [
|
||||
prev,
|
||||
playPause,
|
||||
next,
|
||||
]
|
||||
}),
|
||||
end_widget: lengthLabel,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function Media() {
|
||||
return Widget.Box({
|
||||
vertical: true,
|
||||
css: "min-height: 2px; min-width: 2px;",
|
||||
visible: players.as(p => p.length > 0),
|
||||
children: players.as(p => p.map(Player)),
|
||||
})
|
||||
}
|
||||
|
||||
export const media = popupwindow({
|
||||
name: WINDOW_NAME,
|
||||
class_name: "player_window",
|
||||
visible: false,
|
||||
keymode: "exclusive",
|
||||
child: Media(),
|
||||
//anchor: ["left", "top"],
|
||||
anchor: ["left", "bottom"],
|
||||
})
|
||||
@@ -0,0 +1,114 @@
|
||||
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
|
||||
|
||||
const prefix = "100" // Identifier for minimized workspace
|
||||
|
||||
function getMinimizedWS(maximizedWS){
|
||||
return parseInt(prefix + maximizedWS)
|
||||
}
|
||||
|
||||
function getMaximizedWS(minimizedWS){
|
||||
return parseInt(minimizedWS.toString().substring(prefix.length))
|
||||
}
|
||||
|
||||
function isMinimized(ws){
|
||||
ws = ws.toString()
|
||||
if (ws.length > 3 || ws.slice(0, prefix.length) == prefix){
|
||||
return true
|
||||
}
|
||||
else{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const focusClient = (client) => {
|
||||
|
||||
// If minimized
|
||||
if (isMinimized(client.workspace.id)){
|
||||
var maximizedWS = getMaximizedWS(client.workspace.id)
|
||||
Hyprland.messageAsync(`dispatch movetoworkspacesilent ${maximizedWS},address:${client.address}`)
|
||||
// Focus window
|
||||
//Hyprland.messageAsync(`dispatch focuswindow address:${client.address}`)
|
||||
Hyprland.messageAsync(`dispatch alterzorder top,address:${client.address}`)
|
||||
}
|
||||
|
||||
// If maximized
|
||||
else{
|
||||
var minimizedWS = getMinimizedWS(client.workspace.id)
|
||||
Hyprland.messageAsync(`dispatch movetoworkspacesilent ${minimizedWS},address:${client.address}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Client = (client = null) => Widget.Box({
|
||||
// Attributes
|
||||
attribute: {
|
||||
returnWS: client.workspace,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const appButton = (client = null) => Widget.Button({
|
||||
class_name: "dock-button",
|
||||
tooltip_text: client.title,
|
||||
on_primary_click: () => focusClient(client),
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: "dock-app-indicator",
|
||||
vpack: "center",
|
||||
setup: (self) => {
|
||||
self.hook(Hyprland, () => {
|
||||
//self.toggleClassName("dock-button-current", Hyprland.active.client.address === client.address)
|
||||
self.toggleClassName("dock-app-indicator-active", Hyprland.active.client.address === client.address)
|
||||
}, 'event')
|
||||
}
|
||||
}),
|
||||
/*
|
||||
Widget.Icon({
|
||||
class_name: 'client-icon',
|
||||
css: 'font-size: 3rem;',
|
||||
icon: client.class,
|
||||
}),
|
||||
*/
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
label: client.class,
|
||||
truncate: 'end',
|
||||
maxWidthChars: 8,
|
||||
})
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const clientList = Widget.Scrollable({
|
||||
hscroll: "never",
|
||||
child: Widget.Box({
|
||||
class_name: "dock-container",
|
||||
css: "min-height: 6rem;",
|
||||
css: "min-width: 6rem;",
|
||||
vertical: true,
|
||||
}).hook(Hyprland, self => {
|
||||
//check if ws is empty
|
||||
self.children = Hyprland.clients.filter(client => client.class != "" && (client.workspace.id === Hyprland.active.workspace.id || client.workspace.id === getMinimizedWS(Hyprland.active.workspace.id))).map(client => {
|
||||
return appButton(client)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
export const Dock = (monitor = 0) => Widget.Window({
|
||||
name: `Dock`, // name has to be unique
|
||||
monitor,
|
||||
visible: false,
|
||||
//margins: [4,64,4,64],
|
||||
//margins: [8,8,8,8],
|
||||
anchor: ['bottom', 'left', 'top'],
|
||||
exclusivity: 'exclusive',
|
||||
child: clientList,
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
const { Gtk } = imports.gi;
|
||||
const Lang = imports.lang;
|
||||
|
||||
export const RoundedCorner = (place, props) => Widget.DrawingArea({
|
||||
...props,
|
||||
hpack: place.includes('left') ? 'start' : 'end',
|
||||
vpack: place.includes('top') ? 'start' : 'end',
|
||||
setup: (widget) => Utils.timeout(1, () => {
|
||||
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
|
||||
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
|
||||
widget.set_size_request(r, r);
|
||||
// @ts-ignore
|
||||
widget.connect('draw', Lang.bind(widget, (widget, cr) => {
|
||||
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
|
||||
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
|
||||
// const borderColor = widget.get_style_context().get_property('color', Gtk.StateFlags.NORMAL);
|
||||
// const borderWidth = widget.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
|
||||
widget.set_size_request(r, r);
|
||||
|
||||
switch (place) {
|
||||
case 'top_left':
|
||||
cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
|
||||
cr.lineTo(0, 0);
|
||||
break;
|
||||
|
||||
case 'top_right':
|
||||
cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
|
||||
cr.lineTo(r, 0);
|
||||
break;
|
||||
|
||||
case 'bottom_left':
|
||||
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
|
||||
cr.lineTo(0, r);
|
||||
break;
|
||||
|
||||
case 'bottom_right':
|
||||
cr.arc(0, 0, r, 0, Math.PI / 2);
|
||||
cr.lineTo(r, r);
|
||||
break;
|
||||
}
|
||||
|
||||
cr.closePath();
|
||||
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
|
||||
cr.fill();
|
||||
// cr.setLineWidth(borderWidth);
|
||||
// cr.setSourceRGBA(borderColor.red, borderColor.green, borderColor.blue, borderColor.alpha);
|
||||
// cr.stroke();
|
||||
}));
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import Cairo from 'gi://cairo?version=1.0';
|
||||
|
||||
export const dummyRegion = new Cairo.Region();
|
||||
export const enableClickThrough = (self) => self.input_shape_combine_region(dummyRegion);
|
||||
@@ -0,0 +1,44 @@
|
||||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
|
||||
import { WindowProps, Window } from 'types/widgets/window';
|
||||
const { Gtk, GLib } = imports.gi;
|
||||
|
||||
type _WindowProps = WindowProps<any, unknown, Window<any, unknown>> | undefined[];
|
||||
|
||||
type Type = {
|
||||
name: string,
|
||||
child: any,
|
||||
showClassName?: string,
|
||||
hideClassName?: string,
|
||||
}
|
||||
|
||||
export default <T extends Type>({
|
||||
name,
|
||||
child,
|
||||
showClassName = "",
|
||||
hideClassName = "",
|
||||
...props
|
||||
}: T & Omit<_WindowProps, keyof Type>) => {
|
||||
return Widget.Window({
|
||||
name,
|
||||
visible: false,
|
||||
type: Gtk.WindowType.POPUP,
|
||||
layer: 'overlay',
|
||||
resizable: true,
|
||||
...props,
|
||||
setup: (self) => {
|
||||
self.keybind("Escape", () => App.closeWindow(name));
|
||||
if (showClassName != "" && hideClassName !== "") {
|
||||
self.hook(App, (self, currentName, visible) => {
|
||||
if (currentName === name) {
|
||||
self.toggleClassName(hideClassName, !visible);
|
||||
}
|
||||
});
|
||||
|
||||
if (showClassName !== "" && hideClassName !== "")
|
||||
self.class_name = `${showClassName} ${hideClassName}`;
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
import { Gtk as GtkType } from "types/@girs/gtk-3.0/gtk-3.0";
|
||||
import { Notification as NotificationType } from "types/service/notifications";
|
||||
import Box from "types/widgets/box";
|
||||
import { timeout } from "resource:///com/github/Aylur/ags/utils.js";
|
||||
import GLib from "gi://GLib";
|
||||
import Pango from "gi://Pango";
|
||||
import Label from "types/widgets/label";
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
function NotificationIcon({ app_entry, app_icon, image }: NotificationType): Box<GtkType.Widget, any> {
|
||||
if (image) {
|
||||
return Widget.Box({
|
||||
css: `background-image: url("${image}");`
|
||||
+ "background-size: contain;"
|
||||
+ "background-repeat: no-repeat;"
|
||||
+ "background-position: center;",
|
||||
})
|
||||
}
|
||||
|
||||
let icon = "dialog-information-symbolic"
|
||||
if (Utils.lookUpIcon(app_icon))
|
||||
icon = app_icon
|
||||
|
||||
if (app_entry && Utils.lookUpIcon(app_entry))
|
||||
icon = app_entry
|
||||
|
||||
return Widget.Box({
|
||||
child: Widget.Icon(icon),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const Notification = (notification: NotificationType, dismiss = true) => Widget.Box({
|
||||
class_name: `notification ${notification.urgency}`,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.EventBox({
|
||||
on_primary_click: (box) => {
|
||||
// @ts-ignore
|
||||
const label: Label<any> = box.child.children[1].children[1];
|
||||
if (label.lines < 0) {
|
||||
label.lines = 3;
|
||||
label.truncate = "end";
|
||||
} else {
|
||||
label.lines = -1;
|
||||
label.truncate = "none";
|
||||
}
|
||||
},
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: "notification-icon",
|
||||
child: NotificationIcon(notification),
|
||||
vpack: "start"
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: "notification-title",
|
||||
label: notification.summary,
|
||||
justification: "left",
|
||||
max_width_chars: 3,
|
||||
truncate: "end",
|
||||
lines: 1,
|
||||
xalign: 0,
|
||||
hexpand: true,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "notification-time",
|
||||
label: GLib.DateTime.new_from_unix_local(notification.time).format("%I:%M %p"),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: "standard_icon_button",
|
||||
child: Widget.Icon("window-close-symbolic"),
|
||||
on_clicked: () => {
|
||||
if (dismiss) {
|
||||
notification.dismiss();
|
||||
}
|
||||
else {
|
||||
notification.close();
|
||||
}
|
||||
},
|
||||
css: "margin-left: 5px;"
|
||||
})
|
||||
]
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: "notification-body",
|
||||
justification: "left",
|
||||
max_width_chars: 24,
|
||||
lines: 3,
|
||||
truncate: "end",
|
||||
wrap_mode: Pango.WrapMode.WORD_CHAR,
|
||||
xalign: 0,
|
||||
wrap: true,
|
||||
// HACK: remove linebreaks, so lines property works properly
|
||||
label: notification.body.replace(/(\r\n|\n|\r)/gm, " "),
|
||||
}),
|
||||
notification.hints.value ?
|
||||
Widget.ProgressBar({
|
||||
class_name: "notification-progress",
|
||||
value: Number(notification.hints.value.unpack()) / 100
|
||||
}) : Widget.Box()
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
}),
|
||||
Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
class_name: "notification-actions",
|
||||
children: notification.actions.map(action => Widget.Button({
|
||||
child: Widget.Label(action.label),
|
||||
on_clicked: () => notification.invoke(action.id),
|
||||
class_name: "notification-action-button",
|
||||
}))
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
const NotificationReveal = (notification: NotificationType, visible = false, dismiss = true) => {
|
||||
const transition_duration = 200;
|
||||
const secondRevealer = Widget.Revealer({
|
||||
vpack: "start",
|
||||
child: Notification(notification, dismiss),
|
||||
reveal_child: visible,
|
||||
transition: "slide_left",
|
||||
transition_duration: transition_duration,
|
||||
hexpand: true,
|
||||
class_name: "second_revealer",
|
||||
setup: (revealer) => {
|
||||
timeout(1, () => {
|
||||
revealer.reveal_child = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const firstRevealer = Widget.Revealer({
|
||||
child: secondRevealer,
|
||||
reveal_child: true,
|
||||
transition: "slide_down",
|
||||
transition_duration: transition_duration,
|
||||
class_name: "first_revealer"
|
||||
});
|
||||
|
||||
type BoxAttrs = {
|
||||
destroyWithAnims: any,
|
||||
count: number,
|
||||
id: number,
|
||||
}
|
||||
|
||||
let box: Box<any, BoxAttrs>;
|
||||
|
||||
const destroyWithAnims = () => {
|
||||
secondRevealer.reveal_child = false;
|
||||
timeout(transition_duration, () => {
|
||||
firstRevealer.reveal_child = false;
|
||||
timeout(transition_duration, () => {
|
||||
box.destroy();
|
||||
});
|
||||
});
|
||||
};
|
||||
box = Widget.Box({
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
attribute: {
|
||||
"destroyWithAnims": destroyWithAnims,
|
||||
count: 0,
|
||||
id: notification.id,
|
||||
},
|
||||
children: [firstRevealer],
|
||||
});
|
||||
return box;
|
||||
};
|
||||
|
||||
type NotificationsBoxType = {
|
||||
exclude: string[],
|
||||
include: string[]
|
||||
}
|
||||
|
||||
export function NotificationPopups(
|
||||
dismiss = true,
|
||||
{ exclude, include }: NotificationsBoxType = { exclude: [], include: [] },
|
||||
timeout = true,
|
||||
revealer = NotificationReveal
|
||||
) {
|
||||
const list = Widget.Box({
|
||||
vpack: "start",
|
||||
vertical: true,
|
||||
children: notifications.popups.map(id => revealer(id, false, dismiss)),
|
||||
})
|
||||
|
||||
function onNotified(_: any, id: number) {
|
||||
const n = notifications.getNotification(id)
|
||||
if (notifications.dnd || !n)
|
||||
return;
|
||||
if (exclude.includes(n.app_name.trim()))
|
||||
return
|
||||
if (!include.includes(n.app_name.trim()) && include.length > 0)
|
||||
return
|
||||
const original = list.children.find(n => n.attribute.id === id);
|
||||
const replace = original?.attribute.id;
|
||||
if (!replace) {
|
||||
const notification = revealer(n, false, dismiss);
|
||||
notification.attribute.count = 1;
|
||||
list.pack_end(notification, false, false, 0)
|
||||
}
|
||||
else if (original) {
|
||||
const notification = revealer(n, true, dismiss);
|
||||
notification.attribute.count++;
|
||||
original.destroy()
|
||||
list.pack_end(notification, false, false, 0)
|
||||
}
|
||||
if (timeout)
|
||||
setTimeout(() => {
|
||||
onDismissed(_, id)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function onDismissed(_: any, id: number) {
|
||||
const original = list.children.find(n => n.attribute.id === id);
|
||||
if (!original)
|
||||
return
|
||||
original.attribute.count--;
|
||||
if (original.attribute.count <= 0) {
|
||||
original.attribute.destroyWithAnims()
|
||||
}
|
||||
}
|
||||
|
||||
list.hook(notifications, onNotified, "notified")
|
||||
.hook(notifications, onDismissed, dismiss ? "dismissed" : "closed")
|
||||
return list
|
||||
}
|
||||
|
||||
export function Notifications(monitor = 0) {
|
||||
const window = Widget.Window({
|
||||
monitor,
|
||||
name: `notifications${monitor}`,
|
||||
class_name: "notification-popups",
|
||||
anchor: ["bottom", "right"],
|
||||
type: Gtk.WindowType.POPUP,
|
||||
child: Widget.Box({
|
||||
css: "min-width: 2px; min-height: 2px;",
|
||||
class_name: "notifications",
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
child: NotificationPopups(),
|
||||
}),
|
||||
visible: true,
|
||||
})
|
||||
return window
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
const { Gtk, GLib } = imports.gi;
|
||||
const lock_command = `hyprlock`;
|
||||
const logout_command = `loginctl kill-session $XDG_SESSION_ID`;
|
||||
const shutdown_command = `systemctl poweroff`;
|
||||
const reboot_command = `systemctl reboot`;
|
||||
const suspend_command = `systemctl suspend`;
|
||||
|
||||
function LockButton({ icon, ...props }) {
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
tooltip_text: "Lock",
|
||||
child: Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon"
|
||||
}),
|
||||
class_name: "outline_button",
|
||||
...props,
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
Utils.execAsync(lock_command).catch(print);
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function SuspendButton({ icon, ...props }) {
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
tooltip_text: "Suspend",
|
||||
child: Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon"
|
||||
}),
|
||||
class_name: "outline_button",
|
||||
...props,
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
Utils.execAsync(`mpc -q pause`).catch();
|
||||
Utils.execAsync(`playerctl pause`).catch();
|
||||
Utils.execAsync(suspend_command).catch();
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function LogoutButton({ icon, ...props }) {
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
tooltip_text: "Logout",
|
||||
child: Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon"
|
||||
}),
|
||||
class_name: "outline_button",
|
||||
...props,
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
Utils.execAsync(logout_command).catch();
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function RebootButton({ icon, ...props }) {
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
tooltip_text: "Reboot",
|
||||
child: Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon"
|
||||
}),
|
||||
class_name: "outline_button",
|
||||
...props,
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
Utils.execAsync(reboot_command).catch();
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
function ShutdownButton({ icon, ...props }) {
|
||||
let clickCount = 0;
|
||||
let button = Widget.Button({
|
||||
tooltip_text: "Shutdown",
|
||||
child: Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon"
|
||||
}),
|
||||
class_name: "outline_button",
|
||||
...props,
|
||||
})
|
||||
|
||||
button.connect('clicked', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 2) {
|
||||
Utils.execAsync(shutdown_command).catch();
|
||||
clickCount = 0;
|
||||
}
|
||||
});
|
||||
|
||||
button.connect('focus-out-event', () => {
|
||||
clickCount = 0;
|
||||
});
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
|
||||
export function Buttons() {
|
||||
return Widget.Box({
|
||||
class_name: "sidebar_buttons",
|
||||
hexpand: true,
|
||||
spacing: 5,
|
||||
children: [
|
||||
LockButton({
|
||||
icon: "",
|
||||
hexpand: true,
|
||||
}),
|
||||
SuspendButton({
|
||||
icon: "",
|
||||
hexpand: true,
|
||||
}),
|
||||
LogoutButton({
|
||||
icon: "",
|
||||
hexpand: true,
|
||||
}),
|
||||
RebootButton({
|
||||
icon: "",
|
||||
hexpand: true,
|
||||
}),
|
||||
ShutdownButton({
|
||||
icon: "",
|
||||
hexpand: true,
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import popupwindow from '../misc/popupwindow.js';
|
||||
import { SideBar } from './sidebar.ts'
|
||||
|
||||
export const WINDOW_NAME = "sidebar"
|
||||
|
||||
|
||||
export const sidebar = popupwindow({
|
||||
name: WINDOW_NAME,
|
||||
|
||||
class_name: "sidebar",
|
||||
visible: false,
|
||||
keymode: "exclusive",
|
||||
child: SideBar(),
|
||||
anchor: ["top", "right", "bottom"],
|
||||
css: "min-width: 443px;"
|
||||
})
|
||||
@@ -0,0 +1,387 @@
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const network = await Service.import('network')
|
||||
import { OpenSettings } from "apps/settings/main.ts";
|
||||
import { WINDOW_NAME } from "./main.ts"
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gio = imports.gi.Gio;
|
||||
|
||||
const idle_inhibitor = Variable(false);
|
||||
const night_light = Variable(false);
|
||||
const screen_recorder = Variable(false);
|
||||
const currentPage = Variable(0);
|
||||
export const bluetooth_enabled = Variable('off', {
|
||||
poll: [1000, `${App.configDir}/scripts/bluetooth.sh --get`]
|
||||
})
|
||||
|
||||
const powerProfiles = await Service.import('powerprofiles');
|
||||
const icons = {
|
||||
'performance': ' ',
|
||||
'balanced': ' ',
|
||||
'power-saver': ' '
|
||||
};
|
||||
|
||||
function WifiIndicator() {
|
||||
return Widget.Box({
|
||||
css: "padding-left: 15px; padding-right: 15px; padding-top: 5px; padding-bottom: 5px;",
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "\udb82\udd28",
|
||||
class_name: "awesome_icon",
|
||||
css: `font-weight: bold; font-size: 20px; margin-right: 0.60em;`,
|
||||
}),
|
||||
Widget.Box({
|
||||
visible: network.wifi.bind('enabled'),
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
Widget.Box([
|
||||
Widget.Label({
|
||||
label: "Internet",
|
||||
}),
|
||||
]),
|
||||
Widget.Label({
|
||||
label: network.wifi.bind('ssid')
|
||||
.as(ssid => ssid || 'Unknown'),
|
||||
class_name: "ssid",
|
||||
xalign: 0,
|
||||
vpack: "center",
|
||||
truncate: "end",
|
||||
}),
|
||||
]
|
||||
}),
|
||||
Widget.Label({
|
||||
visible: network.wifi.bind('enabled').as(enabled => !enabled),
|
||||
label: "Internet",
|
||||
}),
|
||||
Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
hexpand: true,
|
||||
child: Widget.Label({
|
||||
label: "",
|
||||
class_name: "awesome_icon",
|
||||
css: `font-weight: normal; font-size: 15px;`,
|
||||
})
|
||||
})
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const WiredIndicator = () => Widget.Box({
|
||||
css: "padding-left: 15px; padding-right: 15px; padding-top: 5px; padding-bottom: 5px;",
|
||||
children: [
|
||||
Widget.Icon({
|
||||
icon: network.wired.bind('icon_name'),
|
||||
}),
|
||||
Widget.Label({
|
||||
label: "Internet",
|
||||
}),
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const NetworkIndicator = () => Widget.Stack({
|
||||
children: {
|
||||
wifi: WifiIndicator(),
|
||||
wired: WiredIndicator(),
|
||||
},
|
||||
shown: network.bind('primary').as(p => p || 'wifi'),
|
||||
})
|
||||
|
||||
|
||||
function IconAndName({ label, icon, padding = "0.3em", arrow = false }) {
|
||||
let box = Widget.Box({
|
||||
css: "padding-left: 15px; padding-right: 15px; padding-top: 5px; padding-bottom: 5px;",
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: icon,
|
||||
class_name: "awesome_icon",
|
||||
css: `font-weight: bold; font-size: 20px; margin-right: ${padding};`,
|
||||
}),
|
||||
Widget.Label({
|
||||
label: label,
|
||||
justification: "center",
|
||||
}),
|
||||
]
|
||||
})
|
||||
if (arrow) {
|
||||
const arrow = Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
hexpand: true,
|
||||
child: Widget.Label({
|
||||
label: "",
|
||||
class_name: "awesome_icon",
|
||||
css: `font-weight: normal; font-size: 15px;`,
|
||||
})
|
||||
})
|
||||
// @ts-ignore
|
||||
box.children = [...box.children, arrow]
|
||||
}
|
||||
return box
|
||||
}
|
||||
|
||||
function isScreenRecordingOn() {
|
||||
// Check if wf-recorder is running
|
||||
// Bash command outputs 0 if yes or 1 if no
|
||||
let state = Utils.execAsync('bash -c "pidof wf-recorder > /dev/null; echo $?"')
|
||||
if (state == "0"){
|
||||
return true
|
||||
}
|
||||
else{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function Page1() {
|
||||
return Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 5,
|
||||
hexpand: true,
|
||||
class_name: "management_box",
|
||||
children: [
|
||||
Widget.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
hexpand: true,
|
||||
spacing: 2.5,
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: network.wifi.bind("enabled")
|
||||
.as(enabled => enabled ? "management_button active" : "management_button"),
|
||||
child: NetworkIndicator(),
|
||||
on_primary_click: () => {
|
||||
network.toggleWifi();
|
||||
},
|
||||
on_secondary_click: () => {
|
||||
App.closeWindow(WINDOW_NAME);
|
||||
App.openWindow("network");
|
||||
}
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: bluetooth_enabled.bind()
|
||||
.as(state => state.trim() == "yes" ? "management_button active" : "management_button"
|
||||
),
|
||||
child: IconAndName({
|
||||
label: "Bluetooth",
|
||||
icon: "",
|
||||
arrow: true,
|
||||
}),
|
||||
on_primary_click: () => {
|
||||
Utils.execAsync(`${App.configDir}/scripts/bluetooth.sh --toggle`)
|
||||
.then(out => { bluetooth_enabled.setValue(out) })
|
||||
},
|
||||
on_secondary_click: () => {
|
||||
Utils.execAsync("blueman-manager")
|
||||
App.closeWindow(WINDOW_NAME)
|
||||
}
|
||||
})
|
||||
]
|
||||
}),
|
||||
Widget.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 2.5,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: idle_inhibitor.bind()
|
||||
.as(bool => bool ? "management_button active" : "management_button"),
|
||||
child: IconAndName({
|
||||
label: "Idle inhibitor",
|
||||
icon: ""
|
||||
}),
|
||||
onClicked: () => {
|
||||
idle_inhibitor.setValue(!idle_inhibitor.value)
|
||||
if (idle_inhibitor.value) Utils.execAsync(['bash', '-c', `pidof wayland-idle-inhibitor.py || ${App.configDir}/scripts/wayland-idle-inhibitor.py`]).catch(print)
|
||||
else Utils.execAsync('pkill -f wayland-idle-inhibitor.py').catch(print);
|
||||
},
|
||||
setup: () => {
|
||||
idle_inhibitor.setValue(!!Utils.exec('pidof wayland-idle-inhibitor.py'));
|
||||
},
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: notifications.bind("dnd").as((bool) => {
|
||||
return bool ? "management_button active" : "management_button";
|
||||
}),
|
||||
on_clicked: () => {
|
||||
notifications.dnd = notifications.dnd ? false : true
|
||||
},
|
||||
child: IconAndName({
|
||||
label: "Do Not Disturb",
|
||||
icon: ""
|
||||
})
|
||||
})
|
||||
]
|
||||
}),
|
||||
Widget.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 2.5,
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: screen_recorder.bind()
|
||||
.as(bool => bool ? "management_button active" : "management_button"),
|
||||
on_clicked: (self) => {
|
||||
screen_recorder.setValue(!screen_recorder.value)
|
||||
// Adjust indicator
|
||||
let isRecording = isScreenRecordingOn()
|
||||
self.toggleClassName("active-button", !isRecording) // Toggles active indicator
|
||||
self.toggleClassName("recording", !isRecording) // Toggles active indicator
|
||||
|
||||
// Starts screen recorder if not running
|
||||
// Stops screen recorder if running
|
||||
Utils.execAsync(['bash', '-c', 'mkdir ~/Videos/Screenrecordings; pkill wf-recorder; if [ $? -ne 0 ]; then wf-recorder -f ~/Videos/Screenrecordings/recording_"$(date +\'%b-%d-%Y-%I:%M:%S-%P\')".mp4 -g "$(slurp)" --pixel-format yuv420p; fi']).catch(logError);
|
||||
},
|
||||
child: IconAndName({
|
||||
label: "Screen Recorder",
|
||||
icon: "",
|
||||
setup: self=>{}
|
||||
})
|
||||
}),
|
||||
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: night_light.bind()
|
||||
.as(mode => mode ? "management_button active" : "management_button"),
|
||||
child: IconAndName({
|
||||
label: "Night Light",
|
||||
icon: ""
|
||||
}),
|
||||
onClicked: () => {
|
||||
night_light.setValue(!night_light.value)
|
||||
Utils.execAsync(`${App.configDir}/scripts/night-light.sh --toggle`)
|
||||
.then(out => {
|
||||
if (out.trim() == "enabled") night_light.setValue(true)
|
||||
else if (out.trim() == "disabled") night_light.setValue(false)
|
||||
})
|
||||
.catch(err => {
|
||||
night_light.setValue(!night_light.value);
|
||||
});
|
||||
},
|
||||
setup: () => {
|
||||
Utils.execAsync(`${App.configDir}/scripts/night-light.sh --get`)
|
||||
.then(out => {
|
||||
if (out.trim() == "enabled") night_light.setValue(true)
|
||||
else if (out.trim() == "disabled") night_light.setValue(false)
|
||||
})
|
||||
.catch(print);
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Page2() {
|
||||
return Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
spacing: 5,
|
||||
hexpand: true,
|
||||
class_name: "management_box",
|
||||
children: [
|
||||
Widget.Box({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
spacing: 2.5,
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: "management_button",
|
||||
child: IconAndName({
|
||||
label: "Color picker",
|
||||
icon: "",
|
||||
arrow: true,
|
||||
}),
|
||||
on_primary_click: () => {
|
||||
App.closeWindow(WINDOW_NAME);
|
||||
Utils.execAsync("sleep 0.5")
|
||||
.then(() =>
|
||||
Utils.execAsync("hyprpicker -a").catch(print)
|
||||
)
|
||||
.catch(print);
|
||||
}
|
||||
}),
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: "management_button",
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: powerProfiles.bind('active_profile').as(x => icons[x])
|
||||
}),
|
||||
Widget.Label({
|
||||
label: powerProfiles.bind('active_profile').as(x => x.toUpperCase())
|
||||
})
|
||||
]
|
||||
}),
|
||||
on_clicked: () => {
|
||||
switch (powerProfiles.active_profile) {
|
||||
case 'balanced':
|
||||
powerProfiles.active_profile = 'performance';
|
||||
break;
|
||||
case 'power-saver':
|
||||
powerProfiles.active_profile = 'balanced';
|
||||
break;
|
||||
default:
|
||||
powerProfiles.active_profile = 'power-saver';
|
||||
break;
|
||||
};
|
||||
},
|
||||
}),
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const createDotButton = (index: number) => Widget.Button({
|
||||
label: '',
|
||||
onClicked: () => currentPage.setValue(index),
|
||||
class_name: currentPage.bind().as(v => v == index ? "dotbutton active" : "dotbutton"),
|
||||
hexpand: false,
|
||||
});
|
||||
|
||||
|
||||
export function Management() {
|
||||
let pages = {
|
||||
"page1": Page1(),
|
||||
"page2": Page2()
|
||||
}
|
||||
const numberOfPages = Object.keys(pages).length;
|
||||
const pageNames = Array.from({ length: numberOfPages }, (_, i) => `page${i + 1}`);
|
||||
|
||||
const stack = Widget.Stack({
|
||||
children: pages,
|
||||
// @ts-ignore
|
||||
shown: currentPage.bind().as(v => `page${v + 1}`),
|
||||
transition: "slide_left_right",
|
||||
transitionDuration: 200,
|
||||
})
|
||||
const dotButtons = pageNames.map((_, index) => createDotButton(index));
|
||||
return Widget.EventBox({
|
||||
onScrollUp: () => currentPage.setValue((currentPage.value + 1) % numberOfPages),
|
||||
onScrollDown: () => {
|
||||
if (currentPage.value - 1) {
|
||||
currentPage.setValue(numberOfPages - 1)
|
||||
return
|
||||
}
|
||||
currentPage.setValue(currentPage.value - 1);
|
||||
},
|
||||
child: Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
stack,
|
||||
Widget.Box({
|
||||
children: dotButtons,
|
||||
class_name: "dotbuttons_box",
|
||||
halign: Gtk.Align.CENTER,
|
||||
})
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
const { Gtk } = imports.gi;
|
||||
import { NotificationsBox } from "./notifications.ts"
|
||||
import { SystemBox } from "./system.ts"
|
||||
let shown = Variable("Messages");
|
||||
|
||||
|
||||
type ButtonType = {
|
||||
page: string,
|
||||
label: string,
|
||||
icon: string
|
||||
}
|
||||
|
||||
|
||||
function Button({ page, label, icon }: ButtonType) {
|
||||
return Widget.Button({
|
||||
class_name: shown.bind().as(_page => _page == page ? "navigation_button active" : "navigation_button"),
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "container_outer",
|
||||
children: [
|
||||
Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
halign: Gtk.Align.CENTER,
|
||||
class_name: "container",
|
||||
child: Widget.Label({
|
||||
hpack: "center",
|
||||
label: icon,
|
||||
class_name: "awesome_icon icon"
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
label: label,
|
||||
class_name: "label",
|
||||
})
|
||||
]
|
||||
}),
|
||||
on_clicked: () => {
|
||||
shown.setValue(page)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function Navigation() {
|
||||
const messages_apps = ["discord", "materialgram", "Telegram Desktop"]
|
||||
let stack = Widget.Stack({
|
||||
children: {
|
||||
"Messages": NotificationsBox({ include: messages_apps }),
|
||||
"Notifications": NotificationsBox({ exclude: messages_apps }),
|
||||
"System": SystemBox()
|
||||
},
|
||||
transition: "crossfade",
|
||||
transitionDuration: 200,
|
||||
// @ts-ignore
|
||||
shown: shown.bind()
|
||||
})
|
||||
const buttons = Widget.Box({
|
||||
class_name: "navigation",
|
||||
hexpand: true,
|
||||
children: [
|
||||
Button({
|
||||
page: "Messages",
|
||||
label: "Messages",
|
||||
icon: ""
|
||||
}),
|
||||
Button({
|
||||
page: "Notifications",
|
||||
label: "Notifs",
|
||||
icon: ""
|
||||
}),
|
||||
Button({
|
||||
page: "System",
|
||||
label: "System",
|
||||
icon: ""
|
||||
})
|
||||
]
|
||||
})
|
||||
return Widget.Box({
|
||||
vexpand: true,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "sidebar_bottom",
|
||||
children: [
|
||||
stack,
|
||||
buttons
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { Notification as NotificationType } from "types/service/notifications";
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const { Gtk, Gdk } = imports.gi;
|
||||
|
||||
function NotificationIcon({ app_entry, app_icon, image }: NotificationType) {
|
||||
if (image) {
|
||||
return Widget.Box({
|
||||
css: `background-image: url("${image}");`
|
||||
+ "background-size: contain;"
|
||||
+ "background-repeat: no-repeat;"
|
||||
+ "background-position: center;",
|
||||
})
|
||||
}
|
||||
|
||||
let icon = "dialog-information-symbolic"
|
||||
if (Utils.lookUpIcon(app_icon))
|
||||
icon = app_icon
|
||||
|
||||
if (app_entry && Utils.lookUpIcon(app_entry))
|
||||
icon = app_entry
|
||||
|
||||
return Widget.Box({
|
||||
child: Widget.Icon(icon),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Notification(n: NotificationType) {
|
||||
const icon = Widget.Box({
|
||||
vpack: "start",
|
||||
class_name: "notification_sidebar_icon",
|
||||
child: NotificationIcon(n),
|
||||
})
|
||||
|
||||
const title = Widget.Label({
|
||||
class_name: "notification_sidebar_title",
|
||||
wrap: true,
|
||||
label: n.summary,
|
||||
use_markup: true,
|
||||
hpack: "start",
|
||||
xalign: 0
|
||||
})
|
||||
|
||||
const body = Widget.Label({
|
||||
class_name: "notification_sidebar_body",
|
||||
label: n.body,
|
||||
wrap: true,
|
||||
use_markup: true,
|
||||
hpack: "start",
|
||||
xalign: 0
|
||||
})
|
||||
|
||||
const actions = Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
class_name: "notification_sidebar_actions",
|
||||
children: n.actions.map(({ id, label }) => Widget.Button({
|
||||
class_name: "notification_sidebar_button",
|
||||
on_clicked: () => {
|
||||
n.invoke(id)
|
||||
},
|
||||
hexpand: false,
|
||||
child: Widget.Label(label),
|
||||
})),
|
||||
})
|
||||
|
||||
let Box = Widget.EventBox(
|
||||
{
|
||||
attribute: { id: n.id },
|
||||
on_secondary_click: () => n.close(),
|
||||
},
|
||||
Widget.Box(
|
||||
{
|
||||
class_name: `notification_sidebar ${n.urgency}`,
|
||||
vertical: true,
|
||||
},
|
||||
Widget.Box([
|
||||
icon,
|
||||
Widget.Box(
|
||||
{
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
},
|
||||
title,
|
||||
body,
|
||||
),
|
||||
]),
|
||||
actions,
|
||||
),
|
||||
)
|
||||
return Box
|
||||
}
|
||||
|
||||
type NotificationsBoxType = {
|
||||
exclude?: string[],
|
||||
include?: string[]
|
||||
}
|
||||
|
||||
export function NotificationsBox({ exclude = [], include = [] }: NotificationsBoxType) {
|
||||
let isEmpty = Variable(false)
|
||||
const list = Widget.Box({
|
||||
vertical: true,
|
||||
children: notifications.popups.map(Notification)
|
||||
})
|
||||
|
||||
const _list = Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
list,
|
||||
Widget.Label({
|
||||
label: "Empty",
|
||||
visible: isEmpty.bind(),
|
||||
class_name: "empty"
|
||||
})
|
||||
],
|
||||
class_name: "notifications_list_sidebar"
|
||||
})
|
||||
|
||||
function setIsEmpty() {
|
||||
if (list.children.length > 0) isEmpty.setValue(false)
|
||||
else isEmpty.setValue(true)
|
||||
}
|
||||
|
||||
function onNotified(_: any, id: number) {
|
||||
const n = notifications.getNotification(id)
|
||||
if (n) {
|
||||
if (exclude.includes(n.app_name.trim()))
|
||||
return
|
||||
if (include.length == 0 || include.includes(n.app_name.trim())) {
|
||||
const notificationWidget = Notification(n)
|
||||
list.children = [notificationWidget, ...list.children]
|
||||
setIsEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onClosed(_: any, id: number) {
|
||||
list.children.find(n => n.attribute.id === id)?.destroy()
|
||||
setIsEmpty()
|
||||
}
|
||||
list.hook(notifications, onNotified, "notified")
|
||||
.hook(notifications, onClosed, "closed")
|
||||
|
||||
const menu = Widget.Menu({
|
||||
class_name: "notifications_menu",
|
||||
children: [
|
||||
Widget.MenuItem({
|
||||
child: Widget.Label('Close all'),
|
||||
class_name: "notifications_menu_item",
|
||||
on_activate: () => {
|
||||
for (let x of list.children) {
|
||||
const n = notifications.getNotification(x.attribute.id)
|
||||
if (n) {
|
||||
n.dismiss()
|
||||
n.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
return Widget.EventBox({
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
on_secondary_click: (_, event) => {
|
||||
menu.popup_at_pointer(event)
|
||||
},
|
||||
child: Widget.Scrollable({
|
||||
class_name: "notifications_sidebar_scrollable",
|
||||
hscroll: "never",
|
||||
child: _list,
|
||||
hexpand: true
|
||||
|
||||
/** this is a simple one liner that could be used instead of
|
||||
hooking into the 'notified' and 'dismissed' signals.
|
||||
but its not very optimized becuase it will recreate
|
||||
the whole list everytime a notification is added or dismissed */
|
||||
// children: notifications.bind('popups')
|
||||
// .as(popups => popups.map(Notification))
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Notification as NotificationType } from "types/service/notifications";
|
||||
import { NotificationPopups, Notification } from "modules/notificationPopups";
|
||||
import Box from "types/widgets/box";
|
||||
|
||||
const notifications = await Service.import("notifications")
|
||||
const { Gtk, Gdk } = imports.gi;
|
||||
|
||||
type NotificationsBoxType = {
|
||||
exclude?: string[],
|
||||
include?: string[]
|
||||
}
|
||||
|
||||
const NotificationNoReveal = (notification: NotificationType, visible = false, dismiss = true) => {
|
||||
type BoxAttrs = {
|
||||
destroyWithAnims: any,
|
||||
count: number,
|
||||
id: number,
|
||||
}
|
||||
|
||||
const _notification = Notification(notification, dismiss);
|
||||
|
||||
let box: Box<any, BoxAttrs>;
|
||||
|
||||
const destroyWithAnims = () => {
|
||||
box.destroy();
|
||||
};
|
||||
box = Widget.Box({
|
||||
hexpand: true,
|
||||
attribute: {
|
||||
"destroyWithAnims": destroyWithAnims,
|
||||
count: 0,
|
||||
id: notification.id,
|
||||
},
|
||||
children: [_notification],
|
||||
});
|
||||
return box;
|
||||
};
|
||||
|
||||
export function NotificationsBox({ exclude = [], include = [] }: NotificationsBoxType) {
|
||||
const popups = NotificationPopups(
|
||||
false, { exclude: exclude, include: include },
|
||||
false, NotificationNoReveal
|
||||
);
|
||||
|
||||
const menu = Widget.Menu({
|
||||
class_name: "notifications_menu",
|
||||
children: [
|
||||
Widget.MenuItem({
|
||||
child: Widget.Label('Close all'),
|
||||
class_name: "notifications_menu_item",
|
||||
on_activate: () => {
|
||||
for (let n of notifications.notifications) {
|
||||
if (n) {
|
||||
n.dismiss()
|
||||
n.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
return Widget.EventBox({
|
||||
vexpand: true,
|
||||
hexpand: true,
|
||||
on_secondary_click: (_, event) => {
|
||||
menu.popup_at_pointer(event)
|
||||
},
|
||||
child: Widget.Scrollable({
|
||||
class_name: "notifications_sidebar_scrollable",
|
||||
hscroll: "never",
|
||||
child: popups,
|
||||
hexpand: true
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Management } from "./management.ts"
|
||||
import { Buttons } from "./buttons.ts";
|
||||
import { Navigation } from "./navigation.ts";
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
export function SideBar() {
|
||||
return Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "sidebar_main_box",
|
||||
hexpand: true,
|
||||
children: [
|
||||
Management(),
|
||||
Buttons(),
|
||||
Navigation()
|
||||
// NotificationsBox()
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { Variable as VariableType } from "types/variable";
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
const cpu_name = await Utils.execAsync(`${App.configDir}/scripts/system.sh --cpu-name`);
|
||||
const cpu_cores = await Utils.execAsync(`${App.configDir}/scripts/system.sh --cpu-cores`);
|
||||
const amount_of_ram = await Utils.execAsync(`${App.configDir}/scripts/system.sh --ram`);
|
||||
const kernel_name = await Utils.execAsync(`${App.configDir}/scripts/system.sh --kernel`);
|
||||
|
||||
type InfoType = {
|
||||
cpu: string,
|
||||
ram: string,
|
||||
swap: string,
|
||||
cpu_temp: string
|
||||
}
|
||||
|
||||
async function SystemInfo(): Promise<InfoType> {
|
||||
const cpu_usage = await Utils.execAsync(`${App.configDir}/scripts/system.sh --cpu-usage`)
|
||||
.then(str => String(str))
|
||||
.catch(err => {
|
||||
print(err)
|
||||
return "0"
|
||||
});
|
||||
const ram_usage = await Utils.execAsync(`${App.configDir}/scripts/system.sh --ram-usage`)
|
||||
.then(str => String(str))
|
||||
.catch(err => {
|
||||
print(err)
|
||||
return "0"
|
||||
});
|
||||
const swap_usage = await Utils.execAsync(`${App.configDir}/scripts/system.sh --swap-usage`)
|
||||
.then(str => String(str))
|
||||
.catch(err => {
|
||||
print(err)
|
||||
return "0"
|
||||
});
|
||||
const cpu_temp = await Utils.execAsync(`${App.configDir}/scripts/system.sh --cpu-temp`)
|
||||
.then(str => String(str))
|
||||
.catch(err => {
|
||||
print(err)
|
||||
return "0"
|
||||
});
|
||||
return {
|
||||
cpu: cpu_usage,
|
||||
ram: ram_usage,
|
||||
swap: swap_usage,
|
||||
cpu_temp: cpu_temp
|
||||
}
|
||||
}
|
||||
|
||||
const usage = Variable({
|
||||
cpu: "0",
|
||||
ram: "0",
|
||||
swap: "0",
|
||||
cpu_temp: "0"
|
||||
} as unknown as Promise<InfoType>, {
|
||||
poll: [1000, async () => {
|
||||
const result = await SystemInfo()
|
||||
.then(result => result)
|
||||
return result
|
||||
}]
|
||||
})
|
||||
|
||||
function checkBacklight() {
|
||||
const get = Utils.execAsync(`${App.configDir}/scripts/backlight.sh --get`)
|
||||
.then(out => Number(out.trim()))
|
||||
.catch(print);
|
||||
return get
|
||||
}
|
||||
|
||||
function checkBrightness() {
|
||||
const get = Utils.execAsync(`${App.configDir}/scripts/brightness.sh --get`)
|
||||
.then(out => Number(out.trim()))
|
||||
.catch(print);
|
||||
return get
|
||||
}
|
||||
|
||||
const current_backlight = Variable(100, {
|
||||
poll: [500, checkBacklight]
|
||||
})
|
||||
|
||||
const current_brightness = Variable(100, {
|
||||
poll: [500, checkBrightness]
|
||||
})
|
||||
|
||||
const Usage = (
|
||||
name: string, var_name: keyof InfoType,
|
||||
class_name: string | undefined
|
||||
) => {
|
||||
const usage_progress_bar = Widget.ProgressBar({
|
||||
class_name: "usage_bar",
|
||||
value: usage.bind().as(usage => Number(usage[var_name]) / 100),
|
||||
})
|
||||
const usage_overlay = Widget.Overlay({
|
||||
child: usage_progress_bar,
|
||||
overlay: Widget.Box({
|
||||
halign: Gtk.Align.END,
|
||||
valign: Gtk.Align.CENTER,
|
||||
class_name: "usage_bar_point"
|
||||
})
|
||||
})
|
||||
const _usage = Widget.Box({
|
||||
class_name: `${class_name} usage_box`,
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: usage.bind().as(usage => `${name}: ${usage[var_name]}%`),
|
||||
hpack: "start",
|
||||
vpack: "center"
|
||||
}),
|
||||
usage_overlay
|
||||
]
|
||||
})
|
||||
|
||||
return _usage
|
||||
}
|
||||
|
||||
const InfoLabel = (name: string, var_name: keyof InfoType, end: string) => {
|
||||
return Widget.Label({
|
||||
class_name: "info_label",
|
||||
truncate: "end",
|
||||
hpack: "start",
|
||||
label: usage.bind().as(usage => `${name}: ${usage[var_name]}${end}`)
|
||||
})
|
||||
}
|
||||
|
||||
const InfoLabelString = (name: string, value: string, end: string) => {
|
||||
return Widget.Label({
|
||||
class_name: "info_label",
|
||||
truncate: "end",
|
||||
hpack: "start",
|
||||
label: `${name}: ${value}${end}`
|
||||
})
|
||||
}
|
||||
|
||||
export function SystemBox() {
|
||||
const backlight = Widget.Slider({
|
||||
min: 0,
|
||||
max: 100,
|
||||
draw_value: false,
|
||||
class_name: "system_scale backlight",
|
||||
// @ts-ignore
|
||||
value: current_backlight.bind(),
|
||||
on_change: self => {
|
||||
current_backlight.setValue(Number(self.value));
|
||||
Utils.execAsync(`${App.configDir}/scripts/backlight.sh --smooth ${self.value}`)
|
||||
.catch(print);
|
||||
},
|
||||
tooltip_markup: "Backlight"
|
||||
})
|
||||
const brightness = Widget.Slider({
|
||||
min: 0,
|
||||
max: 1,
|
||||
draw_value: false,
|
||||
class_name: "system_scale brightness",
|
||||
// @ts-ignore
|
||||
value: current_brightness.bind(),
|
||||
on_change: self => {
|
||||
current_brightness.setValue(Number(self.value));
|
||||
Utils.execAsync(`${App.configDir}/scripts/brightness.sh --set ${self.value}`)
|
||||
.catch(print);
|
||||
},
|
||||
tooltip_markup: "Brightness"
|
||||
})
|
||||
const slider_box = Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "slider_box",
|
||||
children: [
|
||||
backlight,
|
||||
brightness
|
||||
]
|
||||
})
|
||||
|
||||
const usage_box = Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "usage_box",
|
||||
spacing: 0,
|
||||
children: [
|
||||
Usage("CPU", "cpu", "cpu_usage"),
|
||||
Usage("RAM", "ram", "ram_usage"),
|
||||
Usage("SWAP", "swap", "swap_usage")
|
||||
]
|
||||
})
|
||||
|
||||
const info_box = Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "info_box",
|
||||
spacing: 0,
|
||||
children: [
|
||||
InfoLabel("CPU temp", "cpu_temp", "°C"),
|
||||
InfoLabelString("CPU name", cpu_name, ""),
|
||||
InfoLabelString("CPU cores", cpu_cores, ""),
|
||||
InfoLabelString("RAM amount", amount_of_ram, ""),
|
||||
InfoLabelString("Kernel", kernel_name, "")
|
||||
],
|
||||
vexpand: true
|
||||
})
|
||||
|
||||
return Widget.Scrollable({
|
||||
child: Widget.Box({
|
||||
orientation: Gtk.Orientation.VERTICAL,
|
||||
class_name: "system_box",
|
||||
spacing: 10,
|
||||
children: [
|
||||
slider_box,
|
||||
usage_box,
|
||||
info_box
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user