20 changed files with 1780 additions and 0 deletions
@ -0,0 +1,31 @@ |
|||||||
|
"use strict"; |
||||||
|
import GLib from 'gi://GLib'; |
||||||
|
import App from 'resource:///com/github/Aylur/ags/app.js' |
||||||
|
import userOptions from './modules/.configuration/user_options.js'; |
||||||
|
import Overview from './modules/overview/main.js'; |
||||||
|
|
||||||
|
const COMPILED_STYLE_DIR = `${GLib.get_user_config_dir()}/ags/user/` |
||||||
|
|
||||||
|
async function applyStyle() { |
||||||
|
|
||||||
|
App.resetCss(); |
||||||
|
App.applyCss(`${COMPILED_STYLE_DIR}/style.css`); |
||||||
|
console.log('[LOG] Styles loaded') |
||||||
|
} |
||||||
|
applyStyle().catch(print); |
||||||
|
|
||||||
|
const Windows = () => [ |
||||||
|
Overview() |
||||||
|
]; |
||||||
|
const CLOSE_ANIM_TIME = 210; |
||||||
|
App.config({ |
||||||
|
css: `${COMPILED_STYLE_DIR}/style.css`, |
||||||
|
stackTraceOnError: true, |
||||||
|
closeWindowDelay: { |
||||||
|
'sideright': CLOSE_ANIM_TIME, |
||||||
|
'sideleft': CLOSE_ANIM_TIME, |
||||||
|
'osk': CLOSE_ANIM_TIME, |
||||||
|
}, |
||||||
|
windows: Windows().flat(1), |
||||||
|
}); |
||||||
|
|
||||||
@ -0,0 +1,121 @@ |
|||||||
|
|
||||||
|
import userOverrides from '../../user_options.js'; |
||||||
|
|
||||||
|
// Defaults
|
||||||
|
let configOptions = { |
||||||
|
// General stuff
|
||||||
|
'ai': { |
||||||
|
'defaultGPTProvider': "openai", |
||||||
|
'defaultTemperature': 0.9, |
||||||
|
'enhancements': true, |
||||||
|
'useHistory': true, |
||||||
|
'writingCursor': " ...", // Warning: Using weird characters can mess up Markdown rendering
|
||||||
|
}, |
||||||
|
'animations': { |
||||||
|
'choreographyDelay': 35, |
||||||
|
'durationSmall': 110, |
||||||
|
'durationLarge': 180, |
||||||
|
}, |
||||||
|
'appearance': { |
||||||
|
'keyboardUseFlag': false, // Use flag emoji instead of abbreviation letters
|
||||||
|
}, |
||||||
|
'apps': { |
||||||
|
'imageViewer': "loupe", |
||||||
|
'terminal': "foot", // This is only for shell actions
|
||||||
|
}, |
||||||
|
'battery': { |
||||||
|
'low': 20, |
||||||
|
'critical': 10, |
||||||
|
}, |
||||||
|
'music': { |
||||||
|
'preferredPlayer': "plasma-browser-integration", |
||||||
|
}, |
||||||
|
'onScreenKeyboard': { |
||||||
|
'layout': "qwerty_full", // See modules/onscreenkeyboard/onscreenkeyboard.js for available layouts
|
||||||
|
}, |
||||||
|
'overview': { |
||||||
|
'scale': 0.18, // Relative to screen size
|
||||||
|
'numOfRows': 2, |
||||||
|
'numOfCols': 5, |
||||||
|
'wsNumScale': 0.09, |
||||||
|
'wsNumMarginScale': 0.07, |
||||||
|
}, |
||||||
|
'sidebar': { |
||||||
|
'imageColumns': 2, |
||||||
|
'imageBooruCount': 20, |
||||||
|
'imageAllowNsfw': false, |
||||||
|
}, |
||||||
|
'search': { |
||||||
|
'engineBaseUrl': "https://www.google.com/search?q=", |
||||||
|
'excludedSites': [], //add site to exclude from result. eg: "quora.com"
|
||||||
|
}, |
||||||
|
'time': { |
||||||
|
// See https://docs.gtk.org/glib/method.DateTime.format.html
|
||||||
|
// Here's the 12h format: "%I:%M%P"
|
||||||
|
// For seconds, add "%S" and set interval to 1000
|
||||||
|
'format': "%H:%M", |
||||||
|
'interval': 5000, |
||||||
|
'dateFormatLong': "%A, %d/%m", // On bar
|
||||||
|
'dateInterval': 5000, |
||||||
|
'dateFormat': "%d/%m", // On notif time
|
||||||
|
}, |
||||||
|
'weather': { |
||||||
|
'city': "", |
||||||
|
}, |
||||||
|
'workspaces': { |
||||||
|
'shown': 10, |
||||||
|
}, |
||||||
|
// Longer stuff
|
||||||
|
'icons': { |
||||||
|
substitutions: { |
||||||
|
'code-url-handler': "visual-studio-code", |
||||||
|
'Code': "visual-studio-code", |
||||||
|
'GitHub Desktop': "github-desktop", |
||||||
|
'Minecraft* 1.20.1': "minecraft", |
||||||
|
'gnome-tweaks': "org.gnome.tweaks", |
||||||
|
'pavucontrol-qt': "pavucontrol", |
||||||
|
'wps': "wps-office2019-kprometheus", |
||||||
|
'wpsoffice': "wps-office2019-kprometheus", |
||||||
|
'': "image-missing", |
||||||
|
} |
||||||
|
}, |
||||||
|
'keybinds': { |
||||||
|
// Format: Mod1+Mod2+key. CaSe SeNsItIvE!
|
||||||
|
// Modifiers: Shift Ctrl Alt Hyper Meta
|
||||||
|
// See https://docs.gtk.org/gdk3/index.html#constants for the other keys (they are listed as KEY_key)
|
||||||
|
'overview': { |
||||||
|
'altMoveLeft': "Ctrl+b", |
||||||
|
'altMoveRight': "Ctrl+f", |
||||||
|
'deleteToEnd': "Ctrl+k", |
||||||
|
}, |
||||||
|
'sidebar': { |
||||||
|
'apis': { |
||||||
|
'nextTab': "Page_Down", |
||||||
|
'prevTab': "Page_Up", |
||||||
|
}, |
||||||
|
'options': { // Right sidebar
|
||||||
|
'nextTab': "Page_Down", |
||||||
|
'prevTab': "Page_Up", |
||||||
|
}, |
||||||
|
'pin': "Ctrl+p", |
||||||
|
'cycleTab': "Ctrl+Tab", |
||||||
|
'nextTab': "Ctrl+Page_Down", |
||||||
|
'prevTab': "Ctrl+Page_Up", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
// Override defaults with user's options
|
||||||
|
function overrideConfigRecursive(userOverrides, configOptions = {}) { |
||||||
|
for (const [key, value] of Object.entries(userOverrides)) { |
||||||
|
if (typeof value === 'object') { |
||||||
|
overrideConfigRecursive(value, configOptions[key]); |
||||||
|
} else { |
||||||
|
configOptions[key] = value; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
overrideConfigRecursive(userOverrides, configOptions); |
||||||
|
|
||||||
|
globalThis['userOptions'] = configOptions; |
||||||
|
export default configOptions; |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
const { Gtk } = imports.gi; |
||||||
|
|
||||||
|
export function iconExists(iconName) { |
||||||
|
let iconTheme = Gtk.IconTheme.get_default(); |
||||||
|
return iconTheme.has_icon(iconName); |
||||||
|
} |
||||||
|
|
||||||
|
export function substitute(str) { |
||||||
|
if(userOptions.icons.substitutions[str]) return userOptions.icons.substitutions[str]; |
||||||
|
|
||||||
|
if (!iconExists(str)) str = str.toLowerCase().replace(/\s+/g, '-'); // Turn into kebab-case
|
||||||
|
return str; |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
|
||||||
|
export function clamp(x, min, max) { |
||||||
|
return Math.min(Math.max(x, min), max); |
||||||
|
} |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
const { GLib } = imports.gi; |
||||||
|
import Variable from 'resource:///com/github/Aylur/ags/variable.js'; |
||||||
|
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; |
||||||
|
const { execAsync, exec } = Utils; |
||||||
|
|
||||||
|
export const distroID = exec(`bash -c 'cat /etc/os-release | grep "^ID=" | cut -d "=" -f 2 | sed "s/\\"//g"'`).trim(); |
||||||
|
export const isDebianDistro = (distroID == 'linuxmint' || distroID == 'ubuntu' || distroID == 'debian' || distroID == 'zorin' || distroID == 'popos' || distroID == 'raspbian' || distroID == 'kali'); |
||||||
|
export const isArchDistro = (distroID == 'arch' || distroID == 'endeavouros' || distroID == 'cachyos'); |
||||||
|
export const hasFlatpak = !!exec(`bash -c 'command -v flatpak'`); |
||||||
|
|
||||||
|
const LIGHTDARK_FILE_LOCATION = `${GLib.get_user_cache_dir()}/ags/user/colormode.txt`; |
||||||
|
const colorMode = Utils.exec('bash -c "sed -n \'1p\' $HOME/.cache/ags/user/colormode.txt"'); |
||||||
|
export let darkMode = Variable(!(Utils.readFile(LIGHTDARK_FILE_LOCATION).split('\n')[0].trim() == 'light')); |
||||||
|
export const hasPlasmaIntegration = !!Utils.exec('bash -c "command -v plasma-browser-integration-host"'); |
||||||
|
|
||||||
|
export const getDistroIcon = () => { |
||||||
|
// Arches
|
||||||
|
if(distroID == 'arch') return 'arch-symbolic'; |
||||||
|
if(distroID == 'endeavouros') return 'endeavouros-symbolic'; |
||||||
|
if(distroID == 'cachyos') return 'cachyos-symbolic'; |
||||||
|
// Funny flake
|
||||||
|
if(distroID == 'nixos') return 'nixos-symbolic'; |
||||||
|
// Cool thing
|
||||||
|
if(distroID == 'fedora') return 'fedora-symbolic'; |
||||||
|
// Debians
|
||||||
|
if(distroID == 'linuxmint') return 'ubuntu-symbolic'; |
||||||
|
if(distroID == 'ubuntu') return 'ubuntu-symbolic'; |
||||||
|
if(distroID == 'debian') return 'debian-symbolic'; |
||||||
|
if(distroID == 'zorin') return 'ubuntu-symbolic'; |
||||||
|
if(distroID == 'popos') return 'ubuntu-symbolic'; |
||||||
|
if(distroID == 'raspbian') return 'debian-symbolic'; |
||||||
|
if(distroID == 'kali') return 'debian-symbolic'; |
||||||
|
return 'linux-symbolic'; |
||||||
|
} |
||||||
|
|
||||||
|
export const getDistroName = () => { |
||||||
|
// Arches
|
||||||
|
if(distroID == 'arch') return 'Arch Linux'; |
||||||
|
if(distroID == 'endeavouros') return 'EndeavourOS'; |
||||||
|
if(distroID == 'cachyos') return 'CachyOS'; |
||||||
|
// Funny flake
|
||||||
|
if(distroID == 'nixos') return 'NixOS'; |
||||||
|
// Cool thing
|
||||||
|
if(distroID == 'fedora') return 'Fedora'; |
||||||
|
// Debians
|
||||||
|
if(distroID == 'linuxmint') return 'Linux Mint'; |
||||||
|
if(distroID == 'ubuntu') return 'Ubuntu'; |
||||||
|
if(distroID == 'debian') return 'Debian'; |
||||||
|
if(distroID == 'zorin') return 'Zorin'; |
||||||
|
if(distroID == 'popos') return 'Pop!_OS'; |
||||||
|
if(distroID == 'raspbian') return 'Raspbian'; |
||||||
|
if(distroID == 'kali') return 'Kali Linux'; |
||||||
|
return 'Linux'; |
||||||
|
} |
||||||
@ -0,0 +1,86 @@ |
|||||||
|
import Widget from 'resource:///com/github/Aylur/ags/widget.js'; |
||||||
|
|
||||||
|
const { Revealer, Scrollable } = Widget; |
||||||
|
|
||||||
|
export const MarginRevealer = ({ |
||||||
|
transition = 'slide_down', |
||||||
|
child, |
||||||
|
revealChild, |
||||||
|
showClass = 'element-show', // These are for animation curve, they don't really hide
|
||||||
|
hideClass = 'element-hide', // Don't put margins in these classes!
|
||||||
|
extraSetup = () => { }, |
||||||
|
...rest |
||||||
|
}) => { |
||||||
|
const widget = Scrollable({ |
||||||
|
...rest, |
||||||
|
attribute: { |
||||||
|
'revealChild': true, // It'll be set to false after init if it's supposed to hide
|
||||||
|
'transition': transition, |
||||||
|
'show': () => { |
||||||
|
if (widget.attribute.revealChild) return; |
||||||
|
widget.hscroll = 'never'; |
||||||
|
widget.vscroll = 'never'; |
||||||
|
child.toggleClassName(hideClass, false); |
||||||
|
child.toggleClassName(showClass, true); |
||||||
|
widget.attribute.revealChild = true; |
||||||
|
child.css = 'margin: 0px;'; |
||||||
|
}, |
||||||
|
'hide': () => { |
||||||
|
if (!widget.attribute.revealChild) return; |
||||||
|
child.toggleClassName(hideClass, true); |
||||||
|
child.toggleClassName(showClass, false); |
||||||
|
widget.attribute.revealChild = false; |
||||||
|
if (widget.attribute.transition == 'slide_left') |
||||||
|
child.css = `margin-right: -${child.get_allocated_width()}px;`; |
||||||
|
else if (widget.attribute.transition == 'slide_right') |
||||||
|
child.css = `margin-left: -${child.get_allocated_width()}px;`; |
||||||
|
else if (widget.attribute.transition == 'slide_up') |
||||||
|
child.css = `margin-bottom: -${child.get_allocated_height()}px;`; |
||||||
|
else if (widget.attribute.transition == 'slide_down') |
||||||
|
child.css = `margin-top: -${child.get_allocated_height()}px;`; |
||||||
|
}, |
||||||
|
'toggle': () => { |
||||||
|
if (widget.attribute.revealChild) widget.attribute.hide(); |
||||||
|
else widget.attribute.show(); |
||||||
|
}, |
||||||
|
}, |
||||||
|
child: child, |
||||||
|
hscroll: `${revealChild ? 'never' : 'always'}`, |
||||||
|
vscroll: `${revealChild ? 'never' : 'always'}`, |
||||||
|
setup: (self) => { |
||||||
|
extraSetup(self); |
||||||
|
} |
||||||
|
}); |
||||||
|
child.toggleClassName(`${revealChild ? showClass : hideClass}`, true); |
||||||
|
return widget; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: Allow reveal update. Currently this just helps at declaration
|
||||||
|
export const DoubleRevealer = ({ |
||||||
|
transition1 = 'slide_right', |
||||||
|
transition2 = 'slide_left', |
||||||
|
duration1 = 150, |
||||||
|
duration2 = 150, |
||||||
|
child, |
||||||
|
revealChild, |
||||||
|
...rest |
||||||
|
}) => { |
||||||
|
const r2 = Revealer({ |
||||||
|
transition: transition2, |
||||||
|
transitionDuration: duration2, |
||||||
|
revealChild: revealChild, |
||||||
|
child: child, |
||||||
|
}); |
||||||
|
const r1 = Revealer({ |
||||||
|
transition: transition1, |
||||||
|
transitionDuration: duration1, |
||||||
|
revealChild: revealChild, |
||||||
|
child: r2, |
||||||
|
...rest, |
||||||
|
}) |
||||||
|
r1.toggleRevealChild = (value) => { |
||||||
|
r1.revealChild = value; |
||||||
|
r2.revealChild = value; |
||||||
|
} |
||||||
|
return r1; |
||||||
|
} |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import App from 'resource:///com/github/Aylur/ags/app.js'; |
||||||
|
import Widget from 'resource:///com/github/Aylur/ags/widget.js'; |
||||||
|
const { Box, Window } = Widget; |
||||||
|
|
||||||
|
|
||||||
|
export default ({ |
||||||
|
name, |
||||||
|
child, |
||||||
|
showClassName = "", |
||||||
|
hideClassName = "", |
||||||
|
...props |
||||||
|
}) => { |
||||||
|
return Window({ |
||||||
|
name, |
||||||
|
visible: false, |
||||||
|
layer: 'overlay', |
||||||
|
...props, |
||||||
|
|
||||||
|
child: Box({ |
||||||
|
setup: (self) => { |
||||||
|
self.hook(App, (self, currentName, visible) => { |
||||||
|
if (currentName === name) { |
||||||
|
self.toggleClassName(hideClassName, !visible); |
||||||
|
} |
||||||
|
}).keybind("Escape", () => App.closeWindow(name)) |
||||||
|
if (showClassName !== "" && hideClassName !== "") |
||||||
|
self.className = `${showClassName} ${hideClassName}`; |
||||||
|
}, |
||||||
|
child: child, |
||||||
|
}), |
||||||
|
}); |
||||||
|
} |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
import Cairo from 'gi://cairo?version=1.0'; |
||||||
|
|
||||||
|
export const dummyRegion = new Cairo.Region(); |
||||||
|
export const enableClickthrough = (self) => self.input_shape_combine_region(dummyRegion); |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
const { Gdk } = imports.gi; |
||||||
|
|
||||||
|
export function setupCursorHover(button) { // Hand pointing cursor on hover
|
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
button.connect('enter-notify-event', () => { |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'pointer'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
|
||||||
|
button.connect('leave-notify-event', () => { |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'default'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
export function setupCursorHoverAim(button) { // Crosshair cursor on hover
|
||||||
|
button.connect('enter-notify-event', () => { |
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'crosshair'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
|
||||||
|
button.connect('leave-notify-event', () => { |
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'default'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export function setupCursorHoverGrab(button) { // Hand ready to grab on hover
|
||||||
|
button.connect('enter-notify-event', () => { |
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'grab'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
|
||||||
|
button.connect('leave-notify-event', () => { |
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'default'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export function setupCursorHoverInfo(button) { // "?" mark cursor on hover
|
||||||
|
const display = Gdk.Display.get_default(); |
||||||
|
button.connect('enter-notify-event', () => { |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'help'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
|
||||||
|
button.connect('leave-notify-event', () => { |
||||||
|
const cursor = Gdk.Cursor.new_from_name(display, 'default'); |
||||||
|
button.get_window().set_cursor(cursor); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,25 @@ |
|||||||
|
const { Gdk } = imports.gi; |
||||||
|
|
||||||
|
const MODS = { |
||||||
|
'Shift': Gdk.ModifierType.SHIFT_MASK, |
||||||
|
'Ctrl': Gdk.ModifierType.CONTROL_MASK, |
||||||
|
'Alt': Gdk.ModifierType.ALT_MASK, |
||||||
|
'Hyper': Gdk.ModifierType.HYPER_MASK, |
||||||
|
'Meta': Gdk.ModifierType.META_MASK |
||||||
|
} |
||||||
|
|
||||||
|
export const checkKeybind = (event, keybind) => { |
||||||
|
const pressedModMask = event.get_state()[1]; |
||||||
|
const pressedKey = event.get_keyval()[1]; |
||||||
|
const keys = keybind.split('+'); |
||||||
|
for (let i = 0; i < keys.length; i++) { |
||||||
|
if (keys[i] in MODS) { |
||||||
|
if (!(pressedModMask & MODS[keys[i]])) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} else if (pressedKey !== Gdk[`KEY_${keys[i]}`]) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; |
||||||
|
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; |
||||||
|
|
||||||
|
function moveClientToWorkspace(address, workspace) { |
||||||
|
Utils.execAsync(['bash', '-c', `hyprctl dispatch movetoworkspacesilent ${workspace},address:${address} &`]); |
||||||
|
} |
||||||
|
|
||||||
|
export function dumpToWorkspace(from, to) { |
||||||
|
if (from == to) return; |
||||||
|
Hyprland.clients.forEach(client => { |
||||||
|
if (client.workspace.id == from) { |
||||||
|
moveClientToWorkspace(client.address, to); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
export function swapWorkspace(workspaceA, workspaceB) { |
||||||
|
if (workspaceA == workspaceB) return; |
||||||
|
const clientsA = []; |
||||||
|
const clientsB = []; |
||||||
|
Hyprland.clients.forEach(client => { |
||||||
|
if (client.workspace.id == workspaceA) clientsA.push(client.address); |
||||||
|
if (client.workspace.id == workspaceB) clientsB.push(client.address); |
||||||
|
}); |
||||||
|
|
||||||
|
clientsA.forEach((address) => moveClientToWorkspace(address, workspaceB)); |
||||||
|
clientsB.forEach((address) => moveClientToWorkspace(address, workspaceA)); |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
import Widget from 'resource:///com/github/Aylur/ags/widget.js'; |
||||||
|
import { SearchAndWindows } from "./windowcontent.js"; |
||||||
|
import PopupWindow from '../.widgethacks/popupwindow.js'; |
||||||
|
|
||||||
|
export default (id = '') => PopupWindow({ |
||||||
|
name: `overview${id}`, |
||||||
|
exclusivity: 'ignore', |
||||||
|
keymode: 'exclusive', |
||||||
|
visible: false, |
||||||
|
// anchor: ['middle'],
|
||||||
|
layer: 'overlay', |
||||||
|
child: Widget.Box({ |
||||||
|
vertical: true, |
||||||
|
children: [ |
||||||
|
SearchAndWindows(), |
||||||
|
] |
||||||
|
}), |
||||||
|
}) |
||||||
@ -0,0 +1,155 @@ |
|||||||
|
const { Gio, GLib } = imports.gi; |
||||||
|
import App from 'resource:///com/github/Aylur/ags/app.js'; |
||||||
|
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; |
||||||
|
const { execAsync, exec } = Utils; |
||||||
|
// import Todo from "../../services/todo.js";
|
||||||
|
import { darkMode } from '../.miscutils/system.js'; |
||||||
|
|
||||||
|
export function hasUnterminatedBackslash(inputString) { |
||||||
|
// Use a regular expression to match a trailing odd number of backslashes
|
||||||
|
const regex = /\\+$/; |
||||||
|
return regex.test(inputString); |
||||||
|
} |
||||||
|
|
||||||
|
export function launchCustomCommand(command) { |
||||||
|
const args = command.toLowerCase().split(' '); |
||||||
|
if (args[0] == '>raw') { // Mouse raw input
|
||||||
|
Utils.execAsync('hyprctl -j getoption input:accel_profile') |
||||||
|
.then((output) => { |
||||||
|
const value = JSON.parse(output)["str"].trim(); |
||||||
|
if (value != "[[EMPTY]]" && value != "") { |
||||||
|
execAsync(['bash', '-c', `hyprctl keyword input:accel_profile '[[EMPTY]]'`]).catch(print); |
||||||
|
} |
||||||
|
else { |
||||||
|
execAsync(['bash', '-c', `hyprctl keyword input:accel_profile flat`]).catch(print); |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
else if (args[0] == '>img') { // Change wallpaper
|
||||||
|
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchwall.sh`, `&`]).catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>color') { // Generate colorscheme from color picker
|
||||||
|
execAsync([`bash`, `-c`, `${App.configDir}/scripts/color_generation/switchcolor.sh --pick`, `&`]).catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>light') { // Light mode
|
||||||
|
darkMode.value = false; |
||||||
|
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "1s/.*/light/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) |
||||||
|
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) |
||||||
|
.catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>dark') { // Dark mode
|
||||||
|
darkMode.value = true; |
||||||
|
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "1s/.*/dark/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) |
||||||
|
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) |
||||||
|
.catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>badapple') { // Black and white
|
||||||
|
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && sed -i "3s/.*/monochrome/" ${GLib.get_user_cache_dir()}/ags/user/colormode.txt`]) |
||||||
|
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchcolor.sh`])) |
||||||
|
.catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>material') { // Use material colors
|
||||||
|
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && echo "material" > ${GLib.get_user_cache_dir()}/ags/user/colorbackend.txt`]).catch(print) |
||||||
|
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print)) |
||||||
|
.catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>pywal') { // Use Pywal (ik it looks shit but I'm not removing)
|
||||||
|
execAsync([`bash`, `-c`, `mkdir -p ${GLib.get_user_cache_dir()}/ags/user && echo "pywal" > ${GLib.get_user_cache_dir()}/ags/user/colorbackend.txt`]).catch(print) |
||||||
|
.then(execAsync(['bash', '-c', `${App.configDir}/scripts/color_generation/switchwall.sh --noswitch`]).catch(print)) |
||||||
|
.catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>todo') { // Todo
|
||||||
|
Todo.add(args.slice(1).join(' ')); |
||||||
|
} |
||||||
|
else if (args[0] == '>shutdown') { // Shut down
|
||||||
|
execAsync([`bash`, `-c`, `systemctl poweroff || loginctl poweroff`]).catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>reboot') { // Reboot
|
||||||
|
execAsync([`bash`, `-c`, `systemctl reboot || loginctl reboot`]).catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>sleep') { // Sleep
|
||||||
|
execAsync([`bash`, `-c`, `systemctl suspend || loginctl suspend`]).catch(print); |
||||||
|
} |
||||||
|
else if (args[0] == '>logout') { // Log out
|
||||||
|
execAsync([`bash`, `-c`, `pkill Hyprland || pkill sway`]).catch(print); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function execAndClose(command, terminal) { |
||||||
|
App.closeWindow('overview'); |
||||||
|
if (terminal) { |
||||||
|
execAsync([`bash`, `-c`, `${userOptions.apps.terminal} fish -C "${command}"`, `&`]).catch(print); |
||||||
|
} |
||||||
|
else |
||||||
|
execAsync(command).catch(print); |
||||||
|
} |
||||||
|
|
||||||
|
export function couldBeMath(str) { |
||||||
|
const regex = /^[0-9.+*/-]/; |
||||||
|
return regex.test(str); |
||||||
|
} |
||||||
|
|
||||||
|
export function expandTilde(path) { |
||||||
|
if (path.startsWith('~')) { |
||||||
|
return GLib.get_home_dir() + path.slice(1); |
||||||
|
} else { |
||||||
|
return path; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getFileIcon(fileInfo) { |
||||||
|
let icon = fileInfo.get_icon(); |
||||||
|
if (icon) { |
||||||
|
// Get the icon's name
|
||||||
|
return icon.get_names()[0]; |
||||||
|
} else { |
||||||
|
// Default icon for files
|
||||||
|
return 'text-x-generic'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function ls({ path = '~', silent = false }) { |
||||||
|
let contents = []; |
||||||
|
try { |
||||||
|
let expandedPath = expandTilde(path); |
||||||
|
if (expandedPath.endsWith('/')) |
||||||
|
expandedPath = expandedPath.slice(0, -1); |
||||||
|
let folder = Gio.File.new_for_path(expandedPath); |
||||||
|
|
||||||
|
let enumerator = folder.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); |
||||||
|
let fileInfo; |
||||||
|
while ((fileInfo = enumerator.next_file(null)) !== null) { |
||||||
|
let fileName = fileInfo.get_display_name(); |
||||||
|
let fileType = fileInfo.get_file_type(); |
||||||
|
|
||||||
|
let item = { |
||||||
|
parentPath: expandedPath, |
||||||
|
name: fileName, |
||||||
|
type: fileType === Gio.FileType.DIRECTORY ? 'folder' : 'file', |
||||||
|
icon: getFileIcon(fileInfo), |
||||||
|
}; |
||||||
|
|
||||||
|
// Add file extension for files
|
||||||
|
if (fileType === Gio.FileType.REGULAR) { |
||||||
|
let fileExtension = fileName.split('.').pop(); |
||||||
|
item.type = `${fileExtension}`; |
||||||
|
} |
||||||
|
|
||||||
|
contents.push(item); |
||||||
|
contents.sort((a, b) => { |
||||||
|
const aIsFolder = a.type.startsWith('folder'); |
||||||
|
const bIsFolder = b.type.startsWith('folder'); |
||||||
|
if (aIsFolder && !bIsFolder) { |
||||||
|
return -1; |
||||||
|
} else if (!aIsFolder && bIsFolder) { |
||||||
|
return 1; |
||||||
|
} else { |
||||||
|
return a.name.localeCompare(b.name); // Sort alphabetically within folders and files
|
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
if (!silent) console.log(e); |
||||||
|
} |
||||||
|
return contents; |
||||||
|
} |
||||||
@ -0,0 +1,423 @@ |
|||||||
|
// TODO
|
||||||
|
// - Make client destroy/create not destroy and recreate the whole thing
|
||||||
|
// - Active ws hook optimization: only update when moving to next group
|
||||||
|
//
|
||||||
|
const { Gdk, Gtk } = imports.gi; |
||||||
|
const { Gravity } = imports.gi.Gdk; |
||||||
|
import { SCREEN_HEIGHT, SCREEN_WIDTH } from '../../variables.js'; |
||||||
|
import App from 'resource:///com/github/Aylur/ags/app.js'; |
||||||
|
import Variable from 'resource:///com/github/Aylur/ags/variable.js'; |
||||||
|
import Widget from 'resource:///com/github/Aylur/ags/widget.js'; |
||||||
|
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; |
||||||
|
|
||||||
|
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js'; |
||||||
|
const { execAsync, exec } = Utils; |
||||||
|
import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js'; |
||||||
|
import { dumpToWorkspace, swapWorkspace } from "./actions.js"; |
||||||
|
import { substitute } from "../.miscutils/icons.js"; |
||||||
|
|
||||||
|
const NUM_OF_WORKSPACES_SHOWN = userOptions.overview.numOfCols * userOptions.overview.numOfRows; |
||||||
|
const TARGET = [Gtk.TargetEntry.new('text/plain', Gtk.TargetFlags.SAME_APP, 0)]; |
||||||
|
const POPUP_CLOSE_TIME = 100; // ms
|
||||||
|
|
||||||
|
const overviewTick = Variable(false); |
||||||
|
|
||||||
|
export default () => { |
||||||
|
const clientMap = new Map(); |
||||||
|
let workspaceGroup = 0; |
||||||
|
const ContextMenuWorkspaceArray = ({ label, actionFunc, thisWorkspace }) => Widget.MenuItem({ |
||||||
|
label: `${label}`, |
||||||
|
setup: (menuItem) => { |
||||||
|
let submenu = new Gtk.Menu(); |
||||||
|
submenu.className = 'menu'; |
||||||
|
|
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
const startWorkspace = offset + 1; |
||||||
|
const endWorkspace = startWorkspace + NUM_OF_WORKSPACES_SHOWN - 1; |
||||||
|
for (let i = startWorkspace; i <= endWorkspace; i++) { |
||||||
|
let button = new Gtk.MenuItem({ |
||||||
|
label: `Workspace ${i}` |
||||||
|
}); |
||||||
|
button.connect("activate", () => { |
||||||
|
// execAsync([`${onClickBinary}`, `${thisWorkspace}`, `${i}`]).catch(print);
|
||||||
|
actionFunc(thisWorkspace, i); |
||||||
|
overviewTick.setValue(!overviewTick.value); |
||||||
|
}); |
||||||
|
submenu.append(button); |
||||||
|
} |
||||||
|
menuItem.set_reserve_indicator(true); |
||||||
|
menuItem.set_submenu(submenu); |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const Window = ({ address, at: [x, y], size: [w, h], workspace: { id, name }, class: c, title, xwayland }, screenCoords) => { |
||||||
|
const revealInfoCondition = (Math.min(w, h) * userOptions.overview.scale > 70); |
||||||
|
if (w <= 0 || h <= 0 || (c === '' && title === '') || c.endsWith('-dropterm')) return null; |
||||||
|
// Non-primary monitors
|
||||||
|
if (screenCoords.x != 0) x -= screenCoords.x; |
||||||
|
if (screenCoords.y != 0) y -= screenCoords.y; |
||||||
|
// Other offscreen adjustments
|
||||||
|
if (x + w <= 0) x += (Math.floor(x / SCREEN_WIDTH) * SCREEN_WIDTH); |
||||||
|
else if (x < 0) { w = x + w; x = 0; } |
||||||
|
if (y + h <= 0) x += (Math.floor(y / SCREEN_HEIGHT) * SCREEN_HEIGHT); |
||||||
|
else if (y < 0) { h = y + h; y = 0; } |
||||||
|
// Truncate if offscreen
|
||||||
|
if (x + w > SCREEN_WIDTH) w = SCREEN_WIDTH - x; |
||||||
|
if (y + h > SCREEN_HEIGHT) h = SCREEN_HEIGHT - y; |
||||||
|
|
||||||
|
const appIcon = Widget.Icon({ |
||||||
|
icon: substitute(c), |
||||||
|
size: Math.min(w, h) * userOptions.overview.scale / 2.5, |
||||||
|
}); |
||||||
|
return Widget.Button({ |
||||||
|
attribute: { |
||||||
|
address, x, y, w, h, ws: id, |
||||||
|
updateIconSize: (self) => { |
||||||
|
appIcon.size = Math.min(self.attribute.w, self.attribute.h) * userOptions.overview.scale / 2.5; |
||||||
|
}, |
||||||
|
}, |
||||||
|
className: 'overview-tasks-window', |
||||||
|
hpack: 'start', |
||||||
|
vpack: 'start', |
||||||
|
css: ` |
||||||
|
margin-left: ${Math.round(x * userOptions.overview.scale)}px; |
||||||
|
margin-top: ${Math.round(y * userOptions.overview.scale)}px; |
||||||
|
margin-right: -${Math.round((x + w) * userOptions.overview.scale)}px; |
||||||
|
margin-bottom: -${Math.round((y + h) * userOptions.overview.scale)}px; |
||||||
|
`,
|
||||||
|
onClicked: (self) => { |
||||||
|
App.closeWindow('overview'); |
||||||
|
Utils.timeout(POPUP_CLOSE_TIME, () => Hyprland.messageAsync(`dispatch focuswindow address:${address}`)); |
||||||
|
}, |
||||||
|
onMiddleClickRelease: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`), |
||||||
|
onSecondaryClick: (button) => { |
||||||
|
button.toggleClassName('overview-tasks-window-selected', true); |
||||||
|
const menu = Widget.Menu({ |
||||||
|
className: 'menu', |
||||||
|
children: [ |
||||||
|
Widget.MenuItem({ |
||||||
|
child: Widget.Label({ |
||||||
|
xalign: 0, |
||||||
|
label: "Close (Middle-click)", |
||||||
|
}), |
||||||
|
onActivate: () => Hyprland.messageAsync(`dispatch closewindow address:${address}`), |
||||||
|
}), |
||||||
|
ContextMenuWorkspaceArray({ |
||||||
|
label: "Dump windows to workspace", |
||||||
|
actionFunc: dumpToWorkspace, |
||||||
|
thisWorkspace: Number(id) |
||||||
|
}), |
||||||
|
ContextMenuWorkspaceArray({ |
||||||
|
label: "Swap windows with workspace", |
||||||
|
actionFunc: swapWorkspace, |
||||||
|
thisWorkspace: Number(id) |
||||||
|
}), |
||||||
|
], |
||||||
|
}); |
||||||
|
menu.connect("deactivate", () => { |
||||||
|
button.toggleClassName('overview-tasks-window-selected', false); |
||||||
|
}) |
||||||
|
menu.connect("selection-done", () => { |
||||||
|
button.toggleClassName('overview-tasks-window-selected', false); |
||||||
|
}) |
||||||
|
menu.popup_at_widget(button.get_parent(), Gravity.SOUTH, Gravity.NORTH, null); // Show menu below the button
|
||||||
|
button.connect("destroy", () => menu.destroy()); |
||||||
|
}, |
||||||
|
child: Widget.Box({ |
||||||
|
homogeneous: true, |
||||||
|
child: Widget.Box({ |
||||||
|
vertical: true, |
||||||
|
vpack: 'center', |
||||||
|
className: 'spacing-v-5', |
||||||
|
children: [ |
||||||
|
appIcon, |
||||||
|
// TODO: Add xwayland tag instead of just having italics
|
||||||
|
Widget.Revealer({ |
||||||
|
transition: 'slide_down', |
||||||
|
revealChild: revealInfoCondition, |
||||||
|
child: Widget.Label({ |
||||||
|
maxWidthChars: 10, // Doesn't matter what number
|
||||||
|
truncate: 'end', |
||||||
|
className: `${xwayland ? 'txt txt-italic' : 'txt'}`, |
||||||
|
css: ` |
||||||
|
font-size: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale / 14.6}px; |
||||||
|
margin: 0px ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale / 10}px; |
||||||
|
`,
|
||||||
|
// If the title is too short, include the class
|
||||||
|
label: (title.length <= 1 ? `${c}: ${title}` : title), |
||||||
|
}) |
||||||
|
}) |
||||||
|
] |
||||||
|
}) |
||||||
|
}), |
||||||
|
tooltipText: `${c}: ${title}`, |
||||||
|
setup: (button) => { |
||||||
|
setupCursorHoverGrab(button); |
||||||
|
|
||||||
|
button.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.MOVE); |
||||||
|
button.drag_source_set_icon_name(substitute(c)); |
||||||
|
// button.drag_source_set_icon_gicon(icon);
|
||||||
|
|
||||||
|
button.connect('drag-begin', (button) => { // On drag start, add the dragging class
|
||||||
|
button.toggleClassName('overview-tasks-window-dragging', true); |
||||||
|
}); |
||||||
|
button.connect('drag-data-get', (_w, _c, data) => { // On drag finish, give address
|
||||||
|
data.set_text(address, address.length); |
||||||
|
button.toggleClassName('overview-tasks-window-dragging', false); |
||||||
|
}); |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
const Workspace = (index) => { |
||||||
|
// const fixed = Widget.Fixed({
|
||||||
|
// attribute: {
|
||||||
|
// put: (widget, x, y) => {
|
||||||
|
// fixed.put(widget, x, y);
|
||||||
|
// },
|
||||||
|
// move: (widget, x, y) => {
|
||||||
|
// fixed.move(widget, x, y);
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
const fixed = Widget.Box({ |
||||||
|
attribute: { |
||||||
|
put: (widget, x, y) => { |
||||||
|
if (!widget.attribute) return; |
||||||
|
// Note: x and y are already multiplied by userOptions.overview.scale
|
||||||
|
const newCss = ` |
||||||
|
margin-left: ${Math.round(x)}px; |
||||||
|
margin-top: ${Math.round(y)}px; |
||||||
|
margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px; |
||||||
|
margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px; |
||||||
|
`;
|
||||||
|
widget.css = newCss; |
||||||
|
fixed.pack_start(widget, false, false, 0); |
||||||
|
}, |
||||||
|
move: (widget, x, y) => { |
||||||
|
if (!widget) return; |
||||||
|
if (!widget.attribute) return; |
||||||
|
// Note: x and y are already multiplied by userOptions.overview.scale
|
||||||
|
const newCss = ` |
||||||
|
margin-left: ${Math.round(x)}px; |
||||||
|
margin-top: ${Math.round(y)}px; |
||||||
|
margin-right: -${Math.round(x + (widget.attribute.w * userOptions.overview.scale))}px; |
||||||
|
margin-bottom: -${Math.round(y + (widget.attribute.h * userOptions.overview.scale))}px; |
||||||
|
`;
|
||||||
|
widget.css = newCss; |
||||||
|
}, |
||||||
|
} |
||||||
|
}) |
||||||
|
const WorkspaceNumber = ({ index, ...rest }) => Widget.Label({ |
||||||
|
className: 'overview-tasks-workspace-number', |
||||||
|
label: `${index}`, |
||||||
|
css: ` |
||||||
|
margin: ${Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * userOptions.overview.scale * userOptions.overview.wsNumMarginScale}px; |
||||||
|
font-size: ${SCREEN_HEIGHT * userOptions.overview.scale * userOptions.overview.wsNumScale}px; |
||||||
|
`,
|
||||||
|
setup: (self) => self.hook(Hyprland.active.workspace, (self) => { |
||||||
|
// Update when going to new ws group
|
||||||
|
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN); |
||||||
|
self.label = `${currentGroup * NUM_OF_WORKSPACES_SHOWN + index}`; |
||||||
|
}), |
||||||
|
...rest, |
||||||
|
}) |
||||||
|
const widget = Widget.Box({ |
||||||
|
className: 'overview-tasks-workspace', |
||||||
|
vpack: 'center', |
||||||
|
css: ` |
||||||
|
min-width: ${SCREEN_WIDTH * userOptions.overview.scale}px; |
||||||
|
min-height: ${SCREEN_HEIGHT * userOptions.overview.scale}px; |
||||||
|
`,
|
||||||
|
children: [Widget.EventBox({ |
||||||
|
hexpand: true, |
||||||
|
vexpand: true, |
||||||
|
onPrimaryClick: () => { |
||||||
|
App.closeWindow('overview'); |
||||||
|
Utils.timeout(POPUP_CLOSE_TIME, () => Hyprland.messageAsync(`dispatch workspace ${index}`)); |
||||||
|
}, |
||||||
|
setup: (eventbox) => { |
||||||
|
eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY); |
||||||
|
eventbox.connect('drag-data-received', (_w, _c, _x, _y, data) => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
Hyprland.messageAsync(`dispatch movetoworkspacesilent ${index + offset},address:${data.get_text()}`) |
||||||
|
overviewTick.setValue(!overviewTick.value); |
||||||
|
}); |
||||||
|
}, |
||||||
|
child: Widget.Overlay({ |
||||||
|
child: Widget.Box({}), |
||||||
|
overlays: [ |
||||||
|
WorkspaceNumber({ index: index, hpack: 'start', vpack: 'start' }), |
||||||
|
fixed |
||||||
|
] |
||||||
|
}), |
||||||
|
})], |
||||||
|
}); |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
fixed.attribute.put(WorkspaceNumber(offset + index), 0, 0); |
||||||
|
widget.clear = () => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
clientMap.forEach((client, address) => { |
||||||
|
if (!client) return; |
||||||
|
if ((client.attribute.ws <= offset || client.attribute.ws > offset + NUM_OF_WORKSPACES_SHOWN) || |
||||||
|
(client.attribute.ws == offset + index)) { |
||||||
|
client.destroy(); |
||||||
|
client = null; |
||||||
|
clientMap.delete(address); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
widget.set = (clientJson, screenCoords) => { |
||||||
|
let c = clientMap.get(clientJson.address); |
||||||
|
if (c) { |
||||||
|
if (c.attribute?.ws !== clientJson.workspace.id) { |
||||||
|
c.destroy(); |
||||||
|
c = null; |
||||||
|
clientMap.delete(clientJson.address); |
||||||
|
} |
||||||
|
else if (c) { |
||||||
|
c.attribute.w = clientJson.size[0]; |
||||||
|
c.attribute.h = clientJson.size[1]; |
||||||
|
c.attribute.updateIconSize(c); |
||||||
|
fixed.attribute.move(c, |
||||||
|
Math.max(0, clientJson.at[0] * userOptions.overview.scale), |
||||||
|
Math.max(0, clientJson.at[1] * userOptions.overview.scale) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
const newWindow = Window(clientJson, screenCoords); |
||||||
|
if (newWindow === null) return; |
||||||
|
// clientMap.set(clientJson.address, newWindow);
|
||||||
|
fixed.attribute.put(newWindow, |
||||||
|
Math.max(0, newWindow.attribute.x * userOptions.overview.scale), |
||||||
|
Math.max(0, newWindow.attribute.y * userOptions.overview.scale) |
||||||
|
); |
||||||
|
clientMap.set(clientJson.address, newWindow); |
||||||
|
}; |
||||||
|
widget.unset = (clientAddress) => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
let c = clientMap.get(clientAddress); |
||||||
|
if (!c) return; |
||||||
|
c.destroy(); |
||||||
|
c = null; |
||||||
|
clientMap.delete(clientAddress); |
||||||
|
}; |
||||||
|
widget.show = () => { |
||||||
|
fixed.show_all(); |
||||||
|
} |
||||||
|
return widget; |
||||||
|
}; |
||||||
|
|
||||||
|
const arr = (s, n) => { |
||||||
|
const array = []; |
||||||
|
for (let i = 0; i < n; i++) |
||||||
|
array.push(s + i); |
||||||
|
|
||||||
|
return array; |
||||||
|
}; |
||||||
|
|
||||||
|
const OverviewRow = ({ startWorkspace, workspaces, windowName = 'overview' }) => Widget.Box({ |
||||||
|
children: arr(startWorkspace, workspaces).map(Workspace), |
||||||
|
attribute: { |
||||||
|
monitorMap: [], |
||||||
|
getMonitorMap: (box) => { |
||||||
|
execAsync('hyprctl -j monitors').then(monitors => { |
||||||
|
box.attribute.monitorMap = JSON.parse(monitors).reduce((acc, item) => { |
||||||
|
acc[item.id] = { x: item.x, y: item.y }; |
||||||
|
return acc; |
||||||
|
}, {}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
update: (box) => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
if (!App.getWindow(windowName).visible) return; |
||||||
|
Hyprland.messageAsync('j/clients').then(clients => { |
||||||
|
const allClients = JSON.parse(clients); |
||||||
|
const kids = box.get_children(); |
||||||
|
kids.forEach(kid => kid.clear()); |
||||||
|
for (let i = 0; i < allClients.length; i++) { |
||||||
|
const client = allClients[i]; |
||||||
|
const childID = client.workspace.id - (offset + startWorkspace); |
||||||
|
if (offset + startWorkspace <= client.workspace.id && |
||||||
|
client.workspace.id <= offset + startWorkspace + workspaces) { |
||||||
|
const screenCoords = box.attribute.monitorMap[client.monitor]; |
||||||
|
if (kids[childID]) { |
||||||
|
kids[childID].set(client, screenCoords); |
||||||
|
} |
||||||
|
continue; |
||||||
|
} |
||||||
|
} |
||||||
|
kids.forEach(kid => kid.show()); |
||||||
|
}).catch(print); |
||||||
|
}, |
||||||
|
updateWorkspace: (box, id) => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
if (!( // Not in range, ignore
|
||||||
|
offset + startWorkspace <= id && |
||||||
|
id <= offset + startWorkspace + workspaces |
||||||
|
)) return; |
||||||
|
// if (!App.getWindow(windowName).visible) return;
|
||||||
|
Hyprland.messageAsync('j/clients').then(clients => { |
||||||
|
const allClients = JSON.parse(clients); |
||||||
|
const kids = box.get_children(); |
||||||
|
for (let i = 0; i < allClients.length; i++) { |
||||||
|
const client = allClients[i]; |
||||||
|
if (client.workspace.id != id) continue; |
||||||
|
const screenCoords = box.attribute.monitorMap[client.monitor]; |
||||||
|
kids[id - (offset + startWorkspace)]?.set(client, screenCoords); |
||||||
|
} |
||||||
|
kids[id - (offset + startWorkspace)]?.show(); |
||||||
|
}).catch(print); |
||||||
|
}, |
||||||
|
}, |
||||||
|
setup: (box) => { |
||||||
|
box.attribute.getMonitorMap(box); |
||||||
|
box |
||||||
|
.hook(overviewTick, (box) => box.attribute.update(box)) |
||||||
|
.hook(Hyprland, (box, clientAddress) => { |
||||||
|
const offset = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN) * NUM_OF_WORKSPACES_SHOWN; |
||||||
|
const kids = box.get_children(); |
||||||
|
const client = Hyprland.getClient(clientAddress); |
||||||
|
if (!client) return; |
||||||
|
const id = client.workspace.id; |
||||||
|
|
||||||
|
box.attribute.updateWorkspace(box, id); |
||||||
|
kids[id - (offset + startWorkspace)]?.unset(clientAddress); |
||||||
|
}, 'client-removed') |
||||||
|
.hook(Hyprland, (box, clientAddress) => { |
||||||
|
const client = Hyprland.getClient(clientAddress); |
||||||
|
if (!client) return; |
||||||
|
box.attribute.updateWorkspace(box, client.workspace.id); |
||||||
|
}, 'client-added') |
||||||
|
.hook(Hyprland.active.workspace, (box) => { |
||||||
|
// Full update when going to new ws group
|
||||||
|
const previousGroup = box.attribute.workspaceGroup; |
||||||
|
const currentGroup = Math.floor((Hyprland.active.workspace.id - 1) / NUM_OF_WORKSPACES_SHOWN); |
||||||
|
if (currentGroup !== previousGroup) { |
||||||
|
box.attribute.update(box); |
||||||
|
box.attribute.workspaceGroup = currentGroup; |
||||||
|
} |
||||||
|
}) |
||||||
|
.hook(App, (box, name, visible) => { // Update on open
|
||||||
|
if (name == 'overview' && visible) box.attribute.update(box); |
||||||
|
}) |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return Widget.Revealer({ |
||||||
|
revealChild: true, |
||||||
|
transition: 'slide_down', |
||||||
|
transitionDuration: userOptions.animations.durationLarge, |
||||||
|
child: Widget.Box({ |
||||||
|
vertical: true, |
||||||
|
className: 'overview-tasks', |
||||||
|
children: Array.from({ length: userOptions.overview.numOfRows }, (_, index) => |
||||||
|
OverviewRow({ |
||||||
|
startWorkspace: 1 + index * userOptions.overview.numOfCols, |
||||||
|
workspaces: userOptions.overview.numOfCols, |
||||||
|
}) |
||||||
|
) |
||||||
|
}), |
||||||
|
}); |
||||||
|
} |
||||||
@ -0,0 +1,65 @@ |
|||||||
|
import Widget from 'resource:///com/github/Aylur/ags/widget.js'; |
||||||
|
|
||||||
|
export const searchItem = ({ materialIconName, name, actionName, content, onActivate, extraClassName = '', ...rest }) => { |
||||||
|
const actionText = Widget.Revealer({ |
||||||
|
revealChild: false, |
||||||
|
transition: "crossfade", |
||||||
|
transitionDuration: userOptions.animations.durationLarge, |
||||||
|
child: Widget.Label({ |
||||||
|
className: 'overview-search-results-txt txt txt-small txt-action', |
||||||
|
label: `${actionName}`, |
||||||
|
}) |
||||||
|
}); |
||||||
|
const actionTextRevealer = Widget.Revealer({ |
||||||
|
revealChild: false, |
||||||
|
transition: "slide_left", |
||||||
|
transitionDuration: userOptions.animations.durationSmall, |
||||||
|
child: actionText, |
||||||
|
}) |
||||||
|
return Widget.Button({ |
||||||
|
className: `overview-search-result-btn txt ${extraClassName}`, |
||||||
|
onClicked: onActivate, |
||||||
|
child: Widget.Box({ |
||||||
|
children: [ |
||||||
|
Widget.Box({ |
||||||
|
vertical: false, |
||||||
|
children: [ |
||||||
|
Widget.Label({ |
||||||
|
className: `icon-material overview-search-results-icon`, |
||||||
|
label: `${materialIconName}`, |
||||||
|
}), |
||||||
|
Widget.Box({ |
||||||
|
vertical: true, |
||||||
|
children: [ |
||||||
|
Widget.Label({ |
||||||
|
hpack: 'start', |
||||||
|
className: 'overview-search-results-txt txt-smallie txt-subtext', |
||||||
|
label: `${name}`, |
||||||
|
truncate: "end", |
||||||
|
}), |
||||||
|
Widget.Label({ |
||||||
|
hpack: 'start', |
||||||
|
className: 'overview-search-results-txt txt-norm', |
||||||
|
label: `${content}`, |
||||||
|
truncate: "end", |
||||||
|
}), |
||||||
|
] |
||||||
|
}), |
||||||
|
Widget.Box({ hexpand: true }), |
||||||
|
actionTextRevealer, |
||||||
|
], |
||||||
|
}) |
||||||
|
] |
||||||
|
}), |
||||||
|
setup: (self) => self |
||||||
|
.on('focus-in-event', (button) => { |
||||||
|
actionText.revealChild = true; |
||||||
|
actionTextRevealer.revealChild = true; |
||||||
|
}) |
||||||
|
.on('focus-out-event', (button) => { |
||||||
|
actionText.revealChild = false; |
||||||
|
actionTextRevealer.revealChild = false; |
||||||
|
}) |
||||||
|
, |
||||||
|
}); |
||||||
|
} |
||||||
@ -0,0 +1,197 @@ |
|||||||
|
*:not(popover) { |
||||||
|
all: unset; |
||||||
|
} |
||||||
|
|
||||||
|
@import '../../../.cache/wal/colors-waybar.css'; |
||||||
|
/* @import '../../../.cache/wal/colors-waybar-rgba.css'; */ |
||||||
|
|
||||||
|
/* define some colors */ |
||||||
|
@define-color border-color @color2; |
||||||
|
@define-color border-color-alt @color7; |
||||||
|
@define-color noti-bg rgba(0, 0, 0, 0.8); |
||||||
|
@define-color noti-bg-alt #111111; |
||||||
|
|
||||||
|
widget { |
||||||
|
border-radius: 0.818rem; |
||||||
|
-gtk-outline-radius: 0.818rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-window { |
||||||
|
margin-top: 2.727rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-box { |
||||||
|
transition: 300ms cubic-bezier(0, 0.55, 0.45, 1); |
||||||
|
border-radius: 1.705rem; |
||||||
|
-gtk-outline-radius: 1.705rem; |
||||||
|
border-top: 1px solid @border-color; |
||||||
|
border-left: 1px solid @border-color-alt; |
||||||
|
border-right: 1px solid @border-color-alt; |
||||||
|
border-bottom: 1px solid @border-color; |
||||||
|
/* box-shadow: 0px 2px 3px alpha(@color0, 0.45); */ |
||||||
|
margin: 0.476rem; |
||||||
|
min-width: 13.636rem; |
||||||
|
min-height: 3.409rem; |
||||||
|
padding: 0rem 1.364rem; |
||||||
|
padding-right: 2.864rem; |
||||||
|
background-color: @noti-bg; |
||||||
|
color: @color2; |
||||||
|
caret-color: transparent; |
||||||
|
font-weight: bolder; |
||||||
|
} |
||||||
|
.overview-search-box selection { |
||||||
|
background-color: @noti-bg; |
||||||
|
color: @border-color; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-box-extended { |
||||||
|
min-width: 25.909rem; |
||||||
|
caret-color: #FDD9FD; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-prompt { |
||||||
|
color: @color2; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-icon { |
||||||
|
margin: 0rem 1.023rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-prompt-box { |
||||||
|
margin-left: -18.545rem; |
||||||
|
margin-right: 0.544rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-icon-box { |
||||||
|
margin-left: -18.545rem; |
||||||
|
margin-right: 0.544rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-results { |
||||||
|
border-radius: 1.705rem; |
||||||
|
-gtk-outline-radius: 1.705rem; |
||||||
|
border-top: 1px solid @border-color; |
||||||
|
border-left: 1px solid @border-color-alt; |
||||||
|
border-right: 1px solid @border-color-alt; |
||||||
|
border-bottom: 1px solid @border-color; |
||||||
|
box-shadow: 0px 2px 3px @color9; |
||||||
|
margin: 0.476rem; |
||||||
|
min-width: 28.773rem; |
||||||
|
padding: 0.682rem; |
||||||
|
background-color: @noti-bg; |
||||||
|
color: @border-color; |
||||||
|
font-weight: bold; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-results-icon { |
||||||
|
margin: 0rem 0.682rem; |
||||||
|
font-size: 2.386rem; |
||||||
|
min-width: 2.386rem; |
||||||
|
min-height: 2.386rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-results-txt { |
||||||
|
margin-right: 0.682rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-results-txt-cmd { |
||||||
|
margin-right: 0.682rem; |
||||||
|
font-family: "JetBrains Mono NF", "JetBrains Mono Nerd Font", "JetBrains Mono NL", "SpaceMono NF", "SpaceMono Nerd Font", monospace; |
||||||
|
font-size: 1.227rem; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-result-btn { |
||||||
|
border-radius: 1.159rem; |
||||||
|
-gtk-outline-radius: 1.159rem; |
||||||
|
padding: 0.341rem; |
||||||
|
min-width: 2.386rem; |
||||||
|
min-height: 2.386rem; |
||||||
|
caret-color: transparent; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-result-btn:hover, |
||||||
|
.overview-search-result-btn:focus { |
||||||
|
background-color: alpha(@color7, 0.9); |
||||||
|
color: alpha(@color0, 0.7); |
||||||
|
} |
||||||
|
|
||||||
|
.overview-search-result-btn:active { |
||||||
|
background-color: alpha(@color7, 0.9); |
||||||
|
color: @color4; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks { |
||||||
|
border-radius: 1.705rem; |
||||||
|
-gtk-outline-radius: 1.705rem; |
||||||
|
border-top: 1px solid @border-color; |
||||||
|
border-left: 1px solid @border-color-alt; |
||||||
|
border-right: 1px solid @border-color-alt; |
||||||
|
border-bottom: 1px solid @border-color; |
||||||
|
box-shadow: 0px 2px 3px @color5; |
||||||
|
margin: 0.476rem; |
||||||
|
padding: 0.341rem; |
||||||
|
/* background-color: rgba(49, 50, 68, 0.8); */ |
||||||
|
background-color: @noti-bg; |
||||||
|
color: #EBDFED; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-workspace { |
||||||
|
border-radius: 1.159rem; |
||||||
|
-gtk-outline-radius: 1.159rem; |
||||||
|
margin: 0.341rem; |
||||||
|
/* background-color: #26233A; */ |
||||||
|
background-image: url('../../rofi/.current_wallpaper'); |
||||||
|
background-size: cover; |
||||||
|
background-position: center; |
||||||
|
border: 0.068rem solid alpha(@color4, 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-workspace-number { |
||||||
|
font-family: "Open Sans", "Noto Sans", sans-serif; |
||||||
|
color: #CFC2D3; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-window { |
||||||
|
border-radius: 1.159rem; |
||||||
|
-gtk-outline-radius: 1.159rem; |
||||||
|
transition: 300ms cubic-bezier(0.1, 1, 0, 1); |
||||||
|
background-color: alpha(@color3, .7); |
||||||
|
/* background-color: @color_a3; */ |
||||||
|
/* background-color: rgba(46, 40, 50, 0.8); */ |
||||||
|
color: #EBDFED; |
||||||
|
border: 0.068rem solid @color7; |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-window:hover, |
||||||
|
.overview-tasks-window:focus { |
||||||
|
background-color: alpha(@color9, 0.8); |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-window:active { |
||||||
|
background-color: alpha(@color9, 0.8); |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-window-selected { |
||||||
|
background-color: alpha(@color9, 0.8); |
||||||
|
} |
||||||
|
|
||||||
|
.overview-tasks-window-dragging { |
||||||
|
opacity: 0.2; |
||||||
|
} |
||||||
|
|
||||||
|
.growingRadial { |
||||||
|
transition: 300ms cubic-bezier(0.2, 0, 0, 1); |
||||||
|
} |
||||||
|
|
||||||
|
.fadingRadial { |
||||||
|
transition: 50ms cubic-bezier(0.2, 0, 0, 1); |
||||||
|
} |
||||||
|
|
||||||
|
.sidebar-pinned { |
||||||
|
margin: 0rem; |
||||||
|
border-radius: 0rem; |
||||||
|
border-bottom-right-radius: 1.705rem; |
||||||
|
border: 0rem solid; |
||||||
|
} |
||||||
|
|
||||||
|
/*# sourceMappingURL=style.css.map */ |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
|
||||||
|
const userConfigOptions = { |
||||||
|
// For every option, see ~/.config/ags/modules/.configuration/user_options.js
|
||||||
|
// (vscode users ctrl+click this: file://./modules/.configuration/user_options.js)
|
||||||
|
// (vim users: `:vsp` to split window, move cursor to this path, press `gf`. `Ctrl-w` twice to switch between)
|
||||||
|
// options listed in this file will override the default ones in the above file
|
||||||
|
// Here's an example
|
||||||
|
'overview':{ |
||||||
|
'scale': 0.15, |
||||||
|
'numOfRows': 2 |
||||||
|
}, |
||||||
|
'keybinds': { |
||||||
|
'sidebar': { |
||||||
|
'pin': "Ctrl+p", |
||||||
|
'nextTab': "Ctrl+Page_Down", |
||||||
|
'prevTab': "Ctrl+Page_Up", |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
export default userConfigOptions; |
||||||
@ -0,0 +1,21 @@ |
|||||||
|
const { Gtk } = imports.gi; |
||||||
|
import Variable from 'resource:///com/github/Aylur/ags/variable.js'; |
||||||
|
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; |
||||||
|
const { exec, execAsync } = Utils; |
||||||
|
|
||||||
|
Gtk.IconTheme.get_default().append_search_path(`${App.configDir}/assets/icons`); |
||||||
|
|
||||||
|
// Screen size
|
||||||
|
export const SCREEN_WIDTH = Number(exec(`bash -c "xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f1 | head -1" | awk '{print $1}'`)); |
||||||
|
export const SCREEN_HEIGHT = Number(exec(`bash -c "xrandr --current | grep '*' | uniq | awk '{print $1}' | cut -d 'x' -f2 | head -1" | awk '{print $1}'`)); |
||||||
|
|
||||||
|
// Mode switching
|
||||||
|
export const currentShellMode = Variable('normal', {}) // normal, focus
|
||||||
|
globalThis['currentMode'] = currentShellMode; |
||||||
|
globalThis['cycleMode'] = () => { |
||||||
|
if (currentShellMode.value === 'normal') { |
||||||
|
currentShellMode.value = 'focus'; |
||||||
|
} else { |
||||||
|
currentShellMode.value = 'normal'; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue