diff --git a/nixosModules/ags/config/configurations/wim.ts b/nixosModules/ags/config/configurations/wim.ts index 49c98a48..5bd0ec51 100644 --- a/nixosModules/ags/config/configurations/wim.ts +++ b/nixosModules/ags/config/configurations/wim.ts @@ -12,6 +12,7 @@ import Corners from '../widgets/corners/main'; import IconBrowser from '../widgets/icon-browser/main'; import { NotifPopups, NotifCenter } from '../widgets/notifs/wim'; import OSD from '../widgets/osd/main'; +import OSK from '../widgets/on-screen-keyboard/main'; import PowerMenu from '../widgets/powermenu/main'; import Screenshot from '../widgets/screenshot/main'; @@ -65,6 +66,7 @@ export default () => { NotifPopups(); NotifCenter(); OSD(); + OSK(); PowerMenu(); Screenshot(); diff --git a/nixosModules/ags/config/services/tablet.ts b/nixosModules/ags/config/services/tablet.ts new file mode 100644 index 00000000..350ce60c --- /dev/null +++ b/nixosModules/ags/config/services/tablet.ts @@ -0,0 +1,176 @@ +import { execAsync, subprocess } from 'astal'; +import GObject, { register, property, signal } from 'astal/gobject'; + +import { hyprMessage } from '../lib'; + +/* Types */ +import AstalIO from 'gi://AstalIO'; +type RotationName = 'normal' | 'right-up' | 'bottom-up' | 'left-up'; + + +const ROTATION_MAP: Record = { + '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', +]; + +@register() +export default class Tablet extends GObject.Object { + @signal(Boolean) + declare autorotateChanged: (running: boolean) => void; + + @signal(Boolean) + declare inputsChanged: (blocked: boolean) => void; + + + private _currentMode = 'laptop'; + + @property(String) + get currentMode() { + return this._currentMode; + } + + set currentMode(val) { + this._currentMode = val; + + if (this._currentMode === 'tablet') { + execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '0']) + .catch(print); + + this.startAutorotate(); + this._blockInputs(); + } + else if (this._currentMode === 'laptop') { + execAsync(['brightnessctl', '-d', 'tpacpi::kbd_backlight', 's', '2']) + .catch(print); + + this.killAutorotate(); + this._unblockInputs(); + } + + this.notify('current-mode'); + } + + + private _oskState = false; + + @property(Boolean) + get oskState() { + return this._oskState; + } + + set oskState(val) { + this._oskState = val; + this.notify('osk-state'); + } + + public toggleOsk() { + this.oskState = !this.oskState; + } + + + private _autorotate = null as AstalIO.Process | null; + + get autorotateState() { + return this._autorotate !== null; + } + + + private _blockedInputs = null as AstalIO.Process | null; + + private _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-changed', true); + } + + private _unblockInputs() { + if (this._blockedInputs) { + this._blockedInputs.kill(); + this._blockedInputs = null; + + this.emit('inputs-changed', false); + } + } + + + public toggleMode() { + if (this.currentMode === 'laptop') { + this.currentMode = 'tablet'; + } + else if (this.currentMode === 'tablet') { + this.currentMode = 'laptop'; + } + } + + + public startAutorotate() { + if (this._autorotate) { + return; + } + + this._autorotate = subprocess( + ['monitor-sensor'], + (output) => { + if (output.includes('orientation changed')) { + const index = output.split(' ').at(-1) as RotationName | undefined; + + if (!index) { + return; + } + + const orientation = ROTATION_MAP[index]; + + hyprMessage( + `keyword monitor ${SCREEN},transform,${orientation}`, + ).catch(print); + + const batchRotate = DEVICES.map((dev) => + `keyword device:${dev}:transform ${orientation}; `); + + hyprMessage(`[[BATCH]] ${batchRotate.flat()}`); + } + }, + ); + + this.emit('autorotate-changed', true); + } + + public killAutorotate() { + if (this._autorotate) { + this._autorotate.kill(); + this._autorotate = null; + + this.emit('autorotate-changed', false); + } + } + + + private static _default: InstanceType | undefined; + + public static get_default() { + if (!Tablet._default) { + Tablet._default = new Tablet(); + } + + return Tablet._default; + } +} diff --git a/nixosModules/ags/config/style/main.scss b/nixosModules/ags/config/style/main.scss index f8aa1342..d7822bfb 100644 --- a/nixosModules/ags/config/style/main.scss +++ b/nixosModules/ags/config/style/main.scss @@ -7,6 +7,7 @@ @use '../widgets/icon-browser'; @use '../widgets/misc'; @use '../widgets/notifs'; +@use '../widgets/on-screen-keyboard'; @use '../widgets/osd'; @use '../widgets/powermenu'; @use '../widgets/screenshot'; diff --git a/nixosModules/ags/v1/config/scss/osk.scss b/nixosModules/ags/config/widgets/on-screen-keyboard/_index.scss similarity index 71% rename from nixosModules/ags/v1/config/scss/osk.scss rename to nixosModules/ags/config/widgets/on-screen-keyboard/_index.scss index 85dd7332..a5456401 100644 --- a/nixosModules/ags/v1/config/scss/osk.scss +++ b/nixosModules/ags/config/widgets/on-screen-keyboard/_index.scss @@ -1,23 +1,5 @@ -.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; - } - } - } -} +@use 'sass:color'; +@use '../../style/colors'; .osk { padding-top: 4px; @@ -26,12 +8,12 @@ .side { .key { &:active label { - background-color: $contrast-bg; + background-color: colors.$accent-color; } label { - background-color: $bg; - border: 0.08rem solid $darkbg; + background-color: colors.$window_bg_color; + border: 0.08rem solid color.adjust(colors.$window_bg_color, $lightness: -3%); border-radius: 0.7rem; min-height: 3rem; @@ -67,7 +49,7 @@ } &.active { - background-color: $darkbg; + background-color: color.adjust(colors.$window_bg_color, $lightness: -3%); } &.altgr { diff --git a/nixosModules/ags/config/widgets/on-screen-keyboard/gesture.ts b/nixosModules/ags/config/widgets/on-screen-keyboard/gesture.ts new file mode 100644 index 00000000..6e08d351 --- /dev/null +++ b/nixosModules/ags/config/widgets/on-screen-keyboard/gesture.ts @@ -0,0 +1,194 @@ +import { execAsync } from 'astal'; +import { Gtk } from 'astal/gtk3'; + +import { hyprMessage } from '../../lib'; + +import OskWindow from './osk-window'; + + +const KEY_N = 249; +const HIDDEN_MARGIN = 340; + +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.get_child().css = `margin-bottom: -${HIDDEN_MARGIN}px;`; + + let signals = [] as number[]; + + window.setVisible = (state: boolean) => { + if (state) { + window.setSlideDown(); + + window.get_child().css = ` + transition: margin-bottom 0.7s cubic-bezier(0.36, 0, 0.66, -0.56); + margin-bottom: 0px; + `; + } + else { + releaseAllKeys(); + window.setSlideUp(); + + window.get_child().css = ` + transition: margin-bottom 0.7s cubic-bezier(0.36, 0, 0.66, -0.56); + margin-bottom: -${HIDDEN_MARGIN}px; + `; + } + }; + + window.killGestureSigs = () => { + signals.forEach((id) => { + gesture.disconnect(id); + }); + signals = []; + window.startY = null; + }; + + window.setSlideUp = () => { + window.killGestureSigs(); + + // Begin drag + signals.push( + gesture.connect('drag-begin', () => { + hyprMessage('j/cursorpos').then((out) => { + window.startY = JSON.parse(out).y; + }); + }), + ); + + // Update drag + signals.push( + gesture.connect('drag-update', () => { + hyprMessage('j/cursorpos').then((out) => { + if (!window.startY) { + return; + } + + const currentY = JSON.parse(out).y; + const offset = window.startY - currentY; + + if (offset < 0) { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: -${HIDDEN_MARGIN}px; + `; + + return; + } + + window.get_child().css = ` + margin-bottom: ${offset - HIDDEN_MARGIN}px; + `; + }); + }), + ); + + // End drag + signals.push( + gesture.connect('drag-end', () => { + hyprMessage('j/cursorpos').then((out) => { + if (!window.startY) { + return; + } + + const currentY = JSON.parse(out).y; + const offset = window.startY - currentY; + + if (offset > HIDDEN_MARGIN) { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: 0px; + `; + window.setVisible(true); + } + else { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: -${HIDDEN_MARGIN}px; + `; + } + }); + }), + ); + }; + + window.setSlideDown = () => { + window.killGestureSigs(); + + // Begin drag + signals.push( + gesture.connect('drag-begin', () => { + hyprMessage('j/cursorpos').then((out) => { + window.startY = JSON.parse(out).y; + }); + }), + ); + + // Update drag + signals.push( + gesture.connect('drag-update', () => { + hyprMessage('j/cursorpos').then((out) => { + if (!window.startY) { + return; + } + + const currentY = JSON.parse(out).y; + const offset = window.startY - currentY; + + if (offset > 0) { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: 0px; + `; + + return; + } + + window.get_child().css = ` + margin-bottom: ${offset}px; + `; + }); + }), + ); + + // End drag + signals.push( + gesture.connect('drag-end', () => { + hyprMessage('j/cursorpos').then((out) => { + if (!window.startY) { + return; + } + + const currentY = JSON.parse(out).y; + const offset = window.startY - currentY; + + if (offset < -(HIDDEN_MARGIN * 2 / 3)) { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: -${HIDDEN_MARGIN}px; + `; + + window.setVisible(false); + } + else { + window.get_child().css = ` + transition: margin-bottom 0.5s ease-in-out; + margin-bottom: 0px; + `; + } + }); + }), + ); + }; + + return window; +}; diff --git a/nixosModules/ags/v1/config/ts/on-screen-keyboard/keyboard-layouts.ts b/nixosModules/ags/config/widgets/on-screen-keyboard/keyboard-layouts.ts similarity index 100% rename from nixosModules/ags/v1/config/ts/on-screen-keyboard/keyboard-layouts.ts rename to nixosModules/ags/config/widgets/on-screen-keyboard/keyboard-layouts.ts diff --git a/nixosModules/ags/config/widgets/on-screen-keyboard/keyboard.tsx b/nixosModules/ags/config/widgets/on-screen-keyboard/keyboard.tsx new file mode 100644 index 00000000..61fc7548 --- /dev/null +++ b/nixosModules/ags/config/widgets/on-screen-keyboard/keyboard.tsx @@ -0,0 +1,89 @@ +import { Gtk, Widget } from 'astal/gtk3'; + +import Separator from '../misc/separator'; + +import Key from './keys'; + +import { defaultOskLayout, oskLayouts } from './keyboard-layouts'; + +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; + +export default (): Widget.Box => ( + + + + {...keyboardJson.keys.map((row, rowIndex) => { + const keys = [] as Widget.Box[]; + + row.forEach((key, keyIndex) => { + if (keyIndex < L_KEY_PER_ROW[rowIndex]) { + keys.push(Key(key)); + } + }); + + return ( + + + + + {...keys} + + + + + ); + })} + + + + + + + {...keyboardJson.keys.map((row, rowIndex) => { + const keys = [] as Widget.Box[]; + + row.forEach((key, keyIndex) => { + if (keyIndex >= L_KEY_PER_ROW[rowIndex]) { + keys.push(Key(key)); + } + }); + + return ( + + + + {...keys} + + + + + ); + })} + + + +) as Widget.Box; diff --git a/nixosModules/ags/config/widgets/on-screen-keyboard/keys.tsx b/nixosModules/ags/config/widgets/on-screen-keyboard/keys.tsx new file mode 100644 index 00000000..6bf56a33 --- /dev/null +++ b/nixosModules/ags/config/widgets/on-screen-keyboard/keys.tsx @@ -0,0 +1,261 @@ +import { execAsync, Variable } from 'astal'; +import { Gdk, Gtk, Widget } from 'astal/gtk3'; + +import Brightness from '../../services/brightness'; + +import Separator from '../misc/separator'; + +/* Types */ +interface Key { + keytype: string + label: string + labelShift?: string + labelAltGr?: string + shape: string + keycode: number +} + + +const display = Gdk.Display.get_default(); +const brightness = Brightness.get_default(); + +const SPACING = 4; +const LSHIFT_CODE = 42; +const LALT_CODE = 56; +const LCTRL_CODE = 29; + +// 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('notify::caps-level', (_, state) => { + Caps.set(state); +}); + +// Assume both shifts are the same for key.labelShift +const LShift = Variable(false); +const RShift = Variable(false); + +const Shift = Variable(false); + +LShift.subscribe(() => { + Shift.set(LShift.get() || RShift.get()); +}); +RShift.subscribe(() => { + Shift.set(LShift.get() || RShift.get()); +}); + + +const ModKey = (key: Key) => { + let Mod: Variable; + + 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 = ( +