parent
5d27b3d975
commit
f3e06554e4
105 changed files with 245 additions and 254 deletions
nixosModules/ags/v1
78
nixosModules/ags/v1/config/global-types.d.ts
vendored
Normal file
78
nixosModules/ags/v1/config/global-types.d.ts
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
// For ./ts/bar/hovers/keyboard.ts
|
||||
export interface Keyboard {
|
||||
address: string
|
||||
name: string
|
||||
rules: string
|
||||
model: string
|
||||
layout: string
|
||||
variant: string
|
||||
options: string
|
||||
active_keymap: string
|
||||
main: boolean
|
||||
}
|
||||
|
||||
// For ./ts/media-player/gesture.ts
|
||||
export interface Gesture {
|
||||
attribute?: object
|
||||
setup?(self: OverlayGeneric): void
|
||||
props?: OverlayProps<unknown & Widget, unknown>
|
||||
}
|
||||
|
||||
// For ./ts/media-player/mpris.ts
|
||||
type PlayerDragProps = unknown & { dragging: boolean };
|
||||
export type PlayerDrag = AgsCenterBox<
|
||||
unknown & Widget, unknown & Widget, unknown & Widget, unknown & PlayerDragProps
|
||||
>;
|
||||
interface Colors {
|
||||
imageAccent: string
|
||||
buttonAccent: string
|
||||
buttonText: string
|
||||
hoverAccent: string
|
||||
}
|
||||
|
||||
// For ./ts/media-player
|
||||
export interface PlayerBoxProps {
|
||||
bgStyle: string
|
||||
player: MprisPlayer
|
||||
}
|
||||
export type PlayerBox = AgsCenterBox<
|
||||
unknown & Widget, unknown & Widget, unknown & Widget, PlayerBoxProps
|
||||
>;
|
||||
export type PlayerOverlay = AgsOverlay<AgsWidget, {
|
||||
players: Map
|
||||
setup: boolean
|
||||
dragging: boolean
|
||||
includesWidget(playerW: PlayerBox): PlayerBox
|
||||
showTopOnly(): void
|
||||
moveToTop(player: PlayerBox): void
|
||||
}>;
|
||||
export interface PlayerButtonType {
|
||||
player: MprisPlayer
|
||||
colors: Var<Colors>
|
||||
children: StackProps['children']
|
||||
onClick: string
|
||||
prop: string
|
||||
}
|
||||
|
||||
// For ./ts/on-screen-keyboard
|
||||
export type OskWindow = Window<BoxGeneric, {
|
||||
startY: null | number
|
||||
setVisible: (state: boolean) => void
|
||||
killGestureSigs: () => void
|
||||
setSlideUp: () => void
|
||||
setSlideDown: () => void
|
||||
}>;
|
||||
|
||||
// For ./ts/quick-settings
|
||||
import { BluetoothDevice as BTDev } from 'types/service/bluetooth.ts';
|
||||
export interface APType {
|
||||
bssid: string
|
||||
address: string
|
||||
lastSeen: number
|
||||
ssid: string
|
||||
active: boolean
|
||||
strength: number
|
||||
iconName: string
|
||||
}
|
||||
export type APBox = AgsBox<unknown & Widget, { ap: Var<APType> }>;
|
||||
export type DeviceBox = AgsBox<unknown & Widget, { dev: BTDev }>;
|
99
nixosModules/ags/v1/config/scss/osk.scss
Normal file
99
nixosModules/ags/v1/config/scss/osk.scss
Normal file
|
@ -0,0 +1,99 @@
|
|||
.thingy {
|
||||
border-radius: 2rem 2rem 0 0;
|
||||
min-height: 2.7rem;
|
||||
min-width: 20rem;
|
||||
|
||||
.settings {
|
||||
padding: 0.5rem;
|
||||
|
||||
.button {
|
||||
background-color: $bgfull;
|
||||
border: 0.1rem solid $darkbg;
|
||||
border-radius: 0.7rem;
|
||||
padding: 0.3rem;
|
||||
|
||||
&.toggled {
|
||||
background-color: $contrast-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.osk {
|
||||
padding-top: 4px;
|
||||
border-radius: 10px 10px 0;
|
||||
|
||||
.side {
|
||||
.key {
|
||||
&:active label {
|
||||
background-color: $contrast-bg;
|
||||
}
|
||||
|
||||
label {
|
||||
background-color: $bg;
|
||||
border: 0.08rem solid $darkbg;
|
||||
border-radius: 0.7rem;
|
||||
min-height: 3rem;
|
||||
|
||||
transition: background-color 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out;
|
||||
|
||||
&.normal,
|
||||
&.Super {
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
&.Tab,
|
||||
&.Backspace {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
&.Enter,
|
||||
&.Caps {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
&.Shift {
|
||||
min-width: 9rem;
|
||||
}
|
||||
|
||||
&.Space {
|
||||
min-width: 20rem;
|
||||
}
|
||||
|
||||
&.PrtSc,
|
||||
&.AltGr {
|
||||
min-width: 3.2rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $darkbg;
|
||||
}
|
||||
|
||||
&.altgr {
|
||||
border: 0.08rem solid blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.right-side {
|
||||
.key .mod {
|
||||
&.Ctrl {
|
||||
min-width: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.left-side {
|
||||
.key .mod {
|
||||
&.Alt {
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
&.Ctrl {
|
||||
min-width: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
nixosModules/ags/v1/config/scss/player.scss
Normal file
116
nixosModules/ags/v1/config/scss/player.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
.arrow {
|
||||
transition: -gtk-icon-transform 0.3s ease-in-out;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.media {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.player {
|
||||
all: unset;
|
||||
padding: 10px;
|
||||
min-width: 400px;
|
||||
min-height: 200px;
|
||||
border-radius: 30px;
|
||||
border-top: 2px solid $contrast-bg;
|
||||
border-bottom: 2px solid $contrast-bg;
|
||||
transition: background 250ms;
|
||||
|
||||
.top {
|
||||
font-size: 23px;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
.title {
|
||||
font-weight: 500;
|
||||
transition: text 250ms;
|
||||
}
|
||||
|
||||
.artist {
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
transition: text 250ms;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.pausebutton {
|
||||
transition: background-color ease .2s,
|
||||
color ease .2s;
|
||||
font-size: 15px;
|
||||
padding: 4px 4px 4px 7px;
|
||||
}
|
||||
|
||||
.playing {
|
||||
transition: background-color ease .2s,
|
||||
color ease .2s;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.stopped,
|
||||
.paused {
|
||||
transition: background-color ease .2s,
|
||||
color ease .2s;
|
||||
border-radius: 26px;
|
||||
padding: 4px 4px 4px 10px;
|
||||
}
|
||||
|
||||
button label {
|
||||
min-width: 35px;
|
||||
}
|
||||
}
|
||||
|
||||
.position-indicator {
|
||||
min-width: 18px;
|
||||
margin: 7px;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 0 5px 0 rgba(255, 255, 255, 0.3);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.previous,
|
||||
.next,
|
||||
.shuffle,
|
||||
.loop {
|
||||
border-radius: 100%;
|
||||
transition: color 200ms;
|
||||
|
||||
&:hover {
|
||||
border-radius: 100%;
|
||||
background-color: rgba(127, 132, 156, 0.4);
|
||||
transition: color 200ms;
|
||||
}
|
||||
}
|
||||
|
||||
.loop {
|
||||
label {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.position-slider {
|
||||
highlight {
|
||||
margin: 0;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
trough {
|
||||
margin: 0 8px;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
slider {
|
||||
margin: -8px;
|
||||
min-height: 20px;
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
slider:hover {
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
}
|
184
nixosModules/ags/v1/config/scss/quick-settings.scss
Normal file
184
nixosModules/ags/v1/config/scss/quick-settings.scss
Normal file
|
@ -0,0 +1,184 @@
|
|||
.quick-settings {
|
||||
font-size: 30px;
|
||||
min-width: 500px;
|
||||
padding: 0;
|
||||
background-color: $bg;
|
||||
border-radius: 30px 0 30px 30px;
|
||||
border: 2px solid $contrast-bg;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
font-size: 30px;
|
||||
margin-left: 15px;
|
||||
margin-right: 10px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.scrolled-indicator {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
border: 1.5px solid $contrast-bg;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
|
||||
scrolledwindow {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
row {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
margin: 5px;
|
||||
|
||||
label {
|
||||
font-size: 16px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
image {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-label {
|
||||
font-size: 14px;
|
||||
padding: 3px;
|
||||
border: 2px solid $contrast-bg;
|
||||
border-radius: 10px 20px 20px 10px;
|
||||
min-width: 106px;
|
||||
background: #1b1b1b;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.grid-chev {
|
||||
margin-left: 10px;
|
||||
margin-right: 12px;
|
||||
font-size: 25px;
|
||||
|
||||
transition: -gtk-icon-transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
font-size: 10px;
|
||||
min-width: 440px;
|
||||
background-color: $bgfull;
|
||||
border-top: 2px solid $contrast-bg;
|
||||
border-bottom: 2px solid $contrast-bg;
|
||||
border-radius: 15px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.grid-button {
|
||||
min-height: 65px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.left-part {
|
||||
background: #1b1b1b;
|
||||
border-top-left-radius: 15px;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-left: 2px solid $contrast-bg;
|
||||
border-top: 2px solid $contrast-bg;
|
||||
border-bottom: 2px solid $contrast-bg;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.right-part {
|
||||
background: #1b1b1b;
|
||||
border-top-right-radius: 30px;
|
||||
border-bottom-right-radius: 30px;
|
||||
border-right: 2px solid $contrast-bg;
|
||||
border-top: 2px solid $contrast-bg;
|
||||
border-bottom: 2px solid $contrast-bg;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.right-part:hover,
|
||||
.right-part:active {
|
||||
color: $contrast-bg;
|
||||
border: 2px solid $contrast-bg;
|
||||
border-top-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.left-part:hover,
|
||||
.left-part:active {
|
||||
color: $contrast-bg;
|
||||
border: 2px solid $contrast-bg;
|
||||
border-top-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.player {
|
||||
margin-top: 6px;
|
||||
min-height: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.slider-box {
|
||||
min-height: 100px;
|
||||
min-width: 470px;
|
||||
background-color: $bgfull;
|
||||
border-top: 2px solid $contrast-bg;
|
||||
border-bottom: 2px solid $contrast-bg;
|
||||
border-radius: 15px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.slider-label {
|
||||
font-size: 30px;
|
||||
min-width: 40px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
min-height: 55px;
|
||||
margin-right: -15px;
|
||||
|
||||
scale {
|
||||
min-width: 400px;
|
||||
margin-left: 18px;
|
||||
margin-right: 20px;
|
||||
|
||||
highlight {
|
||||
margin: 0;
|
||||
background-color: #79659f;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
trough {
|
||||
background-color: #363847;
|
||||
border-radius: 2em;
|
||||
}
|
||||
|
||||
slider {
|
||||
margin: -4px;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
background: #3e4153;
|
||||
border-radius: 100%;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
slider:hover {
|
||||
background-color: #303240;
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
172
nixosModules/ags/v1/config/services/tablet.ts
Normal file
172
nixosModules/ags/v1/config/services/tablet.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
const Hyprland = await Service.import('hyprland');
|
||||
const { execAsync, subprocess } = Utils;
|
||||
|
||||
import TouchGestures from './touch-gestures.ts';
|
||||
|
||||
const ROTATION_MAP = {
|
||||
'normal': 0,
|
||||
'right-up': 3,
|
||||
'bottom-up': 2,
|
||||
'left-up': 1,
|
||||
};
|
||||
const SCREEN = 'desc:BOE 0x0964';
|
||||
const DEVICES = [
|
||||
'wacom-hid-52eb-finger',
|
||||
'wacom-hid-52eb-pen',
|
||||
];
|
||||
|
||||
// Types
|
||||
import { Subprocess } from 'types/@girs/gio-2.0/gio-2.0.cjs';
|
||||
|
||||
|
||||
class Tablet extends Service {
|
||||
static {
|
||||
Service.register(this, {
|
||||
'device-fetched': ['boolean'],
|
||||
'autorotate-started': ['boolean'],
|
||||
'autorotate-destroyed': ['boolean'],
|
||||
'autorotate-toggled': ['boolean'],
|
||||
'inputs-blocked': ['boolean'],
|
||||
'inputs-unblocked': ['boolean'],
|
||||
'laptop-mode': ['boolean'],
|
||||
'tablet-mode': ['boolean'],
|
||||
'mode-toggled': ['boolean'],
|
||||
'osk-toggled': ['boolean'],
|
||||
});
|
||||
}
|
||||
|
||||
#tabletMode = false;
|
||||
#oskState = false;
|
||||
#autorotate = null as Subprocess | null;
|
||||
#blockedInputs = null as Subprocess | null;
|
||||
|
||||
get tabletMode() {
|
||||
return this.#tabletMode;
|
||||
}
|
||||
|
||||
get autorotateState() {
|
||||
return this.#autorotate !== null;
|
||||
}
|
||||
|
||||
get oskState() {
|
||||
return this.#oskState;
|
||||
}
|
||||
|
||||
set oskState(value: boolean) {
|
||||
this.#oskState = value;
|
||||
this.emit('osk-toggled', this.#oskState);
|
||||
}
|
||||
|
||||
#blockInputs() {
|
||||
if (this.#blockedInputs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#blockedInputs = subprocess(['libinput', 'debug-events', '--grab',
|
||||
'--device', '/dev/input/by-path/platform-i8042-serio-0-event-kbd',
|
||||
'--device', '/dev/input/by-path/platform-i8042-serio-1-event-mouse',
|
||||
'--device', '/dev/input/by-path/platform-AMDI0010:02-event-mouse',
|
||||
'--device', '/dev/input/by-path/platform-thinkpad_acpi-event',
|
||||
'--device', '/dev/video-bus'],
|
||||
() => { /**/ });
|
||||
this.emit('inputs-blocked', true);
|
||||
}
|
||||
|
||||
#unblockInputs() {
|
||||
if (this.#blockedInputs) {
|
||||
this.#blockedInputs.force_exit();
|
||||
this.#blockedInputs = null;
|
||||
this.emit('inputs-unblocked', true);
|
||||
}
|
||||
}
|
||||
|
||||
setTabletMode() {
|
||||
execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '0'])
|
||||
.catch(print);
|
||||
|
||||
this.startAutorotate();
|
||||
this.#blockInputs();
|
||||
|
||||
this.#tabletMode = true;
|
||||
this.emit('tablet-mode', true);
|
||||
this.emit('mode-toggled', true);
|
||||
}
|
||||
|
||||
setLaptopMode() {
|
||||
execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '2'])
|
||||
.catch(print);
|
||||
|
||||
this.killAutorotate();
|
||||
this.#unblockInputs();
|
||||
|
||||
this.#tabletMode = false;
|
||||
this.emit('laptop-mode', true);
|
||||
this.emit('mode-toggled', true);
|
||||
}
|
||||
|
||||
toggleMode() {
|
||||
if (this.#tabletMode) {
|
||||
this.setLaptopMode();
|
||||
}
|
||||
else {
|
||||
this.setTabletMode();
|
||||
}
|
||||
|
||||
this.emit('mode-toggled', true);
|
||||
}
|
||||
|
||||
startAutorotate() {
|
||||
if (this.#autorotate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#autorotate = subprocess(
|
||||
['monitor-sensor'],
|
||||
(output) => {
|
||||
if (output.includes('orientation changed')) {
|
||||
const index = output.split(' ').at(-1);
|
||||
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orientation = ROTATION_MAP[index];
|
||||
|
||||
Hyprland.messageAsync(
|
||||
`keyword monitor ${SCREEN},transform,${orientation}`,
|
||||
).catch(print);
|
||||
|
||||
const batchRotate = DEVICES.map((dev) =>
|
||||
`keyword device:${dev}:transform ${orientation}; `);
|
||||
|
||||
Hyprland.messageAsync(`[[BATCH]] ${batchRotate.flat()}`);
|
||||
|
||||
if (TouchGestures.gestureDaemon) {
|
||||
TouchGestures.killDaemon();
|
||||
TouchGestures.startDaemon();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
this.emit('autorotate-started', true);
|
||||
this.emit('autorotate-toggled', true);
|
||||
}
|
||||
|
||||
killAutorotate() {
|
||||
if (this.#autorotate) {
|
||||
this.#autorotate.force_exit();
|
||||
this.#autorotate = null;
|
||||
this.emit('autorotate-destroyed', true);
|
||||
this.emit('autorotate-toggled', false);
|
||||
}
|
||||
}
|
||||
|
||||
toggleOsk() {
|
||||
this.#oskState = !this.#oskState;
|
||||
this.emit('osk-toggled', this.#oskState);
|
||||
}
|
||||
}
|
||||
|
||||
const tabletService = new Tablet();
|
||||
|
||||
export default tabletService;
|
26
nixosModules/ags/v1/config/ts/bar/heart.ts
Normal file
26
nixosModules/ags/v1/config/ts/bar/heart.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
const { Label } = Widget;
|
||||
|
||||
import CursorBox from '../../misc/cursorbox.ts';
|
||||
import Persist from '../../misc/persist.ts';
|
||||
|
||||
const HeartState = Variable('');
|
||||
|
||||
Persist({
|
||||
name: 'heart',
|
||||
gobject: HeartState,
|
||||
prop: 'value',
|
||||
condition: '',
|
||||
whenFalse: '',
|
||||
});
|
||||
|
||||
|
||||
export default () => CursorBox({
|
||||
on_primary_click_release: () => {
|
||||
HeartState.setValue(HeartState.value === '' ? '' : '');
|
||||
},
|
||||
|
||||
child: Label({
|
||||
class_name: 'heart-toggle',
|
||||
label: HeartState.bind(),
|
||||
}),
|
||||
});
|
54
nixosModules/ags/v1/config/ts/bar/keyboard-layout.ts
Normal file
54
nixosModules/ags/v1/config/ts/bar/keyboard-layout.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
const Hyprland = await Service.import('hyprland');
|
||||
const { Icon, Label } = Widget;
|
||||
|
||||
import HoverRevealer from './hover-revealer.ts';
|
||||
|
||||
const DEFAULT_KB = 'at-translated-set-2-keyboard';
|
||||
|
||||
// Types
|
||||
import { Keyboard, LabelGeneric } from 'global-types';
|
||||
|
||||
|
||||
const getKbdLayout = (self: LabelGeneric, _: string, layout: string) => {
|
||||
if (layout) {
|
||||
if (layout === 'error') {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortName = layout.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
// At launch, kb layout is undefined
|
||||
Hyprland.messageAsync('j/devices').then((obj) => {
|
||||
const keyboards = Array.from(JSON.parse(obj)
|
||||
.keyboards) as Keyboard[];
|
||||
const kb = keyboards.find((v) => v.name === DEFAULT_KB);
|
||||
|
||||
if (kb) {
|
||||
layout = kb.active_keymap;
|
||||
|
||||
const shortName = layout
|
||||
.match(/\(([A-Za-z]+)\)/);
|
||||
|
||||
self.label = shortName ? shortName[1] : layout;
|
||||
}
|
||||
else {
|
||||
self.label = 'None';
|
||||
}
|
||||
}).catch(print);
|
||||
}
|
||||
};
|
||||
|
||||
export default () => HoverRevealer({
|
||||
class_name: 'keyboard',
|
||||
spacing: 4,
|
||||
|
||||
icon: Icon({
|
||||
icon: 'input-keyboard-symbolic',
|
||||
size: 20,
|
||||
}),
|
||||
label: Label({ css: 'font-size: 20px;' })
|
||||
.hook(Hyprland, getKbdLayout, 'keyboard-layout'),
|
||||
});
|
167
nixosModules/ags/v1/config/ts/media-player/gesture.ts
Normal file
167
nixosModules/ags/v1/config/ts/media-player/gesture.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
const { timeout } = Utils;
|
||||
const { Box, EventBox, Overlay } = Widget;
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
const MAX_OFFSET = 200;
|
||||
const OFFSCREEN = 500;
|
||||
const ANIM_DURATION = 500;
|
||||
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
|
||||
opacity ${ANIM_DURATION}ms ease;`;
|
||||
|
||||
// Types
|
||||
import {
|
||||
CenterBoxGeneric,
|
||||
Gesture,
|
||||
OverlayGeneric,
|
||||
PlayerBox,
|
||||
} from 'global-types';
|
||||
|
||||
|
||||
export default ({
|
||||
setup = () => { /**/ },
|
||||
...props
|
||||
}: Gesture) => {
|
||||
const widget = EventBox();
|
||||
const gesture = Gtk.GestureDrag.new(widget);
|
||||
|
||||
// Have empty PlayerBox to define the size of the widget
|
||||
const emptyPlayer = Box({
|
||||
class_name: 'player',
|
||||
attribute: { empty: true },
|
||||
});
|
||||
|
||||
const content = Overlay({
|
||||
...props,
|
||||
attribute: {
|
||||
players: new Map(),
|
||||
setup: false,
|
||||
dragging: false,
|
||||
|
||||
includesWidget: (playerW: OverlayGeneric) => {
|
||||
return content.overlays.find((w) => w === playerW);
|
||||
},
|
||||
|
||||
showTopOnly: () => content.overlays.forEach((over) => {
|
||||
over.visible = over === content.overlays.at(-1);
|
||||
}),
|
||||
|
||||
moveToTop: (player: CenterBoxGeneric) => {
|
||||
player.visible = true;
|
||||
content.reorder_overlay(player, -1);
|
||||
timeout(ANIM_DURATION, () => {
|
||||
content.attribute.showTopOnly();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
child: emptyPlayer,
|
||||
|
||||
setup: (self) => {
|
||||
setup(self);
|
||||
|
||||
self
|
||||
.hook(gesture, (_, realGesture) => {
|
||||
if (realGesture) {
|
||||
self.overlays.forEach((over) => {
|
||||
over.visible = true;
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.attribute.showTopOnly();
|
||||
}
|
||||
|
||||
// Don't allow gesture when only one player
|
||||
if (self.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.dragging = true;
|
||||
let offset = gesture.get_offset()[1];
|
||||
const playerBox = self.overlays.at(-1) as PlayerBox;
|
||||
|
||||
if (!offset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Slide right
|
||||
if (offset >= 0) {
|
||||
playerBox.setCss(`
|
||||
margin-left: ${offset}px;
|
||||
margin-right: -${offset}px;
|
||||
${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
|
||||
// Slide left
|
||||
else {
|
||||
offset = Math.abs(offset);
|
||||
playerBox.setCss(`
|
||||
margin-left: -${offset}px;
|
||||
margin-right: ${offset}px;
|
||||
${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
}, 'drag-update')
|
||||
|
||||
|
||||
.hook(gesture, () => {
|
||||
// Don't allow gesture when only one player
|
||||
if (self.overlays.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.attribute.dragging = false;
|
||||
const offset = gesture.get_offset()[1];
|
||||
|
||||
const playerBox = self.overlays.at(-1) as PlayerBox;
|
||||
|
||||
// If crosses threshold after letting go, slide away
|
||||
if (offset && Math.abs(offset) > MAX_OFFSET) {
|
||||
// Disable inputs during animation
|
||||
widget.sensitive = false;
|
||||
|
||||
// Slide away right
|
||||
if (offset >= 0) {
|
||||
playerBox.setCss(`
|
||||
${TRANSITION}
|
||||
margin-left: ${OFFSCREEN}px;
|
||||
margin-right: -${OFFSCREEN}px;
|
||||
opacity: 0.7; ${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
|
||||
// Slide away left
|
||||
else {
|
||||
playerBox.setCss(`
|
||||
${TRANSITION}
|
||||
margin-left: -${OFFSCREEN}px;
|
||||
margin-right: ${OFFSCREEN}px;
|
||||
opacity: 0.7; ${playerBox.attribute.bgStyle}
|
||||
`);
|
||||
}
|
||||
|
||||
timeout(ANIM_DURATION, () => {
|
||||
// Put the player in the back after anim
|
||||
self.reorder_overlay(playerBox, 0);
|
||||
// Recenter player
|
||||
playerBox.setCss(playerBox.attribute.bgStyle);
|
||||
|
||||
widget.sensitive = true;
|
||||
|
||||
self.attribute.showTopOnly();
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Recenter with transition for animation
|
||||
playerBox.setCss(`${TRANSITION}
|
||||
${playerBox.attribute.bgStyle}`);
|
||||
}
|
||||
}, 'drag-end');
|
||||
},
|
||||
});
|
||||
|
||||
widget.add(content);
|
||||
|
||||
return widget;
|
||||
};
|
473
nixosModules/ags/v1/config/ts/media-player/mpris.ts
Normal file
473
nixosModules/ags/v1/config/ts/media-player/mpris.ts
Normal file
|
@ -0,0 +1,473 @@
|
|||
const Mpris = await Service.import('mpris');
|
||||
|
||||
const { Button, Icon, Label, Stack, Slider, CenterBox, Box } = Widget;
|
||||
const { execAsync, lookUpIcon, readFileAsync } = Utils;
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
import CursorBox from '../misc/cursorbox.ts';
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
const icons = {
|
||||
mpris: {
|
||||
fallback: 'audio-x-generic-symbolic',
|
||||
shuffle: {
|
||||
enabled: '',
|
||||
disabled: '',
|
||||
},
|
||||
loop: {
|
||||
none: '',
|
||||
track: '',
|
||||
playlist: '',
|
||||
},
|
||||
playing: ' ',
|
||||
paused: ' ',
|
||||
stopped: ' ',
|
||||
prev: '',
|
||||
next: '',
|
||||
},
|
||||
};
|
||||
|
||||
// Types
|
||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
||||
import { Variable as Var } from 'types/variable';
|
||||
|
||||
import {
|
||||
AgsWidget,
|
||||
CenterBoxPropsGeneric,
|
||||
Colors,
|
||||
PlayerBox,
|
||||
PlayerButtonType,
|
||||
PlayerDrag,
|
||||
PlayerOverlay,
|
||||
} from 'global-types';
|
||||
|
||||
|
||||
export const CoverArt = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
props: CenterBoxPropsGeneric,
|
||||
) => CenterBox({
|
||||
...props,
|
||||
vertical: true,
|
||||
|
||||
attribute: {
|
||||
bgStyle: '',
|
||||
player,
|
||||
},
|
||||
|
||||
setup: (self: PlayerBox) => {
|
||||
// Give temp cover art
|
||||
readFileAsync(player.cover_path).catch(() => {
|
||||
if (!colors.value && !player.track_cover_url) {
|
||||
colors.setValue({
|
||||
imageAccent: '#6b4fa2',
|
||||
buttonAccent: '#ecdcff',
|
||||
buttonText: '#25005a',
|
||||
hoverAccent: '#d4baff',
|
||||
});
|
||||
|
||||
self.attribute.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${(colors as Var<Colors>).value.imageAccent}),
|
||||
rgb(0, 0, 0);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
self.setCss(self.attribute.bgStyle);
|
||||
}
|
||||
});
|
||||
|
||||
self.hook(player, () => {
|
||||
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
|
||||
coloryou "${player.cover_path}" | grep -v Warning`])
|
||||
.then((out) => {
|
||||
if (!Mpris.players.find((p) => player === p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
colors.setValue(JSON.parse(out));
|
||||
|
||||
self.attribute.bgStyle = `
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 0, 0, 0.4) 30%,
|
||||
${colors.value.imageAccent}),
|
||||
url("${player.cover_path}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
`;
|
||||
|
||||
if (!(self.get_parent() as PlayerDrag)
|
||||
.attribute.dragging) {
|
||||
self.setCss(self.attribute.bgStyle);
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (err !== '') {
|
||||
print(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const TitleLabel = (player: MprisPlayer) => Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: 'end',
|
||||
justification: 'left',
|
||||
class_name: 'title',
|
||||
label: player.bind('track_title'),
|
||||
});
|
||||
|
||||
export const ArtistLabel = (player: MprisPlayer) => Label({
|
||||
xalign: 0,
|
||||
max_width_chars: 40,
|
||||
truncate: 'end',
|
||||
justification: 'left',
|
||||
class_name: 'artist',
|
||||
label: player.bind('track_artists')
|
||||
.transform((a) => a.join(', ') || ''),
|
||||
});
|
||||
|
||||
|
||||
export const PlayerIcon = (player: MprisPlayer, overlay: PlayerOverlay) => {
|
||||
const playerIcon = (
|
||||
p: MprisPlayer,
|
||||
widget?: PlayerOverlay,
|
||||
playerBox?: PlayerBox,
|
||||
) => CursorBox({
|
||||
tooltip_text: p.identity || '',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
if (widget && playerBox) {
|
||||
widget.attribute.moveToTop(playerBox);
|
||||
}
|
||||
},
|
||||
|
||||
child: Icon({
|
||||
class_name: widget ? 'position-indicator' : 'player-icon',
|
||||
size: widget ? 0 : ICON_SIZE,
|
||||
|
||||
setup: (self) => {
|
||||
self.hook(p, () => {
|
||||
self.icon = lookUpIcon(p.entry) ?
|
||||
p.entry :
|
||||
icons.mpris.fallback;
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return Box().hook(Mpris, (self) => {
|
||||
const grandPa = self.get_parent()?.get_parent() as AgsWidget;
|
||||
|
||||
if (!grandPa) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thisIndex = overlay.overlays
|
||||
.indexOf(grandPa);
|
||||
|
||||
self.children = (overlay.overlays as PlayerBox[])
|
||||
.map((playerBox, i) => {
|
||||
self.children.push(Separator(2));
|
||||
|
||||
return i === thisIndex ?
|
||||
playerIcon(player) :
|
||||
playerIcon(playerBox.attribute.player, overlay, playerBox);
|
||||
})
|
||||
.reverse();
|
||||
});
|
||||
};
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
export const PositionSlider = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => Slider({
|
||||
class_name: 'position-slider',
|
||||
vpack: 'center',
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
|
||||
on_change: ({ value }) => {
|
||||
player.position = player.length * value;
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
const update = () => {
|
||||
if (!self.dragging) {
|
||||
self.visible = player.length > 0;
|
||||
if (player.length > 0) {
|
||||
self.value = player.position / player.length;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self
|
||||
.poll(1000, () => update())
|
||||
.hook(player, () => update(), 'position')
|
||||
.hook(colors, () => {
|
||||
if (colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
self.setCss(`
|
||||
highlight { background-color: ${c.buttonAccent}; }
|
||||
slider { background-color: ${c.buttonAccent}; }
|
||||
slider:hover { background-color: ${c.hoverAccent}; }
|
||||
trough { background-color: ${c.buttonText}; }
|
||||
`);
|
||||
}
|
||||
})
|
||||
|
||||
// OnClick
|
||||
.on('button-press-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'grabbing',
|
||||
));
|
||||
})
|
||||
|
||||
// OnRelease
|
||||
.on('button-release-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const PlayerButton = ({
|
||||
player,
|
||||
colors,
|
||||
children = {},
|
||||
onClick,
|
||||
prop,
|
||||
}: PlayerButtonType) => CursorBox({
|
||||
child: Button({
|
||||
attribute: { hovered: false },
|
||||
child: Stack({ children }),
|
||||
|
||||
on_primary_click_release: () => player[onClick](),
|
||||
|
||||
on_hover: (self) => {
|
||||
self.attribute.hovered = true;
|
||||
|
||||
if (prop === 'playBackStatus' && colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
Object.values(children).forEach((ch: AgsWidget) => {
|
||||
ch.setCss(`
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
on_hover_lost: (self) => {
|
||||
self.attribute.hovered = false;
|
||||
if (prop === 'playBackStatus' && colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
Object.values(children).forEach((ch: AgsWidget) => {
|
||||
ch.setCss(`
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(player, () => {
|
||||
self.child.shown = `${player[prop]}`;
|
||||
})
|
||||
.hook(colors, () => {
|
||||
if (!Mpris.players.find((p) => player === p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (colors.value) {
|
||||
const c = colors.value;
|
||||
|
||||
if (prop === 'playBackStatus') {
|
||||
if (self.attribute.hovered) {
|
||||
Object.values(children)
|
||||
.forEach((ch: AgsWidget) => {
|
||||
ch.setCss(`
|
||||
background-color: ${c.hoverAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 40px;
|
||||
min-width: 36px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 1px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
else {
|
||||
Object.values(children)
|
||||
.forEach((ch: AgsWidget) => {
|
||||
ch.setCss(`
|
||||
background-color: ${c.buttonAccent};
|
||||
color: ${c.buttonText};
|
||||
min-height: 42px;
|
||||
min-width: 38px;
|
||||
`);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.setCss(`
|
||||
* { color: ${c.buttonAccent}; }
|
||||
*:hover { color: ${c.hoverAccent}; }
|
||||
`);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const ShuffleButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: {
|
||||
true: Label({
|
||||
class_name: 'shuffle enabled',
|
||||
label: icons.mpris.shuffle.enabled,
|
||||
}),
|
||||
false: Label({
|
||||
class_name: 'shuffle disabled',
|
||||
label: icons.mpris.shuffle.disabled,
|
||||
}),
|
||||
},
|
||||
onClick: 'shuffle',
|
||||
prop: 'shuffleStatus',
|
||||
});
|
||||
|
||||
export const LoopButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: {
|
||||
None: Label({
|
||||
class_name: 'loop none',
|
||||
label: icons.mpris.loop.none,
|
||||
}),
|
||||
Track: Label({
|
||||
class_name: 'loop track',
|
||||
label: icons.mpris.loop.track,
|
||||
}),
|
||||
Playlist: Label({
|
||||
class_name: 'loop playlist',
|
||||
label: icons.mpris.loop.playlist,
|
||||
}),
|
||||
},
|
||||
onClick: 'loop',
|
||||
prop: 'loopStatus',
|
||||
});
|
||||
|
||||
export const PlayPauseButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: {
|
||||
Playing: Label({
|
||||
class_name: 'pausebutton playing',
|
||||
label: icons.mpris.playing,
|
||||
}),
|
||||
Paused: Label({
|
||||
class_name: 'pausebutton paused',
|
||||
label: icons.mpris.paused,
|
||||
}),
|
||||
Stopped: Label({
|
||||
class_name: 'pausebutton stopped paused',
|
||||
label: icons.mpris.stopped,
|
||||
}),
|
||||
},
|
||||
onClick: 'playPause',
|
||||
prop: 'playBackStatus',
|
||||
});
|
||||
|
||||
export const PreviousButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: {
|
||||
true: Label({
|
||||
class_name: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
}),
|
||||
false: Label({
|
||||
class_name: 'previous',
|
||||
label: icons.mpris.prev,
|
||||
}),
|
||||
},
|
||||
onClick: 'previous',
|
||||
prop: 'canGoPrev',
|
||||
});
|
||||
|
||||
export const NextButton = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => PlayerButton({
|
||||
player,
|
||||
colors,
|
||||
children: {
|
||||
true: Label({
|
||||
class_name: 'next',
|
||||
label: icons.mpris.next,
|
||||
}),
|
||||
false: Label({
|
||||
class_name: 'next',
|
||||
label: icons.mpris.next,
|
||||
}),
|
||||
},
|
||||
onClick: 'next',
|
||||
prop: 'canGoNext',
|
||||
});
|
201
nixosModules/ags/v1/config/ts/media-player/player.ts
Normal file
201
nixosModules/ags/v1/config/ts/media-player/player.ts
Normal file
|
@ -0,0 +1,201 @@
|
|||
const Mpris = await Service.import('mpris');
|
||||
|
||||
const { Box, CenterBox } = Widget;
|
||||
|
||||
import * as mpris from './mpris.ts';
|
||||
import PlayerGesture from './gesture.ts';
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
|
||||
const SPACING = 8;
|
||||
|
||||
// Types
|
||||
import { MprisPlayer } from 'types/service/mpris.ts';
|
||||
import { Variable as Var } from 'types/variable';
|
||||
import { Colors, PlayerBox, PlayerOverlay } from 'global-types';
|
||||
|
||||
|
||||
const Top = (
|
||||
player: MprisPlayer,
|
||||
overlay: PlayerOverlay,
|
||||
) => Box({
|
||||
class_name: 'top',
|
||||
hpack: 'start',
|
||||
vpack: 'start',
|
||||
|
||||
children: [
|
||||
mpris.PlayerIcon(player, overlay),
|
||||
],
|
||||
});
|
||||
|
||||
const Center = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => Box({
|
||||
class_name: 'center',
|
||||
|
||||
children: [
|
||||
CenterBox({
|
||||
vertical: true,
|
||||
|
||||
start_widget: Box({
|
||||
class_name: 'metadata',
|
||||
vertical: true,
|
||||
hpack: 'start',
|
||||
vpack: 'center',
|
||||
hexpand: true,
|
||||
|
||||
children: [
|
||||
mpris.TitleLabel(player),
|
||||
mpris.ArtistLabel(player),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
|
||||
CenterBox({
|
||||
vertical: true,
|
||||
|
||||
center_widget: mpris.PlayPauseButton(player, colors),
|
||||
}),
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const Bottom = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
) => Box({
|
||||
class_name: 'bottom',
|
||||
|
||||
children: [
|
||||
mpris.PreviousButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.PositionSlider(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.NextButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.ShuffleButton(player, colors),
|
||||
Separator(SPACING),
|
||||
|
||||
mpris.LoopButton(player, colors),
|
||||
],
|
||||
});
|
||||
|
||||
const PlayerBox = (
|
||||
player: MprisPlayer,
|
||||
colors: Var<Colors>,
|
||||
overlay: PlayerOverlay,
|
||||
) => {
|
||||
const widget = mpris.CoverArt(player, colors, {
|
||||
class_name: `player ${player.name}`,
|
||||
hexpand: true,
|
||||
|
||||
start_widget: Top(player, overlay),
|
||||
center_widget: Center(player, colors),
|
||||
end_widget: Bottom(player, colors),
|
||||
});
|
||||
|
||||
widget.visible = false;
|
||||
|
||||
return widget;
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const content = PlayerGesture({
|
||||
setup: (self: PlayerOverlay) => {
|
||||
self
|
||||
.hook(Mpris, (_, bus_name) => {
|
||||
const players = self.attribute.players;
|
||||
|
||||
if (players.has(bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sometimes the signal doesn't give the bus_name
|
||||
if (!bus_name) {
|
||||
const player = Mpris.players.find((p) => {
|
||||
return !players.has(p.bus_name);
|
||||
});
|
||||
|
||||
if (player) {
|
||||
bus_name = player.bus_name;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1) as PlayerBox;
|
||||
|
||||
// Make the new player
|
||||
const player = Mpris.getPlayer(bus_name);
|
||||
const colorsVar = Variable({
|
||||
imageAccent: '#6b4fa2',
|
||||
buttonAccent: '#ecdcff',
|
||||
buttonText: '#25005a',
|
||||
hoverAccent: '#d4baff',
|
||||
});
|
||||
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
players.set(
|
||||
bus_name,
|
||||
PlayerBox(player, colorsVar, self),
|
||||
);
|
||||
self.overlays = Array.from(players.values())
|
||||
.map((widget) => widget) as PlayerBox[];
|
||||
|
||||
const includes = self.attribute
|
||||
.includesWidget(previousFirst);
|
||||
|
||||
// Select favorite player at startup
|
||||
const attrs = self.attribute;
|
||||
|
||||
if (!attrs.setup && players.has(FAVE_PLAYER)) {
|
||||
attrs.moveToTop(players.get(FAVE_PLAYER));
|
||||
attrs.setup = true;
|
||||
}
|
||||
|
||||
// Move previousFirst on top again
|
||||
else if (includes) {
|
||||
attrs.moveToTop(previousFirst);
|
||||
}
|
||||
}, 'player-added')
|
||||
|
||||
.hook(Mpris, (_, bus_name) => {
|
||||
const players = self.attribute.players;
|
||||
|
||||
if (!bus_name || !players.has(bus_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the one on top so we can move it up later
|
||||
const previousFirst = self.overlays.at(-1) as PlayerBox;
|
||||
|
||||
// Remake overlays without deleted one
|
||||
players.delete(bus_name);
|
||||
self.overlays = Array.from(players.values())
|
||||
.map((widget) => widget) as PlayerBox[];
|
||||
|
||||
// Move previousFirst on top again
|
||||
const includes = self.attribute
|
||||
.includesWidget(previousFirst);
|
||||
|
||||
if (includes) {
|
||||
self.attribute.moveToTop(previousFirst);
|
||||
}
|
||||
}, 'player-closed');
|
||||
},
|
||||
});
|
||||
|
||||
return Box({
|
||||
class_name: 'media',
|
||||
child: content,
|
||||
});
|
||||
};
|
164
nixosModules/ags/v1/config/ts/on-screen-keyboard/gesture.ts
Normal file
164
nixosModules/ags/v1/config/ts/on-screen-keyboard/gesture.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
const Hyprland = await Service.import('hyprland');
|
||||
|
||||
const { execAsync, timeout } = Utils;
|
||||
|
||||
const { Gtk } = imports.gi;
|
||||
|
||||
import Tablet from '../../services/tablet.ts';
|
||||
|
||||
const KEY_N = 249;
|
||||
const HIDDEN_MARGIN = 340;
|
||||
const ANIM_DURATION = 700;
|
||||
|
||||
// Types
|
||||
import { OskWindow } from 'global-types';
|
||||
|
||||
|
||||
const releaseAllKeys = () => {
|
||||
const keycodes = Array.from(Array(KEY_N).keys());
|
||||
|
||||
execAsync([
|
||||
'ydotool', 'key',
|
||||
...keycodes.map((keycode) => `${keycode}:0`),
|
||||
]).catch(print);
|
||||
};
|
||||
|
||||
export default (window: OskWindow) => {
|
||||
const gesture = Gtk.GestureDrag.new(window);
|
||||
|
||||
window.child.setCss(`margin-bottom: -${HIDDEN_MARGIN}px;`);
|
||||
|
||||
let signals = [] as number[];
|
||||
|
||||
window.attribute = {
|
||||
startY: null,
|
||||
|
||||
setVisible: (state: boolean) => {
|
||||
if (state) {
|
||||
window.visible = true;
|
||||
window.attribute.setSlideDown();
|
||||
|
||||
window.child.setCss(`
|
||||
transition: margin-bottom 0.7s
|
||||
cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
margin-bottom: 0px;
|
||||
`);
|
||||
}
|
||||
else {
|
||||
timeout(ANIM_DURATION + 100 + 100, () => {
|
||||
if (!Tablet.tabletMode) {
|
||||
window.visible = false;
|
||||
}
|
||||
});
|
||||
releaseAllKeys();
|
||||
window.attribute.setSlideUp();
|
||||
|
||||
window.child.setCss(`
|
||||
transition: margin-bottom 0.7s
|
||||
cubic-bezier(0.36, 0, 0.66, -0.56);
|
||||
margin-bottom: -${HIDDEN_MARGIN}px;
|
||||
`);
|
||||
}
|
||||
},
|
||||
|
||||
killGestureSigs: () => {
|
||||
signals.forEach((id) => {
|
||||
gesture.disconnect(id);
|
||||
});
|
||||
signals = [];
|
||||
window.attribute.startY = null;
|
||||
},
|
||||
|
||||
setSlideUp: () => {
|
||||
window.attribute.killGestureSigs();
|
||||
|
||||
// Begin drag
|
||||
signals.push(
|
||||
gesture.connect('drag-begin', () => {
|
||||
Hyprland.messageAsync('j/cursorpos').then((out) => {
|
||||
window.attribute.startY = JSON.parse(out).y;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Update drag
|
||||
signals.push(
|
||||
gesture.connect('drag-update', () => {
|
||||
Hyprland.messageAsync('j/cursorpos').then((out) => {
|
||||
if (!window.attribute.startY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentY = JSON.parse(out).y;
|
||||
const offset = window.attribute.startY - currentY;
|
||||
|
||||
if (offset < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.child.setCss(`
|
||||
margin-bottom: ${offset - HIDDEN_MARGIN}px;
|
||||
`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// End drag
|
||||
signals.push(
|
||||
gesture.connect('drag-end', () => {
|
||||
window.child.setCss(`
|
||||
transition: margin-bottom 0.5s ease-in-out;
|
||||
margin-bottom: -${HIDDEN_MARGIN}px;
|
||||
`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
setSlideDown: () => {
|
||||
window.attribute.killGestureSigs();
|
||||
|
||||
// Begin drag
|
||||
signals.push(
|
||||
gesture.connect('drag-begin', () => {
|
||||
Hyprland.messageAsync('j/cursorpos').then((out) => {
|
||||
window.attribute.startY = JSON.parse(out).y;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// Update drag
|
||||
signals.push(
|
||||
gesture.connect('drag-update', () => {
|
||||
Hyprland.messageAsync('j/cursorpos').then((out) => {
|
||||
if (!window.attribute.startY) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentY = JSON.parse(out).y;
|
||||
const offset = window.attribute.startY - currentY;
|
||||
|
||||
if (offset > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.child.setCss(`
|
||||
margin-bottom: ${offset}px;
|
||||
`);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// End drag
|
||||
signals.push(
|
||||
gesture.connect('drag-end', () => {
|
||||
window.child.setCss(`
|
||||
transition: margin-bottom 0.5s ease-in-out;
|
||||
margin-bottom: 0px;
|
||||
`);
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return window;
|
||||
};
|
|
@ -0,0 +1,551 @@
|
|||
// TODO: right Ctrl https://handwiki.org/wiki/images/4/41/KB_Canadian_Multilingual_Standard.svg
|
||||
|
||||
export const defaultOskLayout = 'qwerty_custom';
|
||||
export const oskLayouts = {
|
||||
qwerty_custom: {
|
||||
name: 'QWERTY - Custom',
|
||||
name_short: 'CSA',
|
||||
comment: 'Like physical keyboard',
|
||||
// A normal key looks like this: {label: "a", labelShift: "A", shape: "normal", keycode: 30, type: "normal"}
|
||||
// A modkey looks like this: {label: "Ctrl", shape: "control", keycode: 29, type: "modkey"}
|
||||
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
|
||||
keys: [
|
||||
[
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Esc',
|
||||
shape: 'fn',
|
||||
keycode: 1,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F1',
|
||||
shape: 'fn',
|
||||
keycode: 59,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F2',
|
||||
shape: 'fn',
|
||||
keycode: 60,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F3',
|
||||
shape: 'fn',
|
||||
keycode: 61,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F4',
|
||||
shape: 'fn',
|
||||
keycode: 62,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F5',
|
||||
shape: 'fn',
|
||||
keycode: 63,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F6',
|
||||
shape: 'fn',
|
||||
keycode: 64,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F7',
|
||||
shape: 'fn',
|
||||
keycode: 65,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F8',
|
||||
shape: 'fn',
|
||||
keycode: 66,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F9',
|
||||
shape: 'fn',
|
||||
keycode: 67,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F10',
|
||||
shape: 'fn',
|
||||
keycode: 68,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F11',
|
||||
shape: 'fn',
|
||||
keycode: 87,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'F12',
|
||||
shape: 'fn',
|
||||
keycode: 88,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Home',
|
||||
shape: 'fn',
|
||||
keycode: 110,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'End',
|
||||
shape: 'fn',
|
||||
keycode: 115,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Del',
|
||||
shape: 'fn',
|
||||
keycode: 111,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '/',
|
||||
labelShift: '\\',
|
||||
labelAltGr: '|',
|
||||
shape: 'normal',
|
||||
keycode: 41,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '1',
|
||||
labelShift: '!',
|
||||
shape: 'normal',
|
||||
keycode: 2,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '2',
|
||||
labelShift: '@',
|
||||
shape: 'normal',
|
||||
keycode: 3,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '3',
|
||||
labelShift: '#',
|
||||
labelAltGr: '¤',
|
||||
shape: 'normal',
|
||||
keycode: 4,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '4',
|
||||
labelShift: '$',
|
||||
shape: 'normal',
|
||||
keycode: 5,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '5',
|
||||
labelShift: '%',
|
||||
shape: 'normal',
|
||||
keycode: 6,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '6',
|
||||
labelShift: '?',
|
||||
shape: 'normal',
|
||||
keycode: 7,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '7',
|
||||
labelShift: '&',
|
||||
labelAltGr: '{',
|
||||
shape: 'normal',
|
||||
keycode: 8,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '8',
|
||||
labelShift: '*',
|
||||
labelAltGr: '}',
|
||||
shape: 'normal',
|
||||
keycode: 9,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '9',
|
||||
labelShift: '(',
|
||||
labelAltGr: '[',
|
||||
shape: 'normal',
|
||||
keycode: 10,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '0',
|
||||
labelShift: ')',
|
||||
labelAltGr: ']',
|
||||
shape: 'normal',
|
||||
keycode: 11,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '-',
|
||||
labelShift: '_',
|
||||
shape: 'normal',
|
||||
keycode: 12,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '=',
|
||||
labelShift: '+',
|
||||
labelAltGr: '¬',
|
||||
shape: 'normal',
|
||||
keycode: 13,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Backspace',
|
||||
shape: 'expand',
|
||||
keycode: 14,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Tab',
|
||||
shape: 'tab',
|
||||
keycode: 15,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'q',
|
||||
labelShift: 'Q',
|
||||
shape: 'normal',
|
||||
keycode: 16,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'w',
|
||||
labelShift: 'W',
|
||||
shape: 'normal',
|
||||
keycode: 17,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'e',
|
||||
labelShift: 'E',
|
||||
labelAltGr: '€',
|
||||
shape: 'normal',
|
||||
keycode: 18,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'r',
|
||||
labelShift: 'R',
|
||||
shape: 'normal',
|
||||
keycode: 19,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 't',
|
||||
labelShift: 'T',
|
||||
shape: 'normal',
|
||||
keycode: 20,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'y',
|
||||
labelShift: 'Y',
|
||||
shape: 'normal',
|
||||
keycode: 21,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'u',
|
||||
labelShift: 'U',
|
||||
shape: 'normal',
|
||||
keycode: 22,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'i',
|
||||
labelShift: 'I',
|
||||
shape: 'normal',
|
||||
keycode: 23,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'o',
|
||||
labelShift: 'O',
|
||||
shape: 'normal',
|
||||
keycode: 24,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'p',
|
||||
labelShift: 'P',
|
||||
shape: 'normal',
|
||||
keycode: 25,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '^',
|
||||
labelShift: '"',
|
||||
labelAltGr: '`',
|
||||
shape: 'normal',
|
||||
keycode: 26,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'ç',
|
||||
labelShift: 'Ç',
|
||||
labelAltGr: '~',
|
||||
shape: 'normal',
|
||||
keycode: 27,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'à',
|
||||
labelShift: 'À',
|
||||
shape: 'expand',
|
||||
keycode: 43,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Caps',
|
||||
shape: 'caps',
|
||||
keycode: 58,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'a',
|
||||
labelShift: 'A',
|
||||
shape: 'normal',
|
||||
keycode: 30,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 's',
|
||||
labelShift: 'S',
|
||||
shape: 'normal',
|
||||
keycode: 31,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'd',
|
||||
labelShift: 'D',
|
||||
shape: 'normal',
|
||||
keycode: 32,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'f',
|
||||
labelShift: 'F',
|
||||
shape: 'normal',
|
||||
keycode: 33,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'g',
|
||||
labelShift: 'G',
|
||||
shape: 'normal',
|
||||
keycode: 34,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'h',
|
||||
labelShift: 'H',
|
||||
shape: 'normal',
|
||||
keycode: 35,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'j',
|
||||
labelShift: 'J',
|
||||
shape: 'normal',
|
||||
keycode: 36,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'k',
|
||||
labelShift: 'K',
|
||||
shape: 'normal',
|
||||
keycode: 37,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'l',
|
||||
labelShift: 'L',
|
||||
shape: 'normal',
|
||||
keycode: 38,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: ';',
|
||||
labelShift: ':',
|
||||
labelAltGr: '°',
|
||||
shape: 'normal',
|
||||
keycode: 39,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'è',
|
||||
labelShift: 'È',
|
||||
shape: 'normal',
|
||||
keycode: 40,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Enter',
|
||||
shape: 'expand',
|
||||
keycode: 28,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Shift',
|
||||
shape: 'shift',
|
||||
keycode: 42,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'z',
|
||||
labelShift: 'Z',
|
||||
labelAltGr: '«',
|
||||
shape: 'normal',
|
||||
keycode: 44,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'x',
|
||||
labelShift: 'X',
|
||||
labelAltGr: '»',
|
||||
shape: 'normal',
|
||||
keycode: 45,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'c',
|
||||
labelShift: 'C',
|
||||
shape: 'normal',
|
||||
keycode: 46,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'v',
|
||||
labelShift: 'V',
|
||||
shape: 'normal',
|
||||
keycode: 47,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'b',
|
||||
labelShift: 'B',
|
||||
shape: 'normal',
|
||||
keycode: 48,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'n',
|
||||
labelShift: 'N',
|
||||
shape: 'normal',
|
||||
keycode: 49,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'm',
|
||||
labelShift: 'M',
|
||||
shape: 'normal',
|
||||
keycode: 50,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: ',',
|
||||
labelShift: "'",
|
||||
labelAltGr: '<',
|
||||
shape: 'normal',
|
||||
keycode: 51,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: '.',
|
||||
labelShift: '"',
|
||||
labelAltGr: '>',
|
||||
shape: 'normal',
|
||||
keycode: 52,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'é',
|
||||
labelShift: 'É',
|
||||
shape: 'normal',
|
||||
keycode: 53,
|
||||
},
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Shift',
|
||||
shape: 'expand',
|
||||
keycode: 54,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Ctrl',
|
||||
shape: 'control',
|
||||
keycode: 29,
|
||||
},
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Super',
|
||||
shape: 'normal',
|
||||
keycode: 125,
|
||||
},
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Alt',
|
||||
shape: 'normal',
|
||||
keycode: 56,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Space',
|
||||
shape: 'space',
|
||||
keycode: 57,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'Space',
|
||||
shape: 'space',
|
||||
keycode: 57,
|
||||
},
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'AltGr',
|
||||
shape: 'normal',
|
||||
keycode: 100,
|
||||
},
|
||||
{
|
||||
keytype: 'normal',
|
||||
label: 'PrtSc',
|
||||
shape: 'fn',
|
||||
keycode: 99,
|
||||
},
|
||||
{
|
||||
keytype: 'modkey',
|
||||
label: 'Ctrl',
|
||||
shape: 'control',
|
||||
keycode: 97,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
169
nixosModules/ags/v1/config/ts/on-screen-keyboard/keyboard.ts
Normal file
169
nixosModules/ags/v1/config/ts/on-screen-keyboard/keyboard.ts
Normal file
|
@ -0,0 +1,169 @@
|
|||
const { Box, CenterBox, Label, ToggleButton } = Widget;
|
||||
|
||||
const { Gdk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
import RoundedCorner from '../corners/screen-corners.ts';
|
||||
import Key from './keys.ts';
|
||||
|
||||
import { defaultOskLayout, oskLayouts } from './keyboard-layouts.ts';
|
||||
const keyboardLayout = defaultOskLayout;
|
||||
const keyboardJson = oskLayouts[keyboardLayout];
|
||||
|
||||
const L_KEY_PER_ROW = [8, 7, 6, 6, 6, 4]; // eslint-disable-line
|
||||
const COLOR = 'rgba(0, 0, 0, 0.3)';
|
||||
const SPACING = 4;
|
||||
|
||||
// Types
|
||||
import { BoxGeneric, OskWindow } from 'global-types';
|
||||
|
||||
|
||||
export default (window: OskWindow) => Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
CenterBox({
|
||||
hpack: 'center',
|
||||
|
||||
start_widget: RoundedCorner('bottomright', `
|
||||
background-color: ${COLOR};
|
||||
`),
|
||||
|
||||
center_widget: CenterBox({
|
||||
class_name: 'thingy',
|
||||
css: `background: ${COLOR};`,
|
||||
|
||||
center_widget: Box({
|
||||
hpack: 'center',
|
||||
class_name: 'settings',
|
||||
|
||||
children: [
|
||||
ToggleButton({
|
||||
class_name: 'button',
|
||||
active: true,
|
||||
vpack: 'center',
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.on('toggled', () => {
|
||||
self.toggleClassName(
|
||||
'toggled',
|
||||
self.get_active(),
|
||||
);
|
||||
window.exclusivity = self.get_active() ?
|
||||
'exclusive' :
|
||||
'normal';
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(
|
||||
Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
),
|
||||
);
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
|
||||
child: Label('Exclusive'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
|
||||
end_widget: RoundedCorner('bottomleft', `
|
||||
background-color: ${COLOR};
|
||||
`),
|
||||
}),
|
||||
|
||||
CenterBox({
|
||||
css: `background: ${COLOR};`,
|
||||
class_name: 'osk',
|
||||
hexpand: true,
|
||||
|
||||
start_widget: Box({
|
||||
class_name: 'left-side side',
|
||||
hpack: 'start',
|
||||
vertical: true,
|
||||
|
||||
children: keyboardJson.keys.map((row, rowIndex) => {
|
||||
const keys = [] as BoxGeneric[];
|
||||
|
||||
row.forEach((key, keyIndex) => {
|
||||
if (keyIndex < L_KEY_PER_ROW[rowIndex]) {
|
||||
keys.push(Key(key));
|
||||
}
|
||||
});
|
||||
|
||||
return Box({
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Box({
|
||||
class_name: 'row',
|
||||
|
||||
children: [
|
||||
Separator(SPACING),
|
||||
|
||||
...keys,
|
||||
],
|
||||
}),
|
||||
|
||||
Separator(SPACING, { vertical: true }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}),
|
||||
|
||||
center_widget: Box({
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
|
||||
children: [
|
||||
],
|
||||
}),
|
||||
|
||||
end_widget: Box({
|
||||
class_name: 'right-side side',
|
||||
hpack: 'end',
|
||||
vertical: true,
|
||||
|
||||
children: keyboardJson.keys.map((row, rowIndex) => {
|
||||
const keys = [] as BoxGeneric[];
|
||||
|
||||
row.forEach((key, keyIndex) => {
|
||||
if (keyIndex >= L_KEY_PER_ROW[rowIndex]) {
|
||||
keys.push(Key(key));
|
||||
}
|
||||
});
|
||||
|
||||
return Box({
|
||||
vertical: true,
|
||||
|
||||
children: [
|
||||
Box({
|
||||
hpack: 'end',
|
||||
class_name: 'row',
|
||||
|
||||
children: keys,
|
||||
}),
|
||||
|
||||
Separator(SPACING, { vertical: true }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
254
nixosModules/ags/v1/config/ts/on-screen-keyboard/keys.ts
Normal file
254
nixosModules/ags/v1/config/ts/on-screen-keyboard/keys.ts
Normal file
|
@ -0,0 +1,254 @@
|
|||
import Brightness from '../../services/brightness.ts';
|
||||
|
||||
const { Box, EventBox, Label } = Widget;
|
||||
const { execAsync } = Utils;
|
||||
|
||||
const { Gdk, Gtk } = imports.gi;
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
import Separator from '../misc/separator.ts';
|
||||
|
||||
// Keep track of when a non modifier key
|
||||
// is clicked to release all modifiers
|
||||
const NormalClick = Variable(false);
|
||||
|
||||
// Keep track of modifier statuses
|
||||
const Super = Variable(false);
|
||||
const LAlt = Variable(false);
|
||||
const LCtrl = Variable(false);
|
||||
const AltGr = Variable(false);
|
||||
const RCtrl = Variable(false);
|
||||
|
||||
const Caps = Variable(false);
|
||||
|
||||
Brightness.connect('caps', (_, state) => {
|
||||
Caps.setValue(state);
|
||||
});
|
||||
|
||||
// Assume both shifts are the same for key.labelShift
|
||||
const LShift = Variable(false);
|
||||
const RShift = Variable(false);
|
||||
|
||||
const Shift = Variable(false);
|
||||
|
||||
LShift.connect('changed', () => {
|
||||
Shift.setValue(LShift.value || RShift.value);
|
||||
});
|
||||
RShift.connect('changed', () => {
|
||||
Shift.setValue(LShift.value || RShift.value);
|
||||
});
|
||||
|
||||
const SPACING = 4;
|
||||
const LSHIFT_CODE = 42;
|
||||
const LALT_CODE = 56;
|
||||
const LCTRL_CODE = 29;
|
||||
|
||||
// Types
|
||||
import { Variable as Var } from 'types/variable.ts';
|
||||
interface Key {
|
||||
keytype: string
|
||||
label: string
|
||||
labelShift?: string
|
||||
labelAltGr?: string
|
||||
shape: string
|
||||
keycode: number
|
||||
}
|
||||
|
||||
|
||||
const ModKey = (key: Key) => {
|
||||
let Mod: Var<boolean>;
|
||||
|
||||
if (key.label === 'Super') {
|
||||
Mod = Super;
|
||||
}
|
||||
|
||||
// Differentiate left and right mods
|
||||
else if (key.label === 'Shift' && key.keycode === LSHIFT_CODE) {
|
||||
Mod = LShift;
|
||||
}
|
||||
|
||||
else if (key.label === 'Alt' && key.keycode === LALT_CODE) {
|
||||
Mod = LAlt;
|
||||
}
|
||||
|
||||
else if (key.label === 'Ctrl' && key.keycode === LCTRL_CODE) {
|
||||
Mod = LCtrl;
|
||||
}
|
||||
|
||||
else if (key.label === 'Shift') {
|
||||
Mod = RShift;
|
||||
}
|
||||
|
||||
else if (key.label === 'AltGr') {
|
||||
Mod = AltGr;
|
||||
}
|
||||
|
||||
else if (key.label === 'Ctrl') {
|
||||
Mod = RCtrl;
|
||||
}
|
||||
const label = Label({
|
||||
class_name: `mod ${key.label}`,
|
||||
label: key.label,
|
||||
});
|
||||
|
||||
const button = EventBox({
|
||||
class_name: 'key',
|
||||
|
||||
on_primary_click_release: () => {
|
||||
console.log('mod toggled');
|
||||
|
||||
execAsync(`ydotool key ${key.keycode}:${Mod.value ? 0 : 1}`);
|
||||
|
||||
label.toggleClassName('active', !Mod.value);
|
||||
Mod.setValue(!Mod.value);
|
||||
},
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(NormalClick, () => {
|
||||
Mod.setValue(false);
|
||||
|
||||
label.toggleClassName('active', false);
|
||||
execAsync(`ydotool key ${key.keycode}:0`);
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
child: label,
|
||||
});
|
||||
|
||||
return Box({
|
||||
children: [
|
||||
button,
|
||||
Separator(SPACING),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const RegularKey = (key: Key) => {
|
||||
const widget = EventBox({
|
||||
class_name: 'key',
|
||||
|
||||
child: Label({
|
||||
class_name: `normal ${key.label}`,
|
||||
label: key.label,
|
||||
|
||||
setup: (self) => {
|
||||
self
|
||||
.hook(Shift, () => {
|
||||
if (!key.labelShift) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.label = Shift.value ? key.labelShift : key.label;
|
||||
})
|
||||
.hook(Caps, () => {
|
||||
if (key.label === 'Caps') {
|
||||
self.toggleClassName('active', Caps.value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!key.labelShift) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.label.match(/[A-Za-z]/)) {
|
||||
self.label = Caps.value ?
|
||||
key.labelShift :
|
||||
key.label;
|
||||
}
|
||||
})
|
||||
.hook(AltGr, () => {
|
||||
if (!key.labelAltGr) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toggleClassName('altgr', AltGr.value);
|
||||
self.label = AltGr.value ? key.labelAltGr : key.label;
|
||||
})
|
||||
|
||||
// OnHover
|
||||
.on('enter-notify-event', () => {
|
||||
if (!display) {
|
||||
return;
|
||||
}
|
||||
self.window.set_cursor(Gdk.Cursor.new_from_name(
|
||||
display,
|
||||
'pointer',
|
||||
));
|
||||
self.toggleClassName('hover', true);
|
||||
})
|
||||
|
||||
// OnHoverLost
|
||||
.on('leave-notify-event', () => {
|
||||
self.window.set_cursor(null);
|
||||
self.toggleClassName('hover', false);
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const gesture = Gtk.GestureLongPress.new(widget);
|
||||
|
||||
gesture.delay_factor = 1.0;
|
||||
|
||||
// Long press
|
||||
widget.hook(gesture, () => {
|
||||
const pointer = gesture.get_point(null);
|
||||
const x = pointer[1];
|
||||
const y = pointer[2];
|
||||
|
||||
if ((!x || !y) || (x === 0 && y === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Not implemented yet');
|
||||
|
||||
// TODO: popup menu for accents
|
||||
}, 'pressed');
|
||||
|
||||
// OnPrimaryClickRelease
|
||||
widget.hook(gesture, () => {
|
||||
const pointer = gesture.get_point(null);
|
||||
const x = pointer[1];
|
||||
const y = pointer[2];
|
||||
|
||||
if ((!x || !y) || (x === 0 && y === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('key clicked');
|
||||
|
||||
execAsync(`ydotool key ${key.keycode}:1`);
|
||||
execAsync(`ydotool key ${key.keycode}:0`);
|
||||
NormalClick.setValue(true);
|
||||
}, 'cancelled');
|
||||
|
||||
return Box({
|
||||
children: [
|
||||
widget,
|
||||
Separator(SPACING),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default (key: Key) => key.keytype === 'normal' ?
|
||||
RegularKey(key) :
|
||||
ModKey(key);
|
32
nixosModules/ags/v1/config/ts/on-screen-keyboard/main.ts
Normal file
32
nixosModules/ags/v1/config/ts/on-screen-keyboard/main.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
const { Window } = Widget;
|
||||
const { execAsync } = Utils;
|
||||
|
||||
import Tablet from '../../services/tablet.ts';
|
||||
import Gesture from './gesture.ts';
|
||||
import Keyboard from './keyboard.ts';
|
||||
|
||||
/* Types */
|
||||
import { OskWindow } from 'global-types';
|
||||
|
||||
// Start ydotool daemon
|
||||
execAsync('ydotoold').catch(print);
|
||||
|
||||
// Window
|
||||
export default () => {
|
||||
const window = Window({
|
||||
name: 'osk',
|
||||
layer: 'overlay',
|
||||
anchor: ['left', 'bottom', 'right'],
|
||||
})
|
||||
.hook(Tablet, (self: OskWindow, state) => {
|
||||
self.attribute.setVisible(state);
|
||||
}, 'osk-toggled')
|
||||
|
||||
.hook(Tablet, () => {
|
||||
window.visible = !(!Tablet.tabletMode && !Tablet.oskState);
|
||||
}, 'mode-toggled');
|
||||
|
||||
window.child = Keyboard(window);
|
||||
|
||||
return Gesture(window);
|
||||
};
|
28
nixosModules/ags/v1/icons.nix
Normal file
28
nixosModules/ags/v1/icons.nix
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
agsConfigDir,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
"${agsConfigDir}/config/icons/down-large-symbolic.svg".source = pkgs.fetchurl {
|
||||
url = "https://www.svgrepo.com/download/158537/down-chevron.svg";
|
||||
hash = "sha256-mOfNjgZh0rt6XosKA2kpLY22lJldSS1XCphgrnvZH1s=";
|
||||
};
|
||||
|
||||
"${agsConfigDir}/config/icons/nixos-logo-symbolic.svg".text =
|
||||
# xml
|
||||
''
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path id="lambda" d="M7.352 1.592l-1.364.002L5.32 2.75l1.557 2.713-3.137-.008-1.32 2.34H14.11l-1.353-2.332-3.192-.006-2.214-3.865z" fill="#000000" />
|
||||
</defs>
|
||||
<use xlink:href="#lambda" />
|
||||
<use xlink:href="#lambda" transform="rotate(120 12 12)" />
|
||||
<use xlink:href="#lambda" transform="rotate(240 12 12)" />
|
||||
<g opacity=".7">
|
||||
<use xlink:href="#lambda" transform="rotate(60 12 12)" />
|
||||
<use xlink:href="#lambda" transform="rotate(180 12 12)" />
|
||||
<use xlink:href="#lambda" transform="rotate(300 12 12)" />
|
||||
</g>
|
||||
</svg>
|
||||
'';
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue