many changed, added nixos-hardware

This commit is contained in:
Charlie Root 2024-07-07 13:23:38 +02:00
commit 2accd81424
149 changed files with 19124 additions and 238 deletions

View file

@ -0,0 +1,59 @@
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;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
class BrightnessService 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(`brightnessctl s ${percent * 100}% -q`)
.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);
}
constructor() {
super();
const current = Number(exec('brightnessctl g'));
const max = Number(exec('brightnessctl m'));
this._screenValue = current / max;
}
// overwriting connectWidget method, lets you
// change the default event that widgets connect to
connectWidget(widget, callback, event = 'screen-changed') {
super.connectWidget(widget, callback, event);
}
}
// the singleton instance
const service = new BrightnessService();
// make it global for easy use with cli
globalThis.brightness = service;
// export to use in other modules
export default service;

View file

@ -0,0 +1,266 @@
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 './messages.js';
// 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 very brief explanation for each command\n3. Otherwise, when asked to summarize information or explaining concepts, you are encouraged to use bullet points and headings. Use casual language and be short and concise. \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.", },
];
function expandTilde(path) {
if (path.startsWith('~')) {
return GLib.get_home_dir() + path.slice(1);
} else {
return path;
}
}
// We're using many models to not be restricted to 3 messages per minute.
// The whole chat will be sent every request anyway.
Utils.exec(`mkdir -p ${GLib.get_user_cache_dir()}/ags/user/ai`);
const KEY_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/ai/openai_key.txt`;
const APIDOM_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/openai_api_dom.txt`;
function replaceapidom(URL) {
//Utils.writeFile(URL, "/tmp/openai-url-old.log"); // For debugging
if (fileExists(expandTilde(APIDOM_FILE_LOCATION))) {
var contents = Utils.readFile(expandTilde(APIDOM_FILE_LOCATION)).trim();
var URL = URL.toString().replace("api.openai.com", contents);
}
//Utils.writeFile(URL, "/tmp/openai-url.log"); // For debugging
return URL;
}
const CHAT_MODELS = ["gpt-3.5-turbo-1106", "gpt-3.5-turbo", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-0613"]
const ONE_CYCLE_COUNT = 3;
class ChatGPTMessage extends Service {
static {
Service.register(this,
{
'delta': ['string'],
},
{
'content': ['string'],
'thinking': ['boolean'],
'done': ['boolean'],
});
}
_role = '';
_content = '';
_thinking = false;
_done = false;
constructor(role, content, thinking = false, 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(thinking) {
this._thinking = thinking;
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 ChatGPTService extends Service {
static {
Service.register(this, {
'initialized': [],
'clear': [],
'newMsg': ['int'],
'hasKey': ['boolean'],
});
}
_assistantPrompt = true;
_messages = [];
_cycleModels = true;
_requestCount = 0;
_temperature = 0.9;
_modelIndex = 0;
_key = '';
_decoder = new TextDecoder();
url = GLib.Uri.parse(replaceapidom('https://api.openai.com/v1/chat/completions'), GLib.UriFlags.NONE);
constructor() {
super();
if (fileExists(expandTilde(KEY_FILE_LOCATION))) this._key = Utils.readFile(expandTilde(KEY_FILE_LOCATION)).trim();
else this.emit('hasKey', false);
if (this._assistantPrompt) this._messages = [...initMessages];
else this._messages = [];
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, expandTilde(KEY_FILE_LOCATION))
.then(this.emit('hasKey', true))
.catch(err => print(err));
}
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 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) {
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);
}
catch {
aiResponse.addDelta(line + '\n');
}
}
this.readResponse(stream, aiResponse);
});
}
addMessage(role, message) {
this._messages.push(new ChatGPTMessage(role, message));
this.emit('newMsg', this._messages.length - 1);
}
send(msg) {
this._messages.push(new ChatGPTMessage('user', msg));
this.emit('newMsg', this._messages.length - 1);
const aiResponse = new ChatGPTMessage('assistant', 'thinking...', true, false)
this._messages.push(aiResponse);
this.emit('newMsg', this._messages.length - 1);
const body = {
model: CHAT_MODELS[this._modelIndex],
messages: this._messages.map(msg => { let m = { role: msg.role, content: msg.content }; return m; }),
temperature: this._temperature,
// temperature: 2, // <- Nuts
stream: true,
};
const session = new Soup.Session();
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);
});
if (this._cycleModels) {
this._requestCount++;
if (this._cycleModels)
this._modelIndex = (this._requestCount - (this._requestCount % ONE_CYCLE_COUNT)) % CHAT_MODELS.length;
}
}
}
export default new ChatGPTService();

View file

