feat(ags): update to official agsV2

This commit is contained in:
matt1432 2024-11-13 19:39:01 -05:00
parent 5d27b3d975
commit f3e06554e4
105 changed files with 245 additions and 254 deletions

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

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

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

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

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

View 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(),
}),
});

View 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'),
});

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

View 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',
});

View 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,
});
};

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

View file

@ -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,
},
],
],
},
};

View 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 }),
],
});
}),
}),
}),
],
});

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

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

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