Add Pinenote Shell widget with control panel
This commit is contained in:
@@ -0,0 +1,193 @@
|
|||||||
|
# PineNote Shell Widget
|
||||||
|
|
||||||
|
A fullscreen GTK widget designed for Pine64 PineNote running Arch Linux with Hyprland, controllable via hyprgrass gestures.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This Astal-based GTK application provides:
|
||||||
|
|
||||||
|
- Screen rotation (portrait/landscape)
|
||||||
|
- Display brightness control (100%, 75%, 25%, 0% steps)
|
||||||
|
- Power operations (suspend, shutdown, reboot)
|
||||||
|
- Quick launch shortcuts for terminal, browser, and e-reader
|
||||||
|
- Display refresh functionality
|
||||||
|
- Audio waveform switching
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **OS**: Arch Linux with Hyprland
|
||||||
|
- **Desktop Environment**: Astal/GJS environment
|
||||||
|
- **Dependencies**: GJS, GTK 4, hyprgrass
|
||||||
|
- **Display**: Pine64 PineNote (or any compatible monitor)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aur install ags-node
|
||||||
|
# or for Arch Linux:
|
||||||
|
sudo pacman -S gjs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clone and Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/projects/pineNoteShell
|
||||||
|
# Project already set up in pinenote-shell/
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
npm install --prefix ./pinenote-shell
|
||||||
|
cd pinenote-shell && npm install
|
||||||
|
|
||||||
|
# Make widget executable
|
||||||
|
chmod +x ./src/main.js
|
||||||
|
gjs --set-mode=auto-start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hyprland Configuration
|
||||||
|
|
||||||
|
Add the following to your `~/.config/hypr/hyprgrass.conf`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[gesture:toggle_widget]
|
||||||
|
hotkeys = [Mod1, T]
|
||||||
|
shorthand = "pinch open"
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
command = "exec ags -t pinenote-shell",
|
||||||
|
timeout = 300,
|
||||||
|
target = "window:pinenote-fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
[gesture:brightness_up]
|
||||||
|
hotkeys = [Mod1, B]
|
||||||
|
shorthand = "swipe up"
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
command = "exec ags --js-evaluate 'globalThis.widget.brightnessController.setBrightness(100)'",
|
||||||
|
timeout = 300,
|
||||||
|
target = "window:pinenote-fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
[gesture:brightness_down]
|
||||||
|
hotkeys = [Mod1, V]
|
||||||
|
shorthand = "swipe down"
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
command = "exec ags --js-evaluate 'globalThis.widget.brightnessController.setBrightness(0)'",
|
||||||
|
timeout = 300,
|
||||||
|
target = "window:pinenote-fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Additional Gestures
|
||||||
|
|
||||||
|
Add more gestures for other controls:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[gesture:rotate_landscape]
|
||||||
|
hotkeys = [Mod1, R]
|
||||||
|
shorthand = "swipe left"
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
command = "exec ags --js-evaluate 'globalThis.widget.screenRotator.rotateTo(\"landscape\")'",
|
||||||
|
timeout = 300,
|
||||||
|
target = "window:pinenote-fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
[gesture:launch_terminal]
|
||||||
|
hotkeys = [Mod1, T]
|
||||||
|
shorthand = "double tap left"
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
command = "exec ags --js-evaluate 'globalThis.widget.launcherManager.launch(\"terminal\")'",
|
||||||
|
timeout = 300,
|
||||||
|
target = "window:pinenote-fullscreen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Controls
|
||||||
|
|
||||||
|
1. **Open/Close**: Activate with hyprgrass gesture (see configuration)
|
||||||
|
2. **Screen Rotation**: Tap landscape/portrait icons to rotate display
|
||||||
|
3. **Brightness**: Click brightness level buttons or use gestures in config
|
||||||
|
4. **System Actions**: Use suspend/shutdown buttons for power operations
|
||||||
|
5. **Launch Apps**: Click terminal, browser, or e-reader shortcuts
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
|
||||||
|
- **Waveform Switching**: Use the waveform control to switch between audio output modes
|
||||||
|
- **Display Refresh**: Force display refresh with the refresh button
|
||||||
|
- **Status Display**: Monitor rotation and brightness levels in footer
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Custom Applications
|
||||||
|
|
||||||
|
Edit `src/main.js` and modify the `LauncherManager.launch()` method:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const launchers = {
|
||||||
|
terminal: "alacritty", // Your preferred terminal
|
||||||
|
browser: "brave-browser", // Or chromium/firefox/edge
|
||||||
|
"e-reader": "calibre", // Your e-reader application
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Common customization options:
|
||||||
|
- `background-color`: Change widget background
|
||||||
|
- `border-radius`, `margin`: Adjust widget borders and spacing
|
||||||
|
- Icon sets: Use different icon themes for better visual consistency
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
- The widget is designed to be lightweight and efficient
|
||||||
|
- For best performance on PineNote, consider reducing animation frequency
|
||||||
|
- Monitor system resources with `htop` or similar tools
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential features for future development:
|
||||||
|
- [ ] Multi-monitor support
|
||||||
|
- [ ] Advanced brightness curves (not just discrete steps)
|
||||||
|
- [ ] Custom app launcher configuration
|
||||||
|
- [ ] Audio waveform visualization
|
||||||
|
- [ ] System monitoring widgets
|
||||||
|
- [ ] Integration with PineNote-specific settings
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is provided as-is for your PineNote setup. Use responsibly and test changes before deploying to production hardware.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "pinenote-shell",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Fullscreen GTK widget for Pine64 PineNote with Hyprland support",
|
||||||
|
"main": "./src/main.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "gjs src/main.js",
|
||||||
|
"dev": "gjs --interpret src/main.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@agi/gtk3": "^0.7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget {
|
||||||
|
background-color: rgba(20, 20, 30, 0.95);
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Ubuntu", "Noto Sans", sans-serif;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .title-big {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #61afef;
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .header {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .section {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #98c379;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .brightness-levelbar {
|
||||||
|
min-height: 200px;
|
||||||
|
width: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .brightness-levelbar.fill-block {
|
||||||
|
background-color: #e5c07b;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .footer {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7f848f;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .status-label {
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget button {
|
||||||
|
background-color: rgba(97, 175, 239, 0.2);
|
||||||
|
border: 1px solid #61afef;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px;
|
||||||
|
color: #ffffff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget button:hover {
|
||||||
|
background-color: rgba(97, 175, 239, 0.4);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .brightness-button {
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget .waveform-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #c678dd;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pinenote-shell-widget Icon {
|
||||||
|
color: #61afef;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user