forked from Shinonome/dots-hyprland
Initial commit
This commit is contained in:
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
})
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user