Initial commit

This commit is contained in:
end-4
2023-12-25 18:16:14 +07:00
commit 16b0d77075
174 changed files with 18893 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
import { Service, Utils } from '../imports.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, let's 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;
+261
View File
@@ -0,0 +1,261 @@
import { Utils, Widget } from '../imports.js';
import Service from 'resource:///com/github/Aylur/ags/service.js';
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Soup from 'gi://Soup?version=3.0';
import { fileExists } from './messages.js';
// This is for custom prompt
// It's hard to make gpt-3.5 listen to all these, I know
// Disabled by default
const initMessages =
[
{
role: "user",
content: `
## Style
- You should a natural tone like a real conversation!
## Formatting
- Try to use **bold**, _italics_ and __underline__ extensively. Using bullet points is also encouraged.
- When providing code blocks or facts, precede with h2 heading (\`##\`) and use 2 spaces for indentation, not 4.
- Use dividers (\`---\`) to separate different information.
## Content
- When asked to perform system tasks, include a bash code block to handle it on a Linux desktop with Wayland.
- Unless requested otherwise or asked writing questions, be as short and concise as possible.
`,
thinking: false,
done: true
},
{
role: "assistant",
content: "Got it! I'll try to give commands to perform Linux tasks. I'll try to use markdown features extensively, use divider when appropriate, and use a heading for code blocks. All code blocks should use 2 spaces for indent. And most importantly, I'll speak naturally.",
thinking: false,
done: true,
}
]
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.
const KEY_FILE_LOCATION = `~/.cache/ags/user/openai_api_key.txt`;
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 = false;
_messages = [];
_cycleModels = true;
_requestCount = 0;
_modelIndex = 0;
_key = '';
_decoder = new TextDecoder();
url = GLib.Uri.parse('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 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; }),
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();
+40
View File
@@ -0,0 +1,40 @@
import { Service, Utils } from '../imports.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;
+58
View File
@@ -0,0 +1,58 @@
const { Notify, GLib, Gio } = imports.gi;
import { Utils } from '../imports.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 = "ags";
const FIRST_RUN_NOTIF_TITLE = "Welcome!";
const FIRST_RUN_NOTIF_BODY = `Looks like this is your first run.\nHit <span foreground="#c06af1" font_weight="bold">Super + /</span> for a list of keybinds.`;
export async function firstRunWelcome() {
if (!fileExists(FIRST_RUN_PATH)) {
console.log('uuwuwuwuwuwuwuwuu');
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(['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());
})
+86
View File
@@ -0,0 +1,86 @@
const { Gio, Gdk, GLib, Gtk } = imports.gi;
import { Service, Utils } from '../imports.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;
}
add(content) {
this._todoJson.push({ content, done: false });
Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
.catch(print);
this.emit('updated');
}
check(index) {
this._todoJson[index].done = true;
Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
.catch(print);
this.emit('updated');
}
uncheck(index) {
this._todoJson[index].done = false;
Utils.writeFile(JSON.stringify(this._todoJson), this._todoPath)
.catch(print);
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`;
if (!fileExists(this._todoPath)) { // No? create file with empty array
Utils.exec(`bash -c 'mkdir -p ~/.cache/ags/user'`);
Utils.exec(`touch ${this._todoPath}`);
Utils.writeFile("[]", this._todoPath).then(() => {
this._todoJson = JSON.parse(Utils.readFile(this._todoPath))
}).catch(print);
}
else {
const fileContents = Utils.readFile(this._todoPath);
this._todoJson = JSON.parse(fileContents);
}
}
}
// 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;