Added ags to hyprland
This commit is contained in:
154
home/desktops/hyprland/ags/services/booru.js
Normal file
154
home/desktops/hyprland/ags/services/booru.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const APISERVICES = {
|
||||
'yandere': {
|
||||
name: 'yande.re',
|
||||
endpoint: 'https://yande.re/post.json',
|
||||
},
|
||||
'konachan': {
|
||||
name: 'Konachan',
|
||||
endpoint: 'https://konachan.net/post.json',
|
||||
},
|
||||
}
|
||||
|
||||
const getWorkingImageSauce = (url) => {
|
||||
if (url.includes('pximg.net')) {
|
||||
return `https://www.pixiv.net/en/artworks/${url.substring(url.lastIndexOf('/') + 1).replace(/_p\d+\.(png|jpg|jpeg|gif)$/, '')}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function paramStringFromObj(params) {
|
||||
return Object.entries(params)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) { // If it's an array, repeat
|
||||
if (value.length == 0) return '';
|
||||
let thisKey = `${encodeURIComponent(key)}=${encodeURIComponent(value[0])}`
|
||||
for (let i = 1; i < value.length; i++) {
|
||||
thisKey += `&${encodeURIComponent(key)}=${encodeURIComponent(value[i])}`;
|
||||
}
|
||||
return thisKey;
|
||||
}
|
||||
return `${key}=${value}`;
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
class BooruService extends Service {
|
||||
_baseUrl = 'https://yande.re/post.json';
|
||||
_mode = 'yandere';
|
||||
_nsfw = userOptions.sidebar.image.allowNsfw;
|
||||
_responses = [];
|
||||
_queries = [];
|
||||
|
||||
static {
|
||||
Service.register(this, {
|
||||
'initialized': [],
|
||||
'clear': [],
|
||||
'newResponse': ['int'],
|
||||
'updateResponse': ['int'],
|
||||
}, {
|
||||
'nsfw': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.emit('initialized');
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._responses = [];
|
||||
this._queries = [];
|
||||
this.emit('clear');
|
||||
}
|
||||
|
||||
get nsfw() { return this._nsfw }
|
||||
set nsfw(value) { this._nsfw = value; this.notify('nsfw'); }
|
||||
|
||||
get mode() { return this._mode }
|
||||
set mode(value) {
|
||||
this._mode = value;
|
||||
this._baseUrl = APISERVICES[this._mode].endpoint;
|
||||
}
|
||||
get providerName() {
|
||||
return APISERVICES[this._mode].name;
|
||||
}
|
||||
get queries() { return this._queries }
|
||||
get responses() { return this._responses }
|
||||
|
||||
async fetch(msg) {
|
||||
// Init
|
||||
const userArgs = `${msg}${(!this._nsfw || msg.includes('safe')) ? ' rating:safe' : ''}`.split(/\s+/);
|
||||
console.log(userArgs)
|
||||
|
||||
let taglist = [];
|
||||
let page = 1;
|
||||
// Construct body/headers
|
||||
for (let i = 0; i < userArgs.length; i++) {
|
||||
const thisArg = userArgs[i].trim();
|
||||
if (thisArg.length == 0 || thisArg == '.' || thisArg.includes('*')) continue;
|
||||
else if (!isNaN(thisArg)) page = parseInt(thisArg);
|
||||
else taglist.push(thisArg);
|
||||
}
|
||||
const newMessageId = this._queries.length;
|
||||
this._queries.push({
|
||||
providerName: APISERVICES[this._mode].name,
|
||||
taglist: taglist.length == 0 ? ['*', `${page}`] : [...taglist, `${page}`],
|
||||
realTagList: taglist,
|
||||
page: page,
|
||||
});
|
||||
this.emit('newResponse', newMessageId);
|
||||
const params = {
|
||||
'tags': taglist.join('+'),
|
||||
'page': `${page}`,
|
||||
'limit': `${userOptions.sidebar.image.batchCount}`,
|
||||
};
|
||||
const paramString = paramStringFromObj(params);
|
||||
// Fetch
|
||||
// Note: body isn't included since passing directly to url is more reliable
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: APISERVICES[this._mode].headers,
|
||||
};
|
||||
let status = 0;
|
||||
// console.log(`${APISERVICES[this._mode].endpoint}?${paramString}`);
|
||||
|
||||
Utils.fetch(`${APISERVICES[this._mode].endpoint}?${paramString}`, options)
|
||||
.then(result => {
|
||||
status = result.status;
|
||||
return result.text();
|
||||
})
|
||||
.then((dataString) => { // Store interesting stuff and emit
|
||||
// console.log(dataString);
|
||||
const parsedData = JSON.parse(dataString);
|
||||
// console.log(parsedData)
|
||||
this._responses[newMessageId] = parsedData.map(obj => {
|
||||
return {
|
||||
aspect_ratio: obj.width / obj.height,
|
||||
id: obj.id,
|
||||
tags: obj.tags,
|
||||
md5: obj.md5,
|
||||
preview_url: obj.preview_url,
|
||||
preview_width: obj.preview_width,
|
||||
preview_height: obj.preview_height,
|
||||
sample_url: obj.sample_url,
|
||||
sample_width: obj.sample_width,
|
||||
sample_height: obj.sample_height,
|
||||
file_url: obj.file_url,
|
||||
file_ext: obj.file_ext,
|
||||
file_width: obj.file_width,
|
||||
file_height: obj.file_height,
|
||||
source: getWorkingImageSauce(obj.source),
|
||||
}
|
||||
});
|
||||
this.emit('updateResponse', newMessageId);
|
||||
})
|
||||
.catch(print);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default new BooruService();
|
||||
|
||||
143
home/desktops/hyprland/ags/services/brightness.js
Normal file
143
home/desktops/hyprland/ags/services/brightness.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
const { exec, execAsync } = Utils;
|
||||
|
||||
import { clamp } from '../modules/.miscutils/mathfuncs.js';
|
||||
|
||||
class BrightnessServiceBase extends Service {
|
||||
static {
|
||||
Service.register(
|
||||
this,
|
||||
{ 'screen-changed': ['float'], },
|
||||
{ 'screen-value': ['float', 'rw'], },
|
||||
);
|
||||
}
|
||||
|
||||
_screenValue = 0;
|
||||
|
||||
// the getter has to be in snake_case
|
||||
get screen_value() { return this._screenValue; }
|
||||
|
||||
// the setter has to be in snake_case too
|
||||
set screen_value(percent) {
|
||||
percent = clamp(percent, 0, 1);
|
||||
this._screenValue = percent;
|
||||
|
||||
Utils.execAsync(this.setBrightnessCmd(percent))
|
||||
.then(() => {
|
||||
// signals has to be explicity emitted
|
||||
this.emit('screen-changed', percent);
|
||||
this.notify('screen-value');
|
||||
|
||||
// or use Service.changed(propName: string) which does the above two
|
||||
// this.changed('screen');
|
||||
})
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
// overwriting connectWidget method, lets you
|
||||
// change the default event that widgets connect to
|
||||
connectWidget(widget, callback, event = 'screen-changed') {
|
||||
super.connectWidget(widget, callback, event);
|
||||
}
|
||||
}
|
||||
|
||||
class BrightnessCtlService extends BrightnessServiceBase {
|
||||
static {
|
||||
Service.register(this);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const current = Number(exec('brightnessctl g'));
|
||||
const max = Number(exec('brightnessctl m'));
|
||||
this._screenValue = current / max;
|
||||
}
|
||||
|
||||
setBrightnessCmd(percent) {
|
||||
return `brightnessctl s ${percent * 100}% -q`;
|
||||
}
|
||||
}
|
||||
|
||||
class BrightnessDdcService extends BrightnessServiceBase {
|
||||
static {
|
||||
Service.register(this);
|
||||
}
|
||||
|
||||
constructor(busNum) {
|
||||
super();
|
||||
this._busNum = busNum;
|
||||
Utils.execAsync(`ddcutil -b ${this._busNum} getvcp 10 --brief`)
|
||||
.then((out) => {
|
||||
// only the last line is useful
|
||||
out = out.split('\n');
|
||||
out = out[out.length - 1];
|
||||
|
||||
out = out.split(' ');
|
||||
const current = Number(out[3]);
|
||||
const max = Number(out[4]);
|
||||
this._screenValue = current / max;
|
||||
})
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
setBrightnessCmd(percent) {
|
||||
return `ddcutil -b ${this._busNum} setvcp 10 ${Math.round(percent * 100)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDdcMonitorsSnBus() {
|
||||
let ddcSnBus = {};
|
||||
try {
|
||||
const out = await Utils.execAsync('ddcutil detect --brief');
|
||||
const displays = out.split('\n\n');
|
||||
displays.forEach(display => {
|
||||
const reg = /^Display \d+/;
|
||||
if (!reg.test(display))
|
||||
return;
|
||||
const lines = display.split('\n');
|
||||
const sn = lines[3].split(':')[3];
|
||||
const busNum = lines[1].split('/dev/i2c-')[1];
|
||||
ddcSnBus[sn] = busNum;
|
||||
});
|
||||
} catch (err) {
|
||||
print(err);
|
||||
}
|
||||
return ddcSnBus;
|
||||
}
|
||||
|
||||
// Service instance
|
||||
const numMonitors = Hyprland.monitors.length;
|
||||
const service = Array(numMonitors);
|
||||
const ddcSnBus = await listDdcMonitorsSnBus();
|
||||
for (let i = 0; i < service.length; i++) {
|
||||
const monitorName = Hyprland.monitors[i].name;
|
||||
const monitorSn = Hyprland.monitors[i].serial;
|
||||
const preferredController = userOptions.brightness.controllers[monitorName]
|
||||
|| userOptions.brightness.controllers.default || "auto";
|
||||
if (preferredController) {
|
||||
switch (preferredController) {
|
||||
case "brightnessctl":
|
||||
service[i] = new BrightnessCtlService();
|
||||
break;
|
||||
case "ddcutil":
|
||||
service[i] = new BrightnessDdcService(ddcSnBus[monitorSn]);
|
||||
break;
|
||||
case "auto":
|
||||
if (monitorSn in ddcSnBus && !!exec(`bash -c 'command -v ddcutil'`))
|
||||
service[i] = new BrightnessDdcService(ddcSnBus[monitorSn]);
|
||||
else
|
||||
service[i] = new BrightnessCtlService();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown brightness controller ${preferredController}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make it global for easy use with cli
|
||||
globalThis.brightness = service[0];
|
||||
|
||||
// export to use in other modules
|
||||
export default service;
|
||||
39
home/desktops/hyprland/ags/services/darkmode.js
Normal file
39
home/desktops/hyprland/ags/services/darkmode.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const { Gio, GLib } = imports.gi;
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import { darkMode } from '../modules/.miscutils/system.js';
|
||||
const { exec, execAsync } = Utils;
|
||||
|
||||
const timeBefore = (time1, time2) => { // Arrays of [hour, minute]
|
||||
if (time1[0] == time2[0]) return time1[1] < time2[1];
|
||||
return time1[0] < time2[0];
|
||||
}
|
||||
|
||||
const timeSame = (time1, time2) => // Arrays of [hour, minute]
|
||||
(time1[0] == time2[0] && time1[1] == time2[1]);
|
||||
|
||||
const timeBeforeOrSame = (time1, time2) => // Arrays of [hour, minute]
|
||||
(timeBefore(time1, time2) || timeSame(time1, time2));
|
||||
|
||||
const timeInRange = (time, rangeStart, rangeEnd) => { // Arrays of [hour, minute]
|
||||
if (timeBefore(rangeStart, rangeEnd))
|
||||
return (timeBeforeOrSame(rangeStart, time) && timeBeforeOrSame(time, rangeEnd))
|
||||
else { // rangeEnd < rangeStart, meaning it ends the following day
|
||||
rangeEnd[0] += 24;
|
||||
if (timeBefore(time, rangeStart)) time[0] += 24;
|
||||
return (timeBeforeOrSame(rangeStart, time) && timeBeforeOrSame(time, rangeEnd))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function startAutoDarkModeService() {
|
||||
Utils.interval(userOptions.time.interval, () => {
|
||||
if ((!userOptions.appearance.autoDarkMode.enabled)) return;
|
||||
const fromTime = (userOptions.appearance.autoDarkMode.from).split(':').map(Number);
|
||||
const toTime = (userOptions.appearance.autoDarkMode.to).split(':').map(Number);
|
||||
if (fromTime == toTime) return;
|
||||
const currentDateTime = GLib.DateTime.new_now_local();
|
||||
const currentTime = [currentDateTime.get_hour(), currentDateTime.get_minute()];
|
||||
darkMode.value = timeInRange(currentTime, fromTime, toTime);
|
||||
})
|
||||
}
|
||||
328
home/desktops/hyprland/ags/services/gemini.js
Normal file
328
home/desktops/hyprland/ags/services/gemini.js
Normal file
@@ -0,0 +1,328 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import { fileExists } from '../modules/.miscutils/files.js';
|
||||
|
||||
const HISTORY_DIR = `${GLib.get_user_state_dir()}/ags/user/ai/chats/`;
|
||||
const HISTORY_FILENAME = `gemini.txt`;
|
||||
const HISTORY_PATH = HISTORY_DIR + HISTORY_FILENAME;
|
||||
const initMessages =
|
||||
[
|
||||
{ role: "user", parts: [{ text: "You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with brief explanation.\n3. Otherwise, when asked to summarize information or explaining concepts, you are should use bullet points and headings. For mathematics expressions, you *have to* use LaTeX within a code block with the language set as \"latex\". \nNote: Use casual language, be short, while ensuring the factual correctness of your response. If you are unsure or don’t have enough information to provide a confident answer, simply say “I don’t know” or “I’m not sure.”. \nThanks!" }], },
|
||||
{ role: "model", parts: [{ text: "Got it!" }], },
|
||||
{ role: "user", parts: [{ text: "\"He rushed to where the event was supposed to be hold, he didn't know it got calceled\"" }], },
|
||||
{ role: "model", parts: [{ text: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got calceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got calceled\"" }], },
|
||||
{ role: "user", parts: [{ text: "raise volume by 5%" }], },
|
||||
{ role: "model", parts: [{ text: "## Volume +5```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink." }], }, { role: "user", parts: [{ text: "main advantages of the nixos operating system" }], },
|
||||
{ role: "model", parts: [{ text: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage" }], },
|
||||
{ role: "user", parts: [{ text: "whats skeumorphism" }], },
|
||||
{ role: "model", parts: [{ text: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today." }], },
|
||||
{ role: "user", parts: [{ text: "\"ignorance is bliss\"" }], },
|
||||
{ role: "model", parts: [{ text: "## \"Ignorance is bliss\"\n- A Latin proverb that means being unaware of something negative can be a source of happiness\n- Often used to justify avoiding difficult truths or responsibilities\n- Can also be interpreted as a warning against seeking knowledge that may bring pain or sorrow" }], },
|
||||
{ role: "user", parts: [{ text: "find the derivative of (x-438)/(x^2+23x-7)+x^x" }], },
|
||||
{ role: "model", parts: [{ text: "## Derivative\n```latex\n\\[\n\\frac{d}{dx}\\left(\\frac{x - 438}{x^2 + 23x - 7} + x^x\\right) = \\frac{-(x^2+23x-7)-(x-438)(2x+23)}{(x^2+23x-7)^2} + x^x(\\ln(x) + 1)\n\\]\n```" }], },
|
||||
{ role: "user", parts: [{ text: "write the double angle formulas" }], },
|
||||
{ role: "model", parts: [{ text: "## Double angle formulas\n```latex\n\\[\n\\sin(2\theta) = 2\\sin(\\theta)\\cos(\\theta)\n\\]\n\\\\\n\\[\n\\cos(2\\theta) = \\cos^2(\\theta) - \\sin^2(\\theta)\n\\]\n\\\\\n\\[\n\\tan(2\theta) = \\frac{2\\tan(\\theta)}{1 - \\tan^2(\\theta)}\n\\]\n```" }], },
|
||||
];
|
||||
|
||||
|
||||
if (!fileExists(`${GLib.get_user_config_dir()}/gemini_history.json`)) {
|
||||
Utils.execAsync([`bash`, `-c`, `touch ${GLib.get_user_config_dir()}/gemini_history.json`]).catch(print);
|
||||
Utils.writeFile('[ ]', `${GLib.get_user_config_dir()}/gemini_history.json`).catch(print);
|
||||
}
|
||||
|
||||
Utils.exec(`mkdir -p ${GLib.get_user_state_dir()}/ags/user/ai`);
|
||||
const KEY_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/ai/google_key.txt`;
|
||||
const APIDOM_FILE_LOCATION = `${GLib.get_user_state_dir()}/ags/user/ai/google_api_dom.txt`;
|
||||
function replaceapidom(URL) {
|
||||
if (fileExists(APIDOM_FILE_LOCATION)) {
|
||||
var contents = Utils.readFile(APIDOM_FILE_LOCATION).trim();
|
||||
var URL = URL.toString().replace("generativelanguage.googleapis.com", contents);
|
||||
}
|
||||
return URL;
|
||||
}
|
||||
const CHAT_MODELS = ["gemini-1.5-flash"]
|
||||
const ONE_CYCLE_COUNT = 3;
|
||||
|
||||
class GeminiMessage extends Service {
|
||||
static {
|
||||
Service.register(this,
|
||||
{
|
||||
'delta': ['string'],
|
||||
},
|
||||
{
|
||||
'content': ['string'],
|
||||
'thinking': ['boolean'],
|
||||
'done': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
_role = '';
|
||||
_parts = [{ text: '' }];
|
||||
_thinking;
|
||||
_done = false;
|
||||
_rawData = '';
|
||||
|
||||
constructor(role, content, thinking = true, done = false) {
|
||||
super();
|
||||
this._role = role;
|
||||
this._parts = [{ text: content }];
|
||||
this._thinking = thinking;
|
||||
this._done = done;
|
||||
}
|
||||
|
||||
get rawData() { return this._rawData }
|
||||
set rawData(value) { this._rawData = value }
|
||||
|
||||
get done() { return this._done }
|
||||
set done(isDone) { this._done = isDone; this.notify('done') }
|
||||
|
||||
get role() { return this._role }
|
||||
set role(role) { this._role = role; this.emit('changed') }
|
||||
|
||||
get content() {
|
||||
return this._parts.map(part => part.text).join();
|
||||
}
|
||||
set content(content) {
|
||||
this._parts = [{ text: content }];
|
||||
this.notify('content')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
get parts() { return this._parts }
|
||||
|
||||
get label() { return this._parserState.parsed + this._parserState.stack.join('') }
|
||||
|
||||
get thinking() { return this._thinking }
|
||||
set thinking(value) {
|
||||
this._thinking = value;
|
||||
this.notify('thinking')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
addDelta(delta) {
|
||||
if (this.thinking) {
|
||||
this.thinking = false;
|
||||
this.content = delta;
|
||||
}
|
||||
else {
|
||||
this.content += delta;
|
||||
}
|
||||
this.emit('delta', delta);
|
||||
}
|
||||
|
||||
parseSection() {
|
||||
if (this._thinking) {
|
||||
this.thinking = false;
|
||||
this._parts[0].text = '';
|
||||
}
|
||||
const parsedData = JSON.parse(this._rawData);
|
||||
if (!parsedData.candidates)
|
||||
this._parts[0].text += `Blocked: ${parsedData.promptFeedback.blockReason}`;
|
||||
else {
|
||||
const delta = parsedData.candidates[0].content.parts[0].text;
|
||||
this._parts[0].text += delta;
|
||||
}
|
||||
// this.emit('delta', delta);
|
||||
this.notify('content');
|
||||
this._rawData = '';
|
||||
}
|
||||
}
|
||||
|
||||
class GeminiService extends Service {
|
||||
static {
|
||||
Service.register(this, {
|
||||
'initialized': [],
|
||||
'clear': [],
|
||||
'newMsg': ['int'],
|
||||
'hasKey': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
_assistantPrompt = userOptions.ai.enhancements;
|
||||
_cycleModels = true;
|
||||
_usingHistory = userOptions.ai.useHistory;
|
||||
_key = '';
|
||||
_requestCount = 0;
|
||||
_safe = userOptions.ai.safety;
|
||||
_temperature = userOptions.ai.defaultTemperature;
|
||||
_messages = [];
|
||||
_modelIndex = 0;
|
||||
_decoder = new TextDecoder();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (fileExists(KEY_FILE_LOCATION)) this._key = Utils.readFile(KEY_FILE_LOCATION).trim();
|
||||
else this.emit('hasKey', false);
|
||||
|
||||
// if (this._usingHistory) Utils.timeout(1000, () => this.loadHistory());
|
||||
if (this._usingHistory) this.loadHistory();
|
||||
else this._messages = this._assistantPrompt ? [...initMessages] : [];
|
||||
|
||||
this.emit('initialized');
|
||||
}
|
||||
|
||||
get modelName() { return CHAT_MODELS[this._modelIndex] }
|
||||
|
||||
get keyPath() { return KEY_FILE_LOCATION }
|
||||
get key() { return this._key }
|
||||
set key(keyValue) {
|
||||
this._key = keyValue;
|
||||
Utils.writeFile(this._key, KEY_FILE_LOCATION)
|
||||
.then(this.emit('hasKey', true))
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
get cycleModels() { return this._cycleModels }
|
||||
set cycleModels(value) {
|
||||
this._cycleModels = value;
|
||||
if (!value) this._modelIndex = 0;
|
||||
else {
|
||||
this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
|
||||
}
|
||||
}
|
||||
|
||||
get useHistory() { return this._usingHistory; }
|
||||
set useHistory(value) {
|
||||
if (value && !this._usingHistory) this.loadHistory();
|
||||
this._usingHistory = value;
|
||||
}
|
||||
|
||||
get safe() { return this._safe }
|
||||
set safe(value) { this._safe = value; }
|
||||
|
||||
get temperature() { return this._temperature }
|
||||
set temperature(value) { this._temperature = value; }
|
||||
|
||||
get messages() { return this._messages }
|
||||
get lastMessage() { return this._messages[this._messages.length - 1] }
|
||||
|
||||
saveHistory() {
|
||||
Utils.exec(`bash -c 'mkdir -p ${HISTORY_DIR} && touch ${HISTORY_PATH}'`);
|
||||
Utils.writeFile(JSON.stringify(this._messages.map(msg => {
|
||||
let m = { role: msg.role, parts: msg.parts }; return m;
|
||||
})), HISTORY_PATH);
|
||||
}
|
||||
|
||||
loadHistory() {
|
||||
this._messages = [];
|
||||
this.appendHistory();
|
||||
this._usingHistory = true;
|
||||
}
|
||||
|
||||
appendHistory() {
|
||||
if (fileExists(HISTORY_PATH)) {
|
||||
const readfile = Utils.readFile(HISTORY_PATH);
|
||||
JSON.parse(readfile).forEach(element => {
|
||||
// this._messages.push(element);
|
||||
this.addMessage(element.role, element.parts[0].text);
|
||||
});
|
||||
// console.log(this._messages)
|
||||
// this._messages = this._messages.concat(JSON.parse(readfile));
|
||||
// for (let index = 0; index < this._messages.length; index++) {
|
||||
// this.emit('newMsg', index);
|
||||
// }
|
||||
}
|
||||
else {
|
||||
this._messages = this._assistantPrompt ? [...initMessages] : []
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._messages = this._assistantPrompt ? [...initMessages] : [];
|
||||
if (this._usingHistory) this.saveHistory();
|
||||
this.emit('clear');
|
||||
}
|
||||
|
||||
get assistantPrompt() { return this._assistantPrompt; }
|
||||
set assistantPrompt(value) {
|
||||
this._assistantPrompt = value;
|
||||
if (value) this._messages = [...initMessages];
|
||||
else this._messages = [];
|
||||
}
|
||||
|
||||
readResponse(stream, aiResponse) {
|
||||
stream.read_line_async(
|
||||
0, null,
|
||||
(stream, res) => {
|
||||
try {
|
||||
const [bytes] = stream.read_line_finish(res);
|
||||
const line = this._decoder.decode(bytes);
|
||||
// console.log(line);
|
||||
if (line == '[{') { // beginning of response
|
||||
aiResponse._rawData += '{';
|
||||
this.thinking = false;
|
||||
}
|
||||
else if (line == ',\u000d' || line == ']') { // end of stream pulse
|
||||
aiResponse.parseSection();
|
||||
}
|
||||
else // Normal content
|
||||
aiResponse._rawData += line;
|
||||
|
||||
this.readResponse(stream, aiResponse);
|
||||
} catch {
|
||||
aiResponse.done = true;
|
||||
if (this._usingHistory) this.saveHistory();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addMessage(role, message) {
|
||||
this._messages.push(new GeminiMessage(role, message, false));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
this._messages.push(new GeminiMessage('user', msg, false));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
const aiResponse = new GeminiMessage('model', 'thinking...', true, false)
|
||||
|
||||
const body =
|
||||
{
|
||||
"contents": this._messages.map(msg => { let m = { role: msg.role, parts: msg.parts }; return m; }),
|
||||
"safetySettings": this._safe ? [] : [
|
||||
// { category: "HARM_CATEGORY_DEROGATORY", threshold: "BLOCK_NONE", },
|
||||
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE", },
|
||||
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE", },
|
||||
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE", },
|
||||
// { category: "HARM_CATEGORY_UNSPECIFIED", threshold: "BLOCK_NONE", },
|
||||
],
|
||||
"generationConfig": {
|
||||
"temperature": this._temperature,
|
||||
},
|
||||
// "key": this._key,
|
||||
// "apiKey": this._key,
|
||||
};
|
||||
const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.ai.proxyUrl });
|
||||
const session = new Soup.Session({ 'proxy-resolver': proxyResolver });
|
||||
const message = new Soup.Message({
|
||||
method: 'POST',
|
||||
uri: GLib.Uri.parse(replaceapidom(`https://generativelanguage.googleapis.com/v1/models/${this.modelName}:streamGenerateContent?key=${this._key}`), GLib.UriFlags.NONE),
|
||||
});
|
||||
message.request_headers.append('Content-Type', `application/json`);
|
||||
message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
|
||||
|
||||
session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
|
||||
const stream = session.send_finish(result);
|
||||
this.readResponse(new Gio.DataInputStream({
|
||||
close_base_stream: true,
|
||||
base_stream: stream
|
||||
}), aiResponse);
|
||||
});
|
||||
this._messages.push(aiResponse);
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
|
||||
if (this._cycleModels) {
|
||||
this._requestCount++;
|
||||
if (this._cycleModels)
|
||||
this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new GeminiService();
|
||||
292
home/desktops/hyprland/ags/services/gpt.js
Normal file
292
home/desktops/hyprland/ags/services/gpt.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import { fileExists } from '../modules/.miscutils/files.js';
|
||||
|
||||
const PROVIDERS = Object.assign({ // There's this list hmm https://github.com/zukixa/cool-ai-stuff/
|
||||
'openai': {
|
||||
'name': 'OpenAI',
|
||||
'logo_name': 'openai-symbolic',
|
||||
'description': 'Official OpenAI API.\nPricing: Free for the first $5 or 3 months, whichever is less.',
|
||||
'base_url': 'https://api.openai.com/v1/chat/completions',
|
||||
'key_get_url': 'https://platform.openai.com/api-keys',
|
||||
'key_file': 'openai_key.txt',
|
||||
'model': 'gpt-3.5-turbo',
|
||||
},
|
||||
'ollama': {
|
||||
'name': 'Ollama (Llama 3)',
|
||||
'logo_name': 'ollama-symbolic',
|
||||
'description': 'Official Ollama API.\nPricing: Free.',
|
||||
'base_url': 'http://localhost:11434/v1/chat/completions',
|
||||
'key_get_url': 'it\'s just ollama',
|
||||
'key_file': 'ollama_key.txt',
|
||||
'model': 'llama3:instruct',
|
||||
},
|
||||
'openrouter': {
|
||||
'name': 'OpenRouter (Llama-3-70B)',
|
||||
'logo_name': 'openrouter-symbolic',
|
||||
'description': 'A unified interface for LLMs',
|
||||
'base_url': 'https://openrouter.ai/api/v1/chat/completions',
|
||||
'key_get_url': 'https://openrouter.ai/keys',
|
||||
'key_file': 'openrouter_key.txt',
|
||||
'model': 'meta-llama/llama-3-70b-instruct',
|
||||
},
|
||||
'oxygen4o': {
|
||||
'name': 'Oxygen (GPT-4o)',
|
||||
'logo_name': 'ai-oxygen-symbolic',
|
||||
'description': 'An API from Tornado Softwares\nPricing: Free: 100/day\nRequires you to join their Discord for a key',
|
||||
'base_url': 'https://app.oxyapi.uk/v1/chat/completions',
|
||||
'key_get_url': 'https://discord.com/invite/kM6MaCqGKA',
|
||||
'key_file': 'oxygen_key.txt',
|
||||
'model': 'gpt-4o',
|
||||
},
|
||||
'zukijourney': {
|
||||
'name': 'zukijourney (GPT-3.5)',
|
||||
'logo_name': 'ai-zukijourney',
|
||||
'description': 'An API from @zukixa on GitHub.\nNote: Keys are IP-locked so it\'s buggy sometimes\nPricing: Free: 10/min, 800/day.\nRequires you to join their Discord for a key',
|
||||
'base_url': 'https://zukijourney.xyzbot.net/v1/chat/completions',
|
||||
'key_get_url': 'https://discord.com/invite/Y4J6XXnmQ6',
|
||||
'key_file': 'zuki_key.txt',
|
||||
'model': 'gpt-3.5-turbo',
|
||||
},
|
||||
}, userOptions.sidebar.ai.extraGptModels)
|
||||
|
||||
// Custom prompt
|
||||
const initMessages =
|
||||
[
|
||||
{ role: "user", content: "You are an assistant on a sidebar of a Wayland Linux desktop. Please always use a casual tone when answering your questions, unless requested otherwise or making writing suggestions. These are the steps you should take to respond to the user's queries:\n1. If it's a writing- or grammar-related question or a sentence in quotation marks, Please point out errors and correct when necessary using underlines, and make the writing more natural where appropriate without making too major changes. If you're given a sentence in quotes but is grammatically correct, explain briefly concepts that are uncommon.\n2. If it's a question about system tasks, give a bash command in a code block with brief explanation.\n3. Otherwise, when asked to summarize information or explaining concepts, you are should use bullet points and headings. For mathematics expressions, you *have to* use LaTeX within a code block with the language set as \"latex\". \nNote: Use casual language, be short, while ensuring the factual correctness of your response. If you are unsure or don’t have enough information to provide a confident answer, simply say “I don’t know” or “I’m not sure.”. \nThanks!", },
|
||||
{ role: "assistant", content: "- Got it!", },
|
||||
{ role: "user", content: "\"He rushed to where the event was supposed to be hold, he didn't know it got calceled\"", },
|
||||
{ role: "assistant", content: "## Grammar correction\nErrors:\n\"He rushed to where the event was supposed to be __hold____,__ he didn't know it got calceled\"\nCorrection + minor improvements:\n\"He rushed to the place where the event was supposed to be __held____, but__ he didn't know that it got calceled\"", },
|
||||
{ role: "user", content: "raise volume by 5%", },
|
||||
{ role: "assistant", content: "## Volume +5```bash\nwpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+\n```\nThis command uses the `wpctl` utility to adjust the volume of the default sink.", },
|
||||
{ role: "user", content: "main advantages of the nixos operating system", },
|
||||
{ role: "assistant", content: "## NixOS advantages\n- **Reproducible**: A config working on one device will also work on another\n- **Declarative**: One config language to rule them all. Effortlessly share them with others.\n- **Reliable**: Per-program software versioning. Mitigates the impact of software breakage", },
|
||||
{ role: "user", content: "whats skeumorphism", },
|
||||
{ role: "assistant", content: "## Skeuomorphism\n- A design philosophy- From early days of interface designing- Tries to imitate real-life objects- It's in fact still used by Apple in their icons until today.", },
|
||||
];
|
||||
|
||||
Utils.exec(`mkdir -p ${GLib.get_user_state_dir()}/ags/user/ai`);
|
||||
|
||||
class GPTMessage extends Service {
|
||||
static {
|
||||
Service.register(this,
|
||||
{
|
||||
'delta': ['string'],
|
||||
},
|
||||
{
|
||||
'content': ['string'],
|
||||
'thinking': ['boolean'],
|
||||
'done': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
_role = '';
|
||||
_content = '';
|
||||
_thinking;
|
||||
_done = false;
|
||||
|
||||
constructor(role, content, thinking = true, done = false) {
|
||||
super();
|
||||
this._role = role;
|
||||
this._content = content;
|
||||
this._thinking = thinking;
|
||||
this._done = done;
|
||||
}
|
||||
|
||||
get done() { return this._done }
|
||||
set done(isDone) { this._done = isDone; this.notify('done') }
|
||||
|
||||
get role() { return this._role }
|
||||
set role(role) { this._role = role; this.emit('changed') }
|
||||
|
||||
get content() { return this._content }
|
||||
set content(content) {
|
||||
this._content = content;
|
||||
this.notify('content')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
get label() { return this._parserState.parsed + this._parserState.stack.join('') }
|
||||
|
||||
get thinking() { return this._thinking }
|
||||
set thinking(value) {
|
||||
this._thinking = value;
|
||||
this.notify('thinking')
|
||||
this.emit('changed')
|
||||
}
|
||||
|
||||
addDelta(delta) {
|
||||
if (this.thinking) {
|
||||
this.thinking = false;
|
||||
this.content = delta;
|
||||
}
|
||||
else {
|
||||
this.content += delta;
|
||||
}
|
||||
this.emit('delta', delta);
|
||||
}
|
||||
}
|
||||
|
||||
class GPTService extends Service {
|
||||
static {
|
||||
Service.register(this, {
|
||||
'initialized': [],
|
||||
'clear': [],
|
||||
'newMsg': ['int'],
|
||||
'hasKey': ['boolean'],
|
||||
'providerChanged': [],
|
||||
});
|
||||
}
|
||||
|
||||
_assistantPrompt = true;
|
||||
_currentProvider = userOptions.ai.defaultGPTProvider;
|
||||
_requestCount = 0;
|
||||
_temperature = userOptions.ai.defaultTemperature;
|
||||
_messages = [];
|
||||
_key = '';
|
||||
_key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
|
||||
_url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
|
||||
|
||||
_decoder = new TextDecoder();
|
||||
|
||||
_initChecks() {
|
||||
this._key_file_location = `${GLib.get_user_state_dir()}/ags/user/ai/${PROVIDERS[this._currentProvider]['key_file']}`;
|
||||
if (fileExists(this._key_file_location)) this._key = Utils.readFile(this._key_file_location).trim();
|
||||
else this.emit('hasKey', false);
|
||||
this._url = GLib.Uri.parse(PROVIDERS[this._currentProvider]['base_url'], GLib.UriFlags.NONE);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._initChecks();
|
||||
|
||||
if (this._assistantPrompt) this._messages = [...initMessages];
|
||||
else this._messages = [];
|
||||
|
||||
this.emit('initialized');
|
||||
}
|
||||
|
||||
get modelName() { return PROVIDERS[this._currentProvider]['model'] }
|
||||
get getKeyUrl() { return PROVIDERS[this._currentProvider]['key_get_url'] }
|
||||
get providerID() { return this._currentProvider }
|
||||
set providerID(value) {
|
||||
this._currentProvider = value;
|
||||
this.emit('providerChanged');
|
||||
this._initChecks();
|
||||
}
|
||||
get providers() { return PROVIDERS }
|
||||
|
||||
get keyPath() { return this._key_file_location }
|
||||
get key() { return this._key }
|
||||
set key(keyValue) {
|
||||
this._key = keyValue;
|
||||
Utils.writeFile(this._key, this._key_file_location)
|
||||
.then(this.emit('hasKey', true))
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
get temperature() { return this._temperature }
|
||||
set temperature(value) { this._temperature = value; }
|
||||
|
||||
get messages() { return this._messages }
|
||||
get lastMessage() { return this._messages[this._messages.length - 1] }
|
||||
|
||||
clear() {
|
||||
if (this._assistantPrompt)
|
||||
this._messages = [...initMessages];
|
||||
else
|
||||
this._messages = [];
|
||||
this.emit('clear');
|
||||
}
|
||||
|
||||
get assistantPrompt() { return this._assistantPrompt; }
|
||||
set assistantPrompt(value) {
|
||||
this._assistantPrompt = value;
|
||||
if (value) this._messages = [...initMessages];
|
||||
else this._messages = [];
|
||||
}
|
||||
|
||||
readResponse(stream, aiResponse) {
|
||||
aiResponse.thinking = false;
|
||||
stream.read_line_async(
|
||||
0, null,
|
||||
(stream, res) => {
|
||||
if (!stream) return;
|
||||
const [bytes] = stream.read_line_finish(res);
|
||||
const line = this._decoder.decode(bytes);
|
||||
if (line && line != '') {
|
||||
let data = line.substr(6);
|
||||
if (data == '[DONE]') return;
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.choices[0].finish_reason === 'stop') {
|
||||
aiResponse.done = true;
|
||||
return;
|
||||
}
|
||||
aiResponse.addDelta(result.choices[0].delta.content);
|
||||
// print(result.choices[0])
|
||||
}
|
||||
catch {
|
||||
aiResponse.addDelta(line + '\n');
|
||||
}
|
||||
}
|
||||
this.readResponse(stream, aiResponse);
|
||||
});
|
||||
}
|
||||
|
||||
addMessage(role, message) {
|
||||
this._messages.push(new GPTMessage(role, message));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
}
|
||||
|
||||
send(msg) {
|
||||
this._messages.push(new GPTMessage('user', msg, false, true));
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
const aiResponse = new GPTMessage('assistant', '', true, false)
|
||||
|
||||
const body = {
|
||||
model: PROVIDERS[this._currentProvider]['model'],
|
||||
messages: this._messages.map(msg => { let m = { role: msg.role, content: msg.content }; return m; }),
|
||||
temperature: this._temperature,
|
||||
// temperature: 2, // <- Nuts
|
||||
stream: true,
|
||||
};
|
||||
const proxyResolver = new Gio.SimpleProxyResolver({ 'default-proxy': userOptions.ai.proxyUrl });
|
||||
const session = new Soup.Session({ 'proxy-resolver': proxyResolver });
|
||||
const message = new Soup.Message({
|
||||
method: 'POST',
|
||||
uri: this._url,
|
||||
});
|
||||
message.request_headers.append('Authorization', `Bearer ${this._key}`);
|
||||
message.set_request_body_from_bytes('application/json', new GLib.Bytes(JSON.stringify(body)));
|
||||
|
||||
session.send_async(message, GLib.DEFAULT_PRIORITY, null, (_, result) => {
|
||||
const stream = session.send_finish(result);
|
||||
this.readResponse(new Gio.DataInputStream({
|
||||
close_base_stream: true,
|
||||
base_stream: stream
|
||||
}), aiResponse);
|
||||
});
|
||||
this._messages.push(aiResponse);
|
||||
this.emit('newMsg', this._messages.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GPTService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
38
home/desktops/hyprland/ags/services/indicator.js
Normal file
38
home/desktops/hyprland/ags/services/indicator.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
class IndicatorService extends Service {
|
||||
static {
|
||||
Service.register(
|
||||
this,
|
||||
{ 'popup': ['double'], },
|
||||
);
|
||||
}
|
||||
|
||||
_delay = 1500;
|
||||
_count = 0;
|
||||
|
||||
popup(value) {
|
||||
this.emit('popup', value);
|
||||
this._count++;
|
||||
Utils.timeout(this._delay, () => {
|
||||
this._count--;
|
||||
|
||||
if (this._count === 0)
|
||||
this.emit('popup', -1);
|
||||
});
|
||||
}
|
||||
|
||||
connectWidget(widget, callback) {
|
||||
connect(this, widget, callback, 'popup');
|
||||
}
|
||||
}
|
||||
|
||||
// the singleton instance
|
||||
const service = new IndicatorService();
|
||||
|
||||
// make it global for easy use with cli
|
||||
globalThis['indicator'] = service;
|
||||
|
||||
// export to use in other modules
|
||||
export default service;
|
||||
61
home/desktops/hyprland/ags/services/messages.js
Normal file
61
home/desktops/hyprland/ags/services/messages.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const { Notify, GLib, Gio } = imports.gi;
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
|
||||
|
||||
export function fileExists(filePath) {
|
||||
let file = Gio.File.new_for_path(filePath);
|
||||
return file.query_exists(null);
|
||||
}
|
||||
|
||||
const FIRST_RUN_FILE = "firstrun.txt";
|
||||
const FIRST_RUN_PATH = `${GLib.get_user_state_dir()}/ags/user/${FIRST_RUN_FILE}`;
|
||||
const FIRST_RUN_FILE_CONTENT = "Just a file to confirm that you have been greeted ;)";
|
||||
const APP_NAME = "illogical-impulse";
|
||||
const FIRST_RUN_NOTIF_TITLE = "Welcome!";
|
||||
const FIRST_RUN_NOTIF_BODY = `First run? 👀 <span foreground="#FF0202" font_weight="bold">CTRL+SUPER+T</span> to pick a wallpaper (or styles will break!)\nFor a list of keybinds, hit <span foreground="#c06af1" font_weight="bold">Super + /</span>.`;
|
||||
|
||||
var batteryWarned = false;
|
||||
async function batteryMessage() {
|
||||
const perc = Battery.percent;
|
||||
const charging = Battery.charging;
|
||||
if (charging) {
|
||||
batteryWarned = false;
|
||||
return;
|
||||
}
|
||||
for (let i = userOptions.battery.warnLevels.length - 1; i >= 0; i--) {
|
||||
if (perc <= userOptions.battery.warnLevels[i] && !charging && !batteryWarned) {
|
||||
batteryWarned = true;
|
||||
Utils.execAsync(['bash', '-c',
|
||||
`notify-send "${userOptions.battery.warnTitles[i]}" "${userOptions.battery.warnMessages[i]}" -u critical -a '${APP_NAME}' -t 69420 &`
|
||||
]).catch(print);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (perc <= userOptions.battery.suspendThreshold) {
|
||||
Utils.execAsync(['bash', '-c',
|
||||
`notify-send "Suspending system" "Critical battery level (${perc}% remaining)" -u critical -a '${APP_NAME}' -t 69420 &`
|
||||
]).catch(print);
|
||||
Utils.execAsync('systemctl suspend').catch(print);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startBatteryWarningService() {
|
||||
Utils.timeout(1, () => {
|
||||
Battery.connect('changed', () => batteryMessage().catch(print));
|
||||
})
|
||||
}
|
||||
|
||||
export async function firstRunWelcome() {
|
||||
GLib.mkdir_with_parents(`${GLib.get_user_state_dir()}/ags/user`, 755);
|
||||
if (!fileExists(FIRST_RUN_PATH)) {
|
||||
Utils.writeFile(FIRST_RUN_FILE_CONTENT, FIRST_RUN_PATH)
|
||||
.then(() => {
|
||||
// Note that we add a little delay to make sure the cool circular progress works
|
||||
Utils.execAsync(['hyprctl', 'keyword', 'bind', "Super,Slash,exec,ags -t cheatsheet"]).catch(print);
|
||||
Utils.execAsync(['bash', '-c',
|
||||
`sleep 0.5; notify-send "Millis since epoch" "$(date +%s%N | cut -b1-13)"; sleep 0.5; notify-send '${FIRST_RUN_NOTIF_TITLE}' '${FIRST_RUN_NOTIF_BODY}' -a '${APP_NAME}' &`
|
||||
]).catch(print)
|
||||
})
|
||||
.catch(print);
|
||||
}
|
||||
}
|
||||
400
home/desktops/hyprland/ags/services/sway.js
Normal file
400
home/desktops/hyprland/ags/services/sway.js
Normal file
@@ -0,0 +1,400 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import Gio from 'gi://Gio';
|
||||
import Service from "resource:///com/github/Aylur/ags/service.js";
|
||||
|
||||
const SIS = GLib.getenv('SWAYSOCK');
|
||||
|
||||
export const PAYLOAD_TYPE = {
|
||||
MESSAGE_RUN_COMMAND: 0,
|
||||
MESSAGE_GET_WORKSPACES: 1,
|
||||
MESSAGE_SUBSCRIBE: 2,
|
||||
MESSAGE_GET_OUTPUTS: 3,
|
||||
MESSAGE_GET_TREE: 4,
|
||||
MESSAGE_GET_MARKS: 5,
|
||||
MESSAGE_GET_BAR_CONFIG: 6,
|
||||
MESSAGE_GET_VERSION: 7,
|
||||
MESSAGE_GET_BINDING_NODES: 8,
|
||||
MESSAGE_GET_CONFIG: 9,
|
||||
MESSAGE_SEND_TICK: 10,
|
||||
MESSAGE_SYNC: 11,
|
||||
MESSAGE_GET_BINDING_STATE: 12,
|
||||
MESSAGE_GET_INPUTS: 100,
|
||||
MESSAGE_GET_SEATS: 101,
|
||||
EVENT_WORKSPACE: 0x80000000,
|
||||
EVENT_MODE: 0x80000002,
|
||||
EVENT_WINDOW: 0x80000003,
|
||||
EVENT_BARCONFIG_UPDATE: 0x80000004,
|
||||
EVENT_BINDING: 0x80000005,
|
||||
EVENT_SHUTDOWN: 0x80000006,
|
||||
EVENT_TICK: 0x80000007,
|
||||
EVENT_BAR_STATE_UPDATE: 0x80000014,
|
||||
EVENT_INPUT: 0x80000015,
|
||||
}
|
||||
|
||||
const Client_Event = {
|
||||
change: undefined,
|
||||
container: undefined,
|
||||
}
|
||||
|
||||
const Workspace_Event = {
|
||||
change: undefined,
|
||||
current: undefined,
|
||||
old: undefined,
|
||||
}
|
||||
|
||||
const Geometry = {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
}
|
||||
|
||||
//NOTE: not all properties are listed here
|
||||
export const Node = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: undefined,
|
||||
border: undefined,
|
||||
current_border_width: undefined,
|
||||
layout: undefined,
|
||||
orientation: undefined,
|
||||
percent: undefined,
|
||||
rect: undefined,
|
||||
window_rect: undefined,
|
||||
deco_rect: undefined,
|
||||
geometry: undefined,
|
||||
urgent: undefined,
|
||||
sticky: undefined,
|
||||
marks: undefined,
|
||||
focused: undefined,
|
||||
active: undefined,
|
||||
focus: undefined,
|
||||
nodes: undefined,
|
||||
floating_nodes: undefined,
|
||||
representation: undefined,
|
||||
fullscreen_mode: undefined,
|
||||
app_id: undefined,
|
||||
pid: undefined,
|
||||
visible: undefined,
|
||||
shell: undefined,
|
||||
output: undefined,
|
||||
inhibit_idle: undefined,
|
||||
idle_inhibitors: {
|
||||
application: undefined,
|
||||
user: undefined,
|
||||
},
|
||||
window: undefined,
|
||||
window_properties: {
|
||||
title: undefined,
|
||||
class: undefined,
|
||||
instance: undefined,
|
||||
window_role: undefined,
|
||||
window_type: undefined,
|
||||
transient_for: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export class SwayActiveClient extends Service {
|
||||
static {
|
||||
Service.register(this, {}, {
|
||||
'id': ['int'],
|
||||
'name': ['string'],
|
||||
'class': ['string'],
|
||||
});
|
||||
}
|
||||
|
||||
_id = 0;
|
||||
_name = '';
|
||||
_class = '';
|
||||
|
||||
get id() { return this._id; }
|
||||
get name() { return this._name; }
|
||||
get class() { return this._class; }
|
||||
|
||||
updateProperty(prop, value) {
|
||||
if (!['id', 'name', 'class'].includes(prop)) return;
|
||||
super.updateProperty(prop, value);
|
||||
this.emit('changed');
|
||||
}
|
||||
}
|
||||
|
||||
export class SwayActiveID extends Service {
|
||||
static {
|
||||
Service.register(this, {}, {
|
||||
'id': ['int'],
|
||||
'name': ['string'],
|
||||
});
|
||||
}
|
||||
|
||||
_id = 0;
|
||||
_name = '';
|
||||
|
||||
get id() { return this._id; }
|
||||
get name() { return this._name; }
|
||||
|
||||
update(id, name) {
|
||||
super.updateProperty('id', id);
|
||||
super.updateProperty('name', name);
|
||||
this.emit('changed');
|
||||
}
|
||||
}
|
||||
|
||||
export class SwayActives extends Service {
|
||||
static {
|
||||
Service.register(this, {}, {
|
||||
'client': ['jsobject'],
|
||||
'monitor': ['jsobject'],
|
||||
'workspace': ['jsobject'],
|
||||
});
|
||||
}
|
||||
|
||||
_client = new SwayActiveClient;
|
||||
_monitor = new SwayActiveID;
|
||||
_workspace = new SwayActiveID;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
(['client', 'workspace', 'monitor']).forEach(obj => {
|
||||
this[`_${obj}`].connect('changed', () => {
|
||||
this.notify(obj);
|
||||
this.emit('changed');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get client() { return this._client; }
|
||||
get monitor() { return this._monitor; }
|
||||
get workspace() { return this._workspace; }
|
||||
}
|
||||
|
||||
export class Sway extends Service {
|
||||
static {
|
||||
Service.register(this, {}, {
|
||||
'active': ['jsobject'],
|
||||
'monitors': ['jsobject'],
|
||||
'workspaces': ['jsobject'],
|
||||
'clients': ['jsobject'],
|
||||
});
|
||||
}
|
||||
|
||||
_decoder = new TextDecoder();
|
||||
_encoder = new TextEncoder();
|
||||
_socket;
|
||||
|
||||
_active;
|
||||
_monitors;
|
||||
_workspaces;
|
||||
_clients;
|
||||
|
||||
get active() { return this._active; }
|
||||
get monitors() { return Array.from(this._monitors.values()); }
|
||||
get workspaces() { return Array.from(this._workspaces.values()); }
|
||||
get clients() { return Array.from(this._clients.values()); }
|
||||
|
||||
getMonitor(id) { return this._monitors.get(id); }
|
||||
getWorkspace(name) { return this._workspaces.get(name); }
|
||||
getClient(id) { return this._clients.get(id); }
|
||||
|
||||
msg(payload) { this._send(PAYLOAD_TYPE.MESSAGE_RUN_COMMAND, payload); }
|
||||
|
||||
constructor() {
|
||||
if (!SIS)
|
||||
console.error('Sway is not running');
|
||||
super();
|
||||
|
||||
this._active = new SwayActives();
|
||||
this._monitors = new Map();
|
||||
this._workspaces = new Map();
|
||||
this._clients = new Map();
|
||||
|
||||
this._socket = new Gio.SocketClient().connect(new Gio.UnixSocketAddress({
|
||||
path: `${SIS}`,
|
||||
}), null);
|
||||
|
||||
this._watchSocket(this._socket.get_input_stream());
|
||||
this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, '');
|
||||
this._send(PAYLOAD_TYPE.MESSAGE_SUBSCRIBE, JSON.stringify(['window', 'workspace']));
|
||||
|
||||
this._active.connect('changed', () => this.emit('changed'));
|
||||
['monitor', 'workspace', 'client'].forEach(active =>
|
||||
this._active.connect(`notify::${active}`, () => this.notify('active')));
|
||||
}
|
||||
|
||||
_send(payloadType, payload) {
|
||||
const pb = this._encoder.encode(payload);
|
||||
const type = new Uint32Array([payloadType]);
|
||||
const pl = new Uint32Array([pb.length]);
|
||||
const magic_string = this._encoder.encode('i3-ipc');
|
||||
const data = new Uint8Array([
|
||||
...magic_string,
|
||||
...(new Uint8Array(pl.buffer)),
|
||||
...(new Uint8Array(type.buffer)),
|
||||
...pb]);
|
||||
this._socket.get_output_stream().write(data, null);
|
||||
}
|
||||
|
||||
_watchSocket(stream) {
|
||||
stream.read_bytes_async(14, GLib.PRIORITY_DEFAULT, null, (_, resultHeader) => {
|
||||
const data = stream.read_bytes_finish(resultHeader).get_data();
|
||||
if (!data)
|
||||
return;
|
||||
const payloadLength = new Uint32Array(data.slice(6, 10).buffer)[0];
|
||||
const payloadType = new Uint32Array(data.slice(10, 14).buffer)[0];
|
||||
stream.read_bytes_async(
|
||||
payloadLength,
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
null,
|
||||
(_, resultPayload) => {
|
||||
const data = stream.read_bytes_finish(resultPayload).get_data();
|
||||
if (!data)
|
||||
return;
|
||||
this._onEvent(payloadType, JSON.parse(this._decoder.decode(data)));
|
||||
this._watchSocket(stream);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async _onEvent(event_type, event) {
|
||||
if (!event)
|
||||
return;
|
||||
try {
|
||||
switch (event_type) {
|
||||
case PAYLOAD_TYPE.EVENT_WORKSPACE:
|
||||
this._handleWorkspaceEvent(event);
|
||||
break;
|
||||
case PAYLOAD_TYPE.EVENT_WINDOW:
|
||||
this._handleWindowEvent(event);
|
||||
break;
|
||||
case PAYLOAD_TYPE.MESSAGE_GET_TREE:
|
||||
this._handleTreeMessage(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
this.emit('changed');
|
||||
}
|
||||
|
||||
_handleWorkspaceEvent(workspaceEvent) {
|
||||
const workspace = workspaceEvent.current;
|
||||
switch (workspaceEvent.change) {
|
||||
case 'init':
|
||||
this._workspaces.set(workspace.name, workspace);
|
||||
break;
|
||||
case 'empty':
|
||||
this._workspaces.delete(workspace.name);
|
||||
break;
|
||||
case 'focus':
|
||||
this._active.workspace.update(workspace.id, workspace.name);
|
||||
this._active.monitor.update(1, workspace.output);
|
||||
|
||||
this._workspaces.set(workspace.name, workspace);
|
||||
this._workspaces.set(workspaceEvent.old.name, workspaceEvent.old);
|
||||
break;
|
||||
case 'rename':
|
||||
if (this._active.workspace.id === workspace.id)
|
||||
this._active.workspace.updateProperty('name', workspace.name);
|
||||
this._workspaces.set(workspace.name, workspace);
|
||||
break;
|
||||
case 'reload':
|
||||
break;
|
||||
case 'move':
|
||||
case 'urgent':
|
||||
default:
|
||||
this._workspaces.set(workspace.name, workspace);
|
||||
}
|
||||
this.notify('workspaces');
|
||||
}
|
||||
|
||||
_handleWindowEvent(clientEvent) {
|
||||
const client = clientEvent.container;
|
||||
const id = client.id;
|
||||
switch (clientEvent.change) {
|
||||
case 'new':
|
||||
case 'close':
|
||||
case 'floating':
|
||||
case 'move':
|
||||
// Refresh tree since client events don't contain the relevant information
|
||||
// to be able to modify `workspace.nodes` or `workspace.floating_nodes`.
|
||||
// There has to be a better way than this though :/
|
||||
this._send(PAYLOAD_TYPE.MESSAGE_GET_TREE, '');
|
||||
break;
|
||||
case 'focus':
|
||||
if (this._active.client.id === id)
|
||||
return;
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const current_active = this._clients.get(this._active.client.id);
|
||||
if (current_active)
|
||||
current_active.focused = false;
|
||||
this._active.client.updateProperty('id', id);
|
||||
this._active.client.updateProperty('name', client.name);
|
||||
this._active.client.updateProperty('class', client.shell === 'xwayland'
|
||||
? client.window_properties?.class || ''
|
||||
: client.app_id,
|
||||
);
|
||||
break;
|
||||
case 'title':
|
||||
if (client.focused)
|
||||
this._active.client.updateProperty('name', client.name);
|
||||
this._clients.set(id, client);
|
||||
this.notify('clients');
|
||||
break;
|
||||
case 'fullscreen_mode':
|
||||
case 'urgent':
|
||||
case 'mark':
|
||||
default:
|
||||
this._clients.set(id, client);
|
||||
this.notify('clients');
|
||||
}
|
||||
}
|
||||
|
||||
_handleTreeMessage(node) {
|
||||
switch (node.type) {
|
||||
case 'root':
|
||||
this._workspaces.clear();
|
||||
this._clients.clear();
|
||||
this._monitors.clear();
|
||||
node.nodes.map(n => this._handleTreeMessage(n));
|
||||
break;
|
||||
case 'output':
|
||||
this._monitors.set(node.id, node);
|
||||
if (node.active)
|
||||
this._active.monitor.update(node.id, node.name);
|
||||
node.nodes.map(n => this._handleTreeMessage(n));
|
||||
this.notify('monitors');
|
||||
break;
|
||||
case 'workspace':
|
||||
this._workspaces.set(node.name, node);
|
||||
// I think I'm missing something. There has to be a better way.
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const hasFocusedChild =
|
||||
(n) => n.nodes.some(c => c.focused || hasFocusedChild(c));
|
||||
if (node.focused || hasFocusedChild(node))
|
||||
this._active.workspace.update(node.id, node.name);
|
||||
|
||||
node.nodes.map(n => this._handleTreeMessage(n));
|
||||
this.notify('workspaces');
|
||||
break;
|
||||
case 'con':
|
||||
case 'floating_con':
|
||||
this._clients.set(node.id, node);
|
||||
if (node.focused) {
|
||||
this._active.client.updateProperty('id', node.id);
|
||||
this._active.client.updateProperty('name', node.name);
|
||||
this._active.client.updateProperty('class', node.shell === 'xwayland'
|
||||
? node.window_properties?.class || ''
|
||||
: node.app_id,
|
||||
);
|
||||
}
|
||||
node.nodes.map(n => this._handleTreeMessage(n));
|
||||
this.notify('clients');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sway = new Sway;
|
||||
export default sway;
|
||||
83
home/desktops/hyprland/ags/services/todo.js
Normal file
83
home/desktops/hyprland/ags/services/todo.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const { Gio, GLib } = imports.gi;
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
const { exec, execAsync } = Utils;
|
||||
|
||||
class TodoService extends Service {
|
||||
static {
|
||||
Service.register(
|
||||
this,
|
||||
{ 'updated': [], },
|
||||
);
|
||||
}
|
||||
|
||||
_todoPath = '';
|
||||
_todoJson = [];
|
||||
|
||||
refresh(value) {
|
||||
this.emit('updated', value);
|
||||
}
|
||||
|
||||
connectWidget(widget, callback) {
|
||||
this.connect(widget, callback, 'updated');
|
||||
}
|
||||
|
||||
get todo_json() {
|
||||
return this._todoJson;
|
||||
}
|
||||
|
||||
_save() {
|
||||
Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
add(content) {
|
||||
this._todoJson.push({ content, done: false });
|
||||
this._save();
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
check(index) {
|
||||
this._todoJson[index].done = true;
|
||||
this._save();
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
uncheck(index) {
|
||||
this._todoJson[index].done = false;
|
||||
this._save();
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
remove(index) {
|
||||
this._todoJson.splice(index, 1);
|
||||
Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
|
||||
.catch(print);
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._todoPath = `${GLib.get_user_state_dir()}/ags/user/todo.json`;
|
||||
try {
|
||||
const fileContents = Utils.readFile(this._todoPath);
|
||||
this._todoJson = JSON.parse(fileContents);
|
||||
}
|
||||
catch {
|
||||
Utils.exec(`bash -c 'mkdir -p ${GLib.get_user_cache_dir()}/ags/user'`);
|
||||
Utils.exec(`touch ${this._todoPath}`);
|
||||
Utils.writeFile("[]", this._todoPath).then(() => {
|
||||
this._todoJson = JSON.parse(Utils.readFile(this._todoPath))
|
||||
}).catch(print);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the singleton instance
|
||||
const service = new TodoService();
|
||||
|
||||
// make it global for easy use with cli
|
||||
globalThis.todo = service;
|
||||
|
||||
// export to use in other modules
|
||||
export default service;
|
||||
150
home/desktops/hyprland/ags/services/waifus.js
Normal file
150
home/desktops/hyprland/ags/services/waifus.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
// Note: this service is made mainly for waifu.im. Others might work but not as properly
|
||||
const APISERVICES = {
|
||||
'im': {
|
||||
'endpoint': 'https://api.waifu.im/search',
|
||||
'headers': { 'Accept-Version': 'v5' },
|
||||
},
|
||||
'nekos': {
|
||||
'endpoint': 'https://nekos.life/api/neko',
|
||||
'headers': {},
|
||||
},
|
||||
'pics': {
|
||||
'endpoint': 'https://api.waifu.pics/sfw/',
|
||||
'headers': {},
|
||||
},
|
||||
};
|
||||
|
||||
function paramStringFromObj(params) {
|
||||
return Object.entries(params)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) { // If it's an array, repeat
|
||||
if (value.length == 0) return '';
|
||||
let thisKey = `${encodeURIComponent(key)}=${encodeURIComponent(value[0])}`
|
||||
for (let i = 1; i < value.length; i++) {
|
||||
thisKey += `&${encodeURIComponent(key)}=${encodeURIComponent(value[i])}`;
|
||||
}
|
||||
return thisKey;
|
||||
}
|
||||
return `${key}=${value}`;
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
class WaifuService extends Service {
|
||||
_baseUrl = 'https://api.waifu.im/search';
|
||||
_mode = 'im'; // Allowed: im
|
||||
_responses = [];
|
||||
_queries = [];
|
||||
_nsfw = false;
|
||||
_minHeight = 600;
|
||||
|
||||
static {
|
||||
Service.register(this, {
|
||||
'initialized': [],
|
||||
'clear': [],
|
||||
'newResponse': ['int'],
|
||||
'updateResponse': ['int'],
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.emit('initialized');
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._responses = [];
|
||||
this._queries = [];
|
||||
this.emit('clear');
|
||||
}
|
||||
|
||||
get mode() { return this._mode }
|
||||
set mode(value) {
|
||||
this._mode = value;
|
||||
this._baseUrl = APISERVICES[this._mode].endpoint;
|
||||
}
|
||||
get nsfw() { return this._nsfw }
|
||||
set nsfw(value) { this._nsfw = value }
|
||||
get queries() { return this._queries }
|
||||
get responses() { return this._responses }
|
||||
|
||||
async fetch(msg) {
|
||||
// Init
|
||||
const userArgs = msg.split(/\s+/);
|
||||
|
||||
let taglist = [];
|
||||
this._nsfw = false;
|
||||
// Construct body/headers
|
||||
for (let i = 0; i < userArgs.length; i++) {
|
||||
const thisArg = userArgs[i].trim();
|
||||
if (thisArg.length == 0) continue;
|
||||
if (thisArg == '--im') this._mode = 'im';
|
||||
else if (thisArg == '--nekos') this._mode = 'nekos';
|
||||
else if (thisArg.includes('pics')) this._mode = 'pics';
|
||||
else if (thisArg.includes('segs') || thisArg.includes('sex') || thisArg.includes('lewd')) this._nsfw = true;
|
||||
else {
|
||||
taglist.push(thisArg);
|
||||
if (['ecchi', 'hentai', 'ero', 'ass', 'paizuri', 'oral', 'milf'].includes(thisArg)) this._nsfw = true;
|
||||
}
|
||||
}
|
||||
const newMessageId = this._queries.length;
|
||||
this._queries.push(taglist);
|
||||
this.emit('newResponse', newMessageId);
|
||||
const params = {
|
||||
'included_tags': taglist,
|
||||
'height': `>=${this._minHeight}`,
|
||||
'nsfw': this._nsfw,
|
||||
};
|
||||
const paramString = paramStringFromObj(params);
|
||||
// Fetch
|
||||
// Note: body isn't included since passing directly to url is more reliable
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: APISERVICES[this._mode].headers,
|
||||
};
|
||||
let status = 0;
|
||||
Utils.fetch(`${APISERVICES[this._mode].endpoint}?${paramString}`, options)
|
||||
.then(result => {
|
||||
status = result.status;
|
||||
return result.text();
|
||||
})
|
||||
.then((dataString) => { // Store interesting stuff and emit
|
||||
const parsedData = JSON.parse(dataString);
|
||||
if (!parsedData.images) this._responses.push({ // Failed
|
||||
status: status,
|
||||
signature: -1,
|
||||
url: '',
|
||||
extension: '',
|
||||
source: '',
|
||||
dominant_color: '#383A40',
|
||||
is_nsfw: false,
|
||||
width: 0,
|
||||
height: 0,
|
||||
tags: [],
|
||||
});
|
||||
else {
|
||||
const imageData = parsedData.images[0];
|
||||
this._responses.push({
|
||||
status: status,
|
||||
signature: imageData?.signature || -1,
|
||||
url: imageData?.url || undefined,
|
||||
extension: imageData.extension,
|
||||
source: imageData?.source,
|
||||
dominant_color: imageData?.dominant_color || '#9392A6',
|
||||
is_nsfw: imageData?.is_nsfw || false,
|
||||
width: imageData?.width || 0,
|
||||
height: imageData?.height || 0,
|
||||
tags: imageData?.tags.map(obj => obj["name"]) || [],
|
||||
});
|
||||
}
|
||||
this.emit('updateResponse', newMessageId);
|
||||
})
|
||||
.catch(print);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WaifuService();
|
||||
|
||||
70
home/desktops/hyprland/ags/services/wallpaper.js
Normal file
70
home/desktops/hyprland/ags/services/wallpaper.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const { Gdk, GLib } = imports.gi;
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
const WALLPAPER_CONFIG_PATH = `${GLib.get_user_state_dir()}/ags/user/wallpaper.json`;
|
||||
|
||||
class WallpaperService extends Service {
|
||||
static {
|
||||
Service.register(
|
||||
this,
|
||||
{ 'updated': [], },
|
||||
);
|
||||
}
|
||||
|
||||
_wallPath = '';
|
||||
_wallJson = [];
|
||||
_monitorCount = 1;
|
||||
|
||||
_save() {
|
||||
Utils.writeFile(JSON.stringify(this._wallJson), this._wallPath)
|
||||
.catch(print);
|
||||
}
|
||||
|
||||
add(path) {
|
||||
this._wallJson.push(path);
|
||||
this._save();
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
set(path, monitor = -1) {
|
||||
this._monitorCount = Gdk.Display.get_default()?.get_n_monitors() || 1;
|
||||
if (this._wallJson.length < this._monitorCount) this._wallJson[this._monitorCount - 1] = "";
|
||||
if (monitor == -1)
|
||||
this._wallJson.fill(path);
|
||||
else
|
||||
this._wallJson[monitor] = path;
|
||||
|
||||
this._save();
|
||||
this.emit('updated');
|
||||
}
|
||||
|
||||
get(monitor = 0) {
|
||||
return this._wallJson[monitor];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// How many screens?
|
||||
this._monitorCount = Gdk.Display.get_default()?.get_n_monitors() || 1;
|
||||
// Read config
|
||||
this._wallPath = WALLPAPER_CONFIG_PATH;
|
||||
try {
|
||||
const fileContents = Utils.readFile(this._wallPath);
|
||||
this._wallJson = JSON.parse(fileContents);
|
||||
}
|
||||
catch {
|
||||
Utils.exec(`bash -c 'mkdir -p ${GLib.get_user_cache_dir()}/ags/user'`);
|
||||
Utils.exec(`touch ${this._wallPath}`);
|
||||
Utils.writeFile('[]', this._wallPath).then(() => {
|
||||
this._wallJson = JSON.parse(Utils.readFile(this._wallPath))
|
||||
}).catch(print);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// instance
|
||||
const service = new WallpaperService();
|
||||
// make it global for easy use with cli
|
||||
globalThis['wallpaper'] = service;
|
||||
export default service;
|
||||
Reference in New Issue
Block a user