Migrate pinenote-shell to AGS v3/Astal framework

Replace the custom GJS/GTK4 @agi/gtk3 setup with AGS v3 on GTK3 using
Astal services (Hyprland, Brightness). Updates README installation
instructions, package scripts, and rewrites main.js to use AGS JSX
widgets and reactive state bindings. Adds a minimal package-lock.json.
This commit is contained in:
2026-07-13 13:31:57 +02:00
parent 850a8d0b1c
commit 4938bb472b
4 changed files with 365 additions and 417 deletions
+77 -23
View File
@@ -16,18 +16,22 @@ This Astal-based GTK application provides:
## Requirements
- **OS**: Arch Linux with Hyprland
- **Desktop Environment**: Astal/GJS environment
- **Dependencies**: GJS, GTK 4, hyprgrass
- **Desktop Environment**: AGS (Aylur's GTK Shell) v3 / Astal
- **Dependencies**: GJS, GTK 3, Astal (`libastal-*-git`), `ags`, hyprgrass
- **Display**: Pine64 PineNote (or any compatible monitor)
## Installation
### Prerequisites
Astal and AGS are not distributed via npm. Install the system packages from the AUR:
```bash
aur install ags-node
# or for Arch Linux:
sudo pacman -S gjs
# Install AGS and Astal core libraries (GTK3 + GJS bindings)
yay -S ags libastal-meta libastal-gjs-git libastal-hyprland-git libastal-brightness-git
# Base GTK/Layer-Shell support
sudo pacman -S gjs gtk3 gtk-layer-shell
```
### Clone and Setup
@@ -36,26 +40,48 @@ sudo pacman -S gjs
cd ~/projects/pineNoteShell
# Project already set up in pinenote-shell/
# Install dependencies
npm install --prefix ./pinenote-shell
cd pinenote-shell && npm install
# No npm dependencies are required
cd pinenote-shell
# Make widget executable
chmod +x ./src/main.js
gjs --set-mode=auto-start
```
> **Note:** `npm install` is not needed for this project. The previous `package.json` referenced a package (`@agi/gtk3`) that does not exist on the npm registry.
## Running
The entry point uses an `ags run` shebang, so you can start it directly:
```bash
./src/main.js
```
Or use the `ags` CLI explicitly:
```bash
ags run ./src/main.js
```
You can toggle the window from another terminal:
```bash
ags toggle pinenote-fullscreen -i pinenote-shell
```
### Hyprland Configuration
Add the following to your `~/.config/hypr/hyprgrass.conf`:
> **Note:** AGS v3 uses `ags toggle` / `ags request` instead of the old `--js-evaluate` flag.
```ini
[gesture:toggle_widget]
hotkeys = [Mod1, T]
shorthand = "pinch open"
actions = [
{
command = "exec ags -t pinenote-shell",
command = "exec ags toggle pinenote-fullscreen -i pinenote-shell",
timeout = 300,
target = "window:pinenote-fullscreen"
}
@@ -66,7 +92,7 @@ hotkeys = [Mod1, B]
shorthand = "swipe up"
actions = [
{
command = "exec ags --js-evaluate 'globalThis.widget.brightnessController.setBrightness(100)'",
command = "exec ags request 'brightness 100' -i pinenote-shell",
timeout = 300,
target = "window:pinenote-fullscreen"
}
@@ -77,13 +103,35 @@ hotkeys = [Mod1, V]
shorthand = "swipe down"
actions = [
{
command = "exec ags --js-evaluate 'globalThis.widget.brightnessController.setBrightness(0)'",
command = "exec ags request 'brightness 0' -i pinenote-shell",
timeout = 300,
target = "window:pinenote-fullscreen"
}
]
```
If you want the brightness requests to actually work, add a `requestHandler` to `app.start` in `src/main.js`, for example:
```js
app.start({
instanceName: "pinenote-shell",
css: readFile(cssPath),
requestHandler(argv, response) {
if (argv[0] === "brightness") {
const level = parseInt(argv[1], 10)
brightness.set(level)
response(`brightness set to ${level}`)
}
response("unknown command")
},
main() {
PinenoteShell()
},
})
```
(You'll need to lift the `brightness` manager out of the component scope for this.)
### Additional Gestures
Add more gestures for other controls:
@@ -94,7 +142,7 @@ hotkeys = [Mod1, R]
shorthand = "swipe left"
actions = [
{
command = "exec ags --js-evaluate 'globalThis.widget.screenRotator.rotateTo(\"landscape\")'",
command = "exec ags request 'rotate landscape' -i pinenote-shell",
timeout = 300,
target = "window:pinenote-fullscreen"
}
@@ -105,7 +153,7 @@ hotkeys = [Mod1, T]
shorthand = "double tap left"
actions = [
{
command = "exec ags --js-evaluate 'globalThis.widget.launcherManager.launch(\"terminal\")'",
command = "exec ags request 'launch terminal' -i pinenote-shell",
timeout = 300,
target = "window:pinenote-fullscreen"
}
@@ -144,14 +192,19 @@ const launchers = {
### Hyprland Integration
The widget uses hyprctl commands to communicate with:
- **Screen rotation**: Monitors current rotation via `hyprland.active.monitor.rotation`
- **Display refresh**: Uses `hyprctl keyword reload` for display updates
- **Brightness**: Controls through `/sys/class/backlight/` interface
The widget uses `hyprctl` and the `AstalHyprland` library to communicate with Hyprland:
- **Screen rotation**: Monitors the focused monitor's `transform` property
- **Display refresh**: Uses `pdlc-cli --refresh` for PineNote e-ink refresh
- **Brightness**: Uses `AstalBrightness` (`/sys/class/backlight/`)
## Styling
The widget uses CSS styling loaded from `$HOME/.config/ags/style.css`. You can customize the appearance by modifying this file or creating your own theme.
The widget uses CSS styling loaded from `$HOME/.config/ags/style.css`. The `style.css` file in this directory is the source of truth; copy or symlink it there before running:
```bash
mkdir -p ~/.config/ags
cp ./style.css ~/.config/ags/style.css
```
Common customization options:
- `background-color`: Change widget background
@@ -162,15 +215,16 @@ Common customization options:
### Widget Won't Appear
1. **Check Astal installation**: Ensure gjs and ags are installed correctly
2. **Verify hyprgrass config**: Make sure your gesture bindings are correct
3. **Debug output**: Run with `gjs -d src/main.js` to see console errors
1. **Check Astal/AGS installation**: Ensure `ags`, `gjs`, and the Astal libraries are installed correctly
2. **Verify CSS path**: Make sure `~/.config/ags/style.css` exists; otherwise the app will crash on start
3. **Verify hyprgrass config**: Make sure your gesture bindings are correct
4. **Debug output**: Run with `ags run ./src/main.js` in a terminal to see console errors
### Controls Not Working
1. **Permission issues**: Some operations may require appropriate permissions
2. **Missing applications**: Ensure your preferred terminal/browser exist
3. **Hyprland compatibility**: Test basic hyprctl commands manually first
3. **Hyprland compatibility**: Test basic `hyprctl` commands manually first
### Performance Issues
+12
View File
@@ -0,0 +1,12 @@
{
"name": "pinenote-shell",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pinenote-shell",
"version": "1.0.0"
}
}
}
+3 -5
View File
@@ -5,10 +5,8 @@
"main": "./src/main.js",
"type": "module",
"scripts": {
"start": "gjs src/main.js",
"dev": "gjs --interpret src/main.js"
"start": "ags run src/main.js",
"dev": "ags run src/main.js"
},
"dependencies": {
"@agi/gtk3": "^0.7.4"
}
"dependencies": {}
}
+301 -417
View File
@@ -1,272 +1,175 @@
//!/usr/bin/env -S gjs -m
#!/usr/bin/env -S ags run
import GLib from "gi://GLib"
import app from "ags/gtk3/app"
import { Astal, Gtk, Gdk } from "ags/gtk3"
import { execAsync } from "ags/process"
import { readFile } from "ags/file"
import { createBinding, createState, createComputed } from "ags"
import Hyprland from "gi://AstalHyprland"
import Brightness from "gi://AstalBrightness"
const { Gdk, Gtk } = imports.gi;
const { App, Astal, Hyprland, Widget, Utils, GObject, exec, execAsync } = await import('@agi/gtk3');
const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor
class PinenoteShell extends App {
const userHome = GLib.get_home_dir()
const cssPath = `${userHome}/.config/ags/style.css`
// --- helpers --------------------------------------------------------------
const notify = (msg) => execAsync(["notify-send", msg]).catch(console.error)
const toTitleCase = (str) =>
str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.slice(1).toLowerCase())
// --- managers --------------------------------------------------------------
class BrightnessController {
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();
this.astal = Brightness.get_default()
this.screen = this.astal?.screen
}
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",
})
}),
]
}),
})
);
get current() {
if (!this.screen) return 0
return Math.round(this.screen.brightness * 100)
}
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}`,
}));
set(level) {
if (!this.screen) return
this.screen.brightness = level / 100
}
}
createStatusLabel() {
let status = [];
if (this.hyprland.active.monitor) {
const monitor = this.hyprland.active.monitor;
status.push(`Rotation: ${monitor.rotation || "auto"}`);
class ScreenRotator {
constructor(hyprland) {
this.hyprland = hyprland
}
status.push(`Brightness: ${this.brightnessController.currentBrightness}%`);
async rotateTo(rotation) {
try {
const monitor = this.hyprland.focused_monitor
if (!monitor) return
return status.join(" | ");
const transform =
rotation === "landscape"
? Hyprland.Monitor.Transform.NORMAL
: Hyprland.Monitor.Transform.ROTATE_270_DEG
// Hyprland expects keyword monitor <name>,transform,<value>
await execAsync([
"hyprctl",
"keyword",
"monitor",
`${monitor.name},transform,${transform}`,
])
notify(`Screen rotated to ${rotation}`)
} catch (err) {
notify(`Failed to rotate screen: ${err.message}`)
}
}
}
closeWidget() {
const window = this.getWindow("fullscreen", "pinenote-fullscreen");
if (window) window.setVisible(false);
class PowerManager {
async suspend() {
try {
await execAsync(["systemctl", "suspend"])
} catch (err) {
notify(`Failed to suspend: ${err.message}`)
}
}
toggleVisibility() {
const window = this.getWindow("fullscreen", "pinenote-fullscreen");
if (window) window.setVisible(!window.isVisible());
async shutdown() {
try {
await execAsync(["systemctl", "poweroff"])
} catch (err) {
notify(`Failed to power off: ${err.message}`)
}
}
showPowerMenu() {
async reboot() {
try {
await execAsync(["systemctl", "reboot"])
} catch (err) {
notify(`Failed to reboot: ${err.message}`)
}
}
}
class LauncherManager {
launch(appType) {
const launchers = {
terminal: "alacritty",
browser: "brave-browser",
"e-reader": "calibre",
}
const cmd = launchers[appType]
if (!cmd) return
execAsync([cmd]).catch((err) => notify(`Failed to launch ${appType}: ${err.message}`))
notify(`${toTitleCase(appType)} launched`)
}
}
class RefreshController {
async triggerRefresh() {
try {
await execAsync(["pdlc-cli", "--refresh"])
notify("E-ink display refreshed")
} catch (err) {
console.log(`Failed to refresh display: ${err.message}`)
}
}
}
class WaveformController {
constructor() {
this.waveforms = ["gc16", "gc42", "a2", "full"]
this.current = createState(this.waveforms[0])
}
setWaveform(index) {
const idx = Math.round(index)
if (idx < 0 || idx >= this.waveforms.length) return
const waveform = this.waveforms[idx]
this.current[1](waveform)
notify(`E-ink waveform: ${waveform}`)
execAsync(["pdlc-cli", "--waveform", waveform]).catch((err) =>
console.log(`Could not apply waveform: ${err.message}`),
)
}
}
// --- widget ----------------------------------------------------------------
function PinenoteShell() {
const hyprland = Hyprland.get_default()
const brightness = new BrightnessController()
const rotator = new ScreenRotator(hyprland)
const power = new PowerManager()
const launcher = new LauncherManager()
const refresh = new RefreshController()
const waveform = new WaveformController()
const [visible, setVisible] = createState(false)
const brightnessLevel = brightness.screen
? createBinding(brightness.screen, "brightness")((v) => Math.round((v || 0) * 100))
: createState(100)[0]
const currentWaveform = waveform.current[0]
function closeWidget() {
setVisible(false)
}
function toggleVisibility() {
setVisible((v) => !v)
}
function showPowerMenu() {
const dialog = new Gtk.MessageDialog({
transient_for: null,
modal: true,
@@ -275,182 +178,163 @@ class PinenoteShell extends App {
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.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.connect("response", (_dialog, response) => {
if (response === Gtk.ResponseType.YES) power.reboot()
else if (response === Gtk.ResponseType.OK) power.shutdown()
_dialog.close()
})
dialog.show();
}
dialog.show()
}
class BrightnessController {
constructor() {
this.currentBrightness = 100;
this.updateFromSystem();
function BrightnessControls() {
const levels = [100, 75, 25, 0]
return levels.map((level) => (
<button
class={`brightness-button brightness-${level}`}
onClicked={() => brightness.set(level)}
>
<label label={`${level}%`} />
</button>
))
}
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}`);
function StatusLabel() {
const focusedMonitor = createBinding(hyprland, "focused-monitor")
const transform = createComputed(() => {
const monitor = focusedMonitor()
if (!monitor) return "auto"
const t = monitor.transform
return t === Hyprland.Monitor.Transform.NORMAL ? "landscape" : "portrait"
})
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;
}
const status = createComputed(() => `Rotation: ${transform()} | Brightness: ${brightnessLevel()}%`)
return <label class="status-label" label={status} />
}
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));
return (
<window
name="pinenote-fullscreen"
namespace="fullscreen"
visible={visible}
anchor={TOP | BOTTOM | LEFT | RIGHT}
exclusivity={Astal.Exclusivity.IGNORE}
keymode={Astal.Keymode.EXCLUSIVE}
onButtonPressEvent={(self, event) => {
const [, x, y] = event.get_coords()
const { x: cx, y: cy, width, height } = self.get_child().get_allocation()
const outside = x < cx || x > cx + width || y < cy || y > cy + height
if (outside) closeWidget()
}}
onKeyPressEvent={(self, event) => {
if (event.get_keyval()[1] === Gdk.KEY_Escape) closeWidget()
}}
>
<box class="pinenote-shell-widget" vertical>
<centerbox class="header">
<label $type="start" class="title-big" label="PineNote Control Panel" />
<box $type="end">
<button class="close-button" onClicked={closeWidget}>
<icon icon="window-close" />
</button>
</box>
</centerbox>
<box vertical homogeneous={false}>
{/* Screen Rotation */}
<box class="section" vertical>
<label class="title" label="Screen Rotation" />
<box homogeneous={false}>
<button onClicked={() => rotator.rotateTo("landscape")}>
<icon icon="view-normal" />
</button>
<button onClicked={() => rotator.rotateTo("portrait")}>
<icon icon="camera-image" />
</button>
</box>
</box>
{/* Brightness */}
<box class="section" vertical>
<label class="title" label="Display Brightness" />
<box homogeneous={false}>{BrightnessControls()}</box>
<levelbar
vertical
class="brightness-levelbar"
value={brightnessLevel((v) => v / 100)}
min={0}
max={1}
/>
</box>
{/* Quick Launch */}
<box class="section" vertical>
<label class="title" label="Launch Applications" />
<box homogeneous={false}>
<button onClicked={() => launcher.launch("terminal")}>
<icon icon="terminal" />
</button>
<button onClicked={() => launcher.launch("browser")}>
<icon icon="web-browser" />
</button>
<button onClicked={() => launcher.launch("e-reader")}>
<icon icon="document-open" />
</button>
</box>
</box>
{/* Display & Waveform */}
<box class="section" vertical>
<label class="title" label="Display &amp; Waveform" />
<box homogeneous={false}>
<button onClicked={() => refresh.triggerRefresh()}>
<icon icon="view-refresh" />
</button>
<box vertical>
<label class="waveform-label" label={currentWaveform} />
<spinbutton
adjustment={new Gtk.Adjustment({ lower: 0, upper: 3, step_increment: 1 })}
digits={0}
onValueChanged={({ value }) => waveform.setWaveform(value)}
/>
</box>
</box>
</box>
{/* System */}
<box class="section" vertical>
<label class="title" label="System Actions" />
<box homogeneous={false}>
<button onClicked={() => power.suspend()}>
<icon icon="system-suspend" />
</button>
<button onClicked={showPowerMenu}>
<icon icon="system-shutdown" />
</button>
</box>
</box>
</box>
<centerbox class="footer">
<StatusLabel $type="start" />
</centerbox>
</box>
</window>
)
}
this.currentBrightness = level;
// --- entry point -----------------------------------------------------------
// 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();
app.start({
instanceName: "pinenote-shell",
css: readFile(cssPath),
main() {
PinenoteShell()
},
})