mirror of
http://100.103.83.12:3003/fegger/pineNoteShell.git
synced 2026-09-17 11:32:44 +00:00
Add Pinenote Shell widget with control panel
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
//!/usr/bin/env -S gjs -m
|
||||
|
||||
const { Gdk, Gtk } = imports.gi;
|
||||
const { App, Astal, Hyprland, Widget, Utils, GObject, exec, execAsync } = await import('@agi/gtk3');
|
||||
|
||||
class PinenoteShell extends App {
|
||||
constructor() {
|
||||
super({
|
||||
id: "pinenote-shell",
|
||||
name: "pinenote.shell",
|
||||
instance: "pinenote-shell",
|
||||
css: Utils.readFile(`${Utils.userHome}/.config/ags/style.css`),
|
||||
// No hot corner needed - hyprgrass gestures control this
|
||||
});
|
||||
|
||||
this.hyprland = new Hyprland({
|
||||
explicitBlur: true,
|
||||
});
|
||||
|
||||
this.brightnessController = new BrightnessController();
|
||||
this.screenRotator = new ScreenRotator(this.hyprland);
|
||||
this.powerManager = new PowerManager();
|
||||
this.launcherManager = new LauncherManager();
|
||||
this.refreshController = new RefreshController();
|
||||
this.waveformController = new WaveformController();
|
||||
|
||||
this.buildInterface();
|
||||
}
|
||||
|
||||
buildInterface() {
|
||||
const centerBox = Widget.Box({
|
||||
vertical: false,
|
||||
children: [
|
||||
// Screen Rotation Section
|
||||
Widget.Box({
|
||||
className: "section",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "Screen Rotation",
|
||||
className: "title",
|
||||
}),
|
||||
Widget.Box({
|
||||
homogeneous: false,
|
||||
children: [
|
||||
Widget.Button({
|
||||
onClicked: () => this.screenRotator.rotateTo("landscape"),
|
||||
child: Widget.Icon({
|
||||
icon: "view-normal",
|
||||
size: 40,
|
||||
})
|
||||
}),
|
||||
Widget.Button({
|
||||
onClicked: () => this.screenRotator.rotateTo("portrait"),
|
||||
child: Widget.Icon({
|
||||
icon: "camera-image",
|
||||
size: 40,
|
||||
})
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
}),
|
||||
|
||||
// Brightness Control Section
|
||||
Widget.Box({
|
||||
className: "section",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "Display Brightness",
|
||||
className: "title",
|
||||
}),
|
||||
Widget.Box({
|
||||
homogeneous: false,
|
||||
children: this.createBrightnessControls(),
|
||||
}),
|
||||
Widget.LevelBar({
|
||||
value: this.brightnessController.currentBrightness / 100,
|
||||
minValue: 0,
|
||||
maxValue: 1,
|
||||
vertical: true,
|
||||
className: "brightness-levelbar",
|
||||
}),
|
||||
]
|
||||
}),
|
||||
|
||||
// Quick Launch Section
|
||||
Widget.Box({
|
||||
className: "section",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "Launch Applications",
|
||||
className: "title",
|
||||
}),
|
||||
Widget.Box({
|
||||
homogeneous: false,
|
||||
children: [
|
||||
Widget.Button({
|
||||
onClicked: () => this.launcherManager.launch("terminal"),
|
||||
child: Widget.Icon({ icon: "terminal", size: 40 }),
|
||||
}),
|
||||
Widget.Button({
|
||||
onClicked: () => this.launcherManager.launch("browser"),
|
||||
child: Widget.Icon({ icon: "web-browser", size: 40 }),
|
||||
}),
|
||||
Widget.Button({
|
||||
onClicked: () => this.launcherManager.launch("e-reader"),
|
||||
child: Widget.Icon({ icon: "document-open", size: 40 }),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
}),
|
||||
|
||||
// Display & Waveform Controls Section
|
||||
Widget.Box({
|
||||
className: "section",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "Display & Waveform",
|
||||
className: "title",
|
||||
}),
|
||||
Widget.Box({
|
||||
homogeneous: false,
|
||||
children: [
|
||||
// Refresh Controls
|
||||
Widget.Button({
|
||||
onClicked: () => this.refreshController.triggerRefresh(),
|
||||
child: Widget.Icon({ icon: "view-refresh", size: 40 }),
|
||||
}),
|
||||
// Waveform Switch (e-ink display mode)
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: this.waveformController.currentWaveform,
|
||||
className: "waveform-label",
|
||||
}),
|
||||
Widget.SpinButton({
|
||||
adjustment: new Gtk.Adjustment(0, 0, 3, 1, 1),
|
||||
digits: 0,
|
||||
onValueChanged: (spin) => {
|
||||
this.waveformController.setWaveform(spin.getValue());
|
||||
}
|
||||
}),
|
||||
]
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
}),
|
||||
|
||||
// System Controls Section
|
||||
Widget.Box({
|
||||
className: "section",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
label: "System Actions",
|
||||
className: "title",
|
||||
}),
|
||||
Widget.Box({
|
||||
homogeneous: false,
|
||||
children: [
|
||||
Widget.Button({
|
||||
onClicked: () => this.powerManager.suspend(),
|
||||
child: Widget.Icon({ icon: "system-suspend", size: 40 }),
|
||||
}),
|
||||
Widget.Button({
|
||||
onClicked: () => this.showPowerMenu(),
|
||||
child: Widget.Icon({ icon: "system-shutdown", size: 40 }),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
]
|
||||
}),
|
||||
].flat(Infinity), // Flatten nested arrays
|
||||
});
|
||||
|
||||
this.addWindow(
|
||||
Astal.Window({
|
||||
namespace: "fullscreen",
|
||||
name: "pinenote-fullscreen",
|
||||
anchor: [Astal.WindowAnchor.TOP, Astal.WindowAnchor.BOTTOM,
|
||||
Astal.WindowAnchor.LEFT, Astal.WindowAnchor.RIGHT],
|
||||
exclusivity: Astal.Exclusivity.NORMAL,
|
||||
visible: false,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
cssClasses: ["pinenote-shell-widget"],
|
||||
setup: (box) => {
|
||||
// Click outside content area to close
|
||||
box.get_parent().connect("button-release-event", () => {
|
||||
this.closeWidget();
|
||||
return true;
|
||||
});
|
||||
},
|
||||
children: [
|
||||
// Header
|
||||
Widget.CenterBox({
|
||||
className: "header",
|
||||
startWidget: Widget.Label({
|
||||
label: "PineNote Control Panel",
|
||||
className: "title-big",
|
||||
}),
|
||||
endWidget: Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
onClicked: () => this.closeWidget(),
|
||||
child: Widget.Icon({ icon: "window-close", size: 20 }),
|
||||
className: "close-button",
|
||||
})
|
||||
]
|
||||
})
|
||||
}),
|
||||
|
||||
// Main Content
|
||||
centerBox,
|
||||
|
||||
// Footer with status
|
||||
Widget.CenterBox({
|
||||
className: "footer",
|
||||
startWidget: Widget.Label({
|
||||
label: this.createStatusLabel(),
|
||||
className: "status-label",
|
||||
})
|
||||
}),
|
||||
]
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
createBrightnessControls() {
|
||||
const levels = [100, 75, 25, 0];
|
||||
return levels.map((level) => Widget.Button({
|
||||
onClicked: () => this.brightnessController.setBrightness(level),
|
||||
child: Widget.Label({ label: `${level}%` }),
|
||||
className: `brightness-button brightness-${level}`,
|
||||
}));
|
||||
}
|
||||
|
||||
createStatusLabel() {
|
||||
let status = [];
|
||||
|
||||
if (this.hyprland.active.monitor) {
|
||||
const monitor = this.hyprland.active.monitor;
|
||||
status.push(`Rotation: ${monitor.rotation || "auto"}`);
|
||||
}
|
||||
|
||||
status.push(`Brightness: ${this.brightnessController.currentBrightness}%`);
|
||||
|
||||
return status.join(" | ");
|
||||
}
|
||||
|
||||
closeWidget() {
|
||||
const window = this.getWindow("fullscreen", "pinenote-fullscreen");
|
||||
if (window) window.setVisible(false);
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
const window = this.getWindow("fullscreen", "pinenote-fullscreen");
|
||||
if (window) window.setVisible(!window.isVisible());
|
||||
}
|
||||
|
||||
showPowerMenu() {
|
||||
const dialog = new Gtk.MessageDialog({
|
||||
transient_for: null,
|
||||
modal: true,
|
||||
destroy_with_parent: true,
|
||||
message_type: Gtk.MessageType.QUESTION,
|
||||
buttons: Gtk.ButtonsType.YES_NO,
|
||||
default_button: Gtk.ResponseType.NO,
|
||||
secondary_text: "Choose an action:",
|
||||
});
|
||||
|
||||
dialog.add_button("Reboot", Gtk.ResponseType.YES);
|
||||
dialog.add_button("Power Off", Gtk.ResponseType.OK);
|
||||
|
||||
dialog.response.connect((_dialog, response) => {
|
||||
if (response === Gtk.ResponseType.YES) {
|
||||
this.powerManager.reboot();
|
||||
} else if (response === Gtk.ResponseType.OK) {
|
||||
this.powerManager.shutdown();
|
||||
}
|
||||
_dialog.close();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
class BrightnessController {
|
||||
constructor() {
|
||||
this.currentBrightness = 100;
|
||||
this.updateFromSystem();
|
||||
}
|
||||
|
||||
async updateFromSystem() {
|
||||
try {
|
||||
// Try to read from /sys/class/backlight
|
||||
const backlightFiles = await Utils.execAsync('find /sys/class/backlight -name "brightness" | head -1');
|
||||
if (backlightFiles) {
|
||||
const maxFile = await Utils.execAsync(`grep max_brightness ${backlightFiles}`);
|
||||
const currentFile = await Utils.execAsync(`cat ${backlightFiles}`);
|
||||
|
||||
this.maxBrightness = parseInt(maxFile.split(' ')[1] || 0);
|
||||
this.currentBrightness = Math.round(parseInt(currentFile) / this.maxBrightness * 100);
|
||||
}
|
||||
} catch (err) {
|
||||
// Keep default brightness if cannot read from system
|
||||
this.maxBrightness = 100;
|
||||
}
|
||||
}
|
||||
|
||||
async setBrightness(level) {
|
||||
try {
|
||||
const backlightFiles = await Utils.execAsync('find /sys/class/backlight -name "brightness" | head -1');
|
||||
if (backlightFiles && level > 0) {
|
||||
const brightnessPath = `${backlightFiles}`;
|
||||
await Utils.writeFile(brightnessPath, Math.round(level * this.maxBrightness / 100));
|
||||
}
|
||||
|
||||
this.currentBrightness = level;
|
||||
|
||||
// Send notification via hyprland
|
||||
exec(`notify-send "Brightness set to ${level}%"`);
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to set brightness: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScreenRotator {
|
||||
constructor(hyprland) {
|
||||
this.hyprland = hyprland;
|
||||
}
|
||||
|
||||
async rotateTo(rotation) {
|
||||
try {
|
||||
if (this.hyprland.active.monitor) {
|
||||
const monitor = this.hyprland.active.monitor;
|
||||
let hyprlandRotation;
|
||||
|
||||
switch (rotation) {
|
||||
case "landscape":
|
||||
hyprlandRotation = 0;
|
||||
break;
|
||||
case "portrait":
|
||||
hyprlandRotation = 3; // 270 degrees in Hyprland's coordinate system
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// Use hyprctl to set monitor rotation
|
||||
await Utils.execAsync(`hyprctl keyword -- monitor "${monitor.id}" rotate ${hyprlandRotation}`);
|
||||
|
||||
exec(`notify-send "Screen rotated to ${rotation}"`);
|
||||
}
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to rotate screen: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PowerManager {
|
||||
constructor() {}
|
||||
|
||||
async suspend() {
|
||||
try {
|
||||
await Utils.execAsync('systemctl suspend');
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to suspend: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
try {
|
||||
await Utils.execAsync('systemctl poweroff');
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to power off: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
|
||||
async reboot() {
|
||||
try {
|
||||
await Utils.execAsync('systemctl reboot');
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to reboot: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LauncherManager {
|
||||
constructor() {}
|
||||
|
||||
async launch(appType) {
|
||||
const launchers = {
|
||||
terminal: "alacritty", // Or your preferred terminal emulator
|
||||
browser: "brave-browser", // Or chromium/firefox
|
||||
"e-reader": "calibre", // Or your e-reader app
|
||||
};
|
||||
|
||||
try {
|
||||
exec(`nohup ${launchers[appType]} &`);
|
||||
exec(`notify-send "${Utils.toTitleCase(appType)} launched"`);
|
||||
} catch (err) {
|
||||
exec(`notify-send "Failed to launch ${appType}: ${err.message}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RefreshController {
|
||||
constructor() {}
|
||||
|
||||
async triggerRefresh() {
|
||||
try {
|
||||
// Trigger a full e-ink display refresh via pdlc-cli
|
||||
await Utils.execAsync('pdlc-cli --refresh 2>/dev/null');
|
||||
exec(`notify-send "E-ink display refreshed"`);
|
||||
} catch (err) {
|
||||
console.log(`Failed to refresh display: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WaveformController {
|
||||
constructor() {
|
||||
// PineNote uses the PDLC-EPD display controller
|
||||
// Waveforms control the e-ink refresh behavior
|
||||
this.currentWaveform = "gc16"; // Default: balanced quality/speed
|
||||
this.waveforms = ["gc16", "gc42", "a2", "full"];
|
||||
}
|
||||
|
||||
setWaveform(index) {
|
||||
const idx = Math.round(index);
|
||||
if (idx >= 0 && idx < this.waveforms.length) {
|
||||
this.currentWaveform = this.waveforms[idx];
|
||||
exec(`notify-send "E-ink waveform: ${this.currentWaveform}"`);
|
||||
|
||||
// Apply waveform via pdlc-cli (PineNote display controller)
|
||||
try {
|
||||
Utils.execAsync(`pdlc-cli --waveform ${this.currentWaveform} 2>/dev/null || true`);
|
||||
} catch (err) {
|
||||
console.log(`Could not apply waveform: ${this.currentWaveform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
App.addIcons(`${Utils.userHome}/.config/ags/icons`);
|
||||
new PinenoteShell();
|
||||
Reference in New Issue
Block a user