@ -0,0 +1,277 @@
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 './messages.js';
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 very brief explanation for each command\n3. Otherwise, when asked to summarize information or explaining concepts, you are encouraged to use bullet points and headings. Use casual language and be short and concise. \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" }], },
];
function expandTilde(path) {
if (path.startsWith('~')) {
return GLib.get_home_dir() + path.slice(1);
} else {
return path;
}
}
Utils.exec(`mkdir -p ${GLib.get_user_cache_dir()}/ags/user/ai`);
const KEY_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/ai/google_key.txt`;
const APIDOM_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/ai/google_api_dom.txt`;
function replaceapidom(URL) {
if (fileExists(expandTilde(APIDOM_FILE_LOCATION))) {
var contents = Utils.readFile(expandTilde(APIDOM_FILE_LOCATION)).trim();
var URL = URL.toString().replace("generativelanguage.googleapis.com", contents);
}
return URL;
}
const CHAT_MODELS = ["gemini-pro"]
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 = false;
_done = false;
_rawData = '';
constructor(role, content, thinking = false, 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(thinking) {
this._thinking = thinking;
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);
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 = true;
_messages = [];
_cycleModels = true;
_requestCount = 0;
_temperature = 0.9;
_modelIndex = 0;
_key = '';
_decoder = new TextDecoder();
constructor() {
super();
if (fileExists(expandTilde(KEY_FILE_LOCATION))) this._key = Utils.readFile(expandTilde(KEY_FILE_LOCATION)).trim();
else this.emit('hasKey', false);
if (this._assistantPrompt) this._messages = [...initMessages];
else this._messages = [];
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, expandTilde(KEY_FILE_LOCATION))
.then(this.emit('hasKey', true))
.catch(err => print(err));
}
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 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) {
stream.read_line_async(
0, null,
(stream, res) => {
try {
const [bytes] = stream.read_line_finish(res);
const line = this._decoder.decode(bytes);
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;
return;
}
});
}
addMessage(role, message) {
this._messages.push(new GeminiMessage(role, message));
this.emit('newMsg', this._messages.length - 1);
}
send(msg) {
this._messages.push(new GeminiMessage('user', msg));
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": [
// { 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 session = new Soup.Session();
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();

View file

@ -0,0 +1,41 @@
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;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
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;

View file

@ -0,0 +1,58 @@
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.build_filenamev([GLib.get_user_cache_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 = `Looks like this is your first run. For a list of keybinds, hit <span foreground="#c06af1" font_weight="bold">Super + /</span>.`;
export async function firstRunWelcome() {
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"]);
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);
}
}
const BATTERY_WARN_LEVELS = [20, 15, 5];
const BATTERY_WARN_TITLES = ["Low battery", "Very low battery", 'Critical Battery']
const BATTERY_WARN_BODIES = ["Plug in the charger", "You there?", 'PLUG THE CHARGER ALREADY']
var batteryWarned = false;
async function batteryMessage() {
const perc = Battery.percent;
const charging = Battery.charging;
if(charging) {
batteryWarned = false;
return;
}
for (let i = BATTERY_WARN_LEVELS.length - 1; i >= 0; i--) {
if (perc <= BATTERY_WARN_LEVELS[i] && !charging && !batteryWarned) {
batteryWarned = true;
Utils.execAsync(['bash', '-c',
`notify-send "${BATTERY_WARN_TITLES[i]}" "${BATTERY_WARN_BODIES[i]}" -u critical -a 'ags' &`
]).catch(print);
break;
}
}
}
// Run them
firstRunWelcome();
Utils.timeout(1, () => {
Battery.connect('changed', () => batteryMessage());
})

View 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;

View file

@ -0,0 +1,89 @@
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;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
function fileExists(filePath) {
let file = Gio.File.new_for_path(filePath);
return file.query_exists(null);
}
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_cache_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;

View file

@ -0,0 +1,161 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
// Usage from my python waifu fetcher, for reference
// Usage: waifu-get.py [OPTION]... [TAG]...
// Options:
// --im\tUse waifu.im API. You can use many tags
// --pics\tUse waifu.pics API. Use 1 tag only.
// --nekos\tUse nekos.life (old) API. No tags.
// --segs\tForce NSFW images
// Tags:
// waifu.im (type):
// maid waifu marin-kitagawa mori-calliope raiden-shogun oppai selfies uniform
// waifu.im (nsfw tags):
// ecchi hentai ero ass paizuri oral milf
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 {
_endpoints = {
'im': 'https://api.waifu.im/search',
'nekos': 'https://nekos.life/api/neko',
'pics': 'https://api.waifu.pics/sfw/',
}
_headers = {
'im': { 'Accept-Version': 'v5' },
'nekos': {},
'pics': {},
}
_baseUrl = 'https://api.waifu.im/search';
_mode = 'im'; // Allowed: im
_responses = [];
_queries = [];
_nsfw = false;
_minHeight = 600;
_status = 0;
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 = this._endpoints[this._mode];
}
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: this._headers[this._mode],
};
var status = 0;
Utils.fetch(`${this._endpoints[this._mode]}?${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({
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(console.error)
}
}
export default new WaifuService();

View file

@ -0,0 +1,75 @@
const { Gdk, 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;
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
function fileExists(filePath) {
let file = Gio.File.new_for_path(filePath);
return file.query_exists(null);
}
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 = `${GLib.get_user_cache_dir()}/ags/user/wallpaper.json`;
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;