feat(ags): make custom onscreen keyboard

This commit is contained in:
matt1432 2023-11-19 15:00:29 -05:00
parent 8f779d37c5
commit d56edd5484
14 changed files with 589 additions and 1 deletions

View file

@ -6,6 +6,7 @@
nixpkgs.overlays = [
(import ./blueberry.nix)
(import ./squeekboard.nix)
neovim-flake.overlay
(final: prev: {

View file

@ -0,0 +1,41 @@
From 10be06ebc72ec8f516003577dabcedb60fe05982 Mon Sep 17 00:00:00 2001
From: matt1432 <matt@nelim.org>
Date: Sat, 18 Nov 2023 20:26:50 -0500
Subject: [PATCH] remove panel
---
src/panel.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/panel.c b/src/panel.c
index a9942c8..333727e 100644
--- a/src/panel.c
+++ b/src/panel.c
@@ -75,13 +75,13 @@ panel_manager_request_widget (struct panel_manager *self, struct wl_output *outp
PHOSH_TYPE_LAYER_SURFACE,
"layer-shell", squeek_wayland->layer_shell,
"wl-output", output,
- "height", height,
+ "height", 1,
"anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM
| ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT
| ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT,
"layer", ZWLR_LAYER_SHELL_V1_LAYER_TOP,
"kbd-interactivity", FALSE,
- "exclusive-zone", height,
+
"namespace", "osk",
NULL
);
@@ -100,7 +100,7 @@ panel_manager_request_widget (struct panel_manager *self, struct wl_output *outp
gtk_window_set_icon_name (GTK_WINDOW(self->window), "squeekboard");
gtk_window_set_keep_above (GTK_WINDOW(self->window), TRUE);
} else {
- panel_manager_resize(self, height);
+ panel_manager_resize(self, 1);
}
if (!self->widget) {
--
2.42.0

View file

@ -0,0 +1,7 @@
final: prev: {
squeekboard = prev.squeekboard.overrideAttrs (o: {
patches = (o.patches or [ ]) ++ [
./patches/remove-panel.patch
];
});
}

View file

@ -39,6 +39,8 @@
"imports": "readonly",
"print": "readonly",
"console": "readonly",
"logError": "readonly"
"logError": "readonly",
"setTimeout": "readonly",
"setInterval": "readonly"
}
}

View file

@ -2,6 +2,7 @@ extends: stylelint-config-standard-scss
ignoreFiles: "**/*.js"
rules:
selector-type-no-unknown: null
selector-class-pattern: null
declaration-empty-line-before: null
no-descending-specificity: null
selector-pseudo-class-no-unknown: null

View file

@ -30,6 +30,7 @@ export default {
'calendar': 500,
'notification-center': 500,
'osd': 500,
'osk': 500,
'overview': 500,
'powermenu': 500,
'quick-settings': 500,
@ -43,6 +44,7 @@ export default {
Calendar(),
NotifCenter(),
OSD(),
OSK(),
Overview(),
Powermenu(),
QSettings(),

View file

@ -0,0 +1,99 @@
import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
import Gtk from 'gi://Gtk';
export default window => {
const gesture = Gtk.GestureDrag.new(window);
gesture.signals = [];
window.killGestureSigs = () => {
gesture.signals.forEach(id => gesture.disconnect(id));
gesture.signals = [];
};
window.setSlideUp = () => {
console.log(window.get_allocated_height());
window.killGestureSigs();
// Begin drag
gesture.signals.push(
gesture.connect('drag-begin', () => {
Hyprland.sendMessage('j/cursorpos').then(out => {
gesture.startY = JSON.parse(out).y;
});
}),
);
// Update drag
gesture.signals.push(
gesture.connect('drag-update', () => {
Hyprland.sendMessage('j/cursorpos').then(out => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset < 0)
return;
window.child.setCss(`
margin-bottom: ${offset}px;
`);
});
}),
);
// End drag
gesture.signals.push(
gesture.connect('drag-end', () => {
window.child.setCss(`
transition: margin-bottom 0.5s ease-in-out;
margin-bottom: 0px;
`);
}),
);
};
window.setSlideDown = () => {
window.killGestureSigs();
// Begin drag
gesture.signals.push(
gesture.connect('drag-begin', () => {
Hyprland.sendMessage('j/cursorpos').then(out => {
gesture.startY = JSON.parse(out).y;
});
}),
);
// Update drag
gesture.signals.push(
gesture.connect('drag-update', () => {
Hyprland.sendMessage('j/cursorpos').then(out => {
const currentY = JSON.parse(out).y;
const offset = gesture.startY - currentY;
if (offset > 0)
return;
window.child.setCss(`
margin-bottom: ${offset}px;
`);
});
}),
);
// End drag
gesture.signals.push(
gesture.connect('drag-end', () => {
window.child.setCss(`
transition: margin-bottom 0.5s ease-in-out;
margin-bottom: 0px;
`);
}),
);
};
window.setSlideDown();
return window;
};

View file

@ -0,0 +1,104 @@
// 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,56 @@
import { Box, CenterBox } from 'resource:///com/github/Aylur/ags/widget.js';
import Separator from '../misc/separator.js';
import Key from './keys.js';
import { defaultOskLayout, oskLayouts } from './keyboard-layouts.js';
const keyboardLayout = defaultOskLayout;
const keyboardJson = oskLayouts[keyboardLayout];
const L_KEY_PER_ROW = [8, 7, 6, 6, 6, 4];
export default () => CenterBox({
class_name: 'osk',
hexpand: true,
start_widget: Box({
class_name: 'left-side side',
hpack: 'start',
vertical: true,
children: keyboardJson.keys.map((row, rowIndex) => Box({
vertical: true,
children: [
Box({
class_name: 'row',
children: row.map((key, keyIndex) => {
return keyIndex < L_KEY_PER_ROW[rowIndex] ? Key(key) : null;
}),
}),
Separator(4, { vertical: true }),
],
})),
}),
center_widget: Box({
hpack: 'center',
}),
end_widget: Box({
class_name: 'right-side side',
hpack: 'end',
vertical: true,
children: keyboardJson.keys.map((row, rowIndex) => Box({
vertical: true,
children: [
Box({
hpack: 'end',
class_name: 'row',
children: row.map((key, keyIndex) => {
return keyIndex >= L_KEY_PER_ROW[rowIndex] ? Key(key) : null;
}),
}),
Separator(4, { vertical: true }),
],
})),
}),
});

View file

@ -0,0 +1,171 @@
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
import Brightness from '../../services/brightness.js';
import { Box, EventBox, Label } from 'resource:///com/github/Aylur/ags/widget.js';
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import Gtk from 'gi://Gtk';
import Separator from '../misc/separator.js';
// 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.value = 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.value = LShift.value || RShift.value);
RShift.connect('changed', () => Shift.value = LShift.value || RShift.value);
export default key => {
if (key.keytype === 'normal')
return RegularKey(key);
else
return ModKey(key);
};
const ModKey = key => {
let Mod;
if (key.label === 'Super')
Mod = Super;
// Differentiate left and right mods
else if (key.label === 'Shift' && key.keycode === 42)
Mod = LShift;
else if (key.label === 'Alt' && key.keycode === 56)
Mod = LAlt;
else if (key.label === 'Ctrl' && key.keycode === 29)
Mod = LCtrl;
else if (key.label === 'Shift')
Mod = RShift;
else if (key.label === 'AltGr')
Mod = AltGr;
else if (key.label === 'Ctrl')
Mod = RCtrl;
const button = EventBox({
cursor: 'pointer',
class_name: 'key',
onPrimaryClickRelease: self => {
console.log('mod toggled');
execAsync(`ydotool key ${key.keycode}:${Mod.value ? 0 : 1}`);
self.child.toggleClassName('active', !Mod.value);
Mod.value = !Mod.value;
},
connections: [[NormalClick, self => {
Mod.value = false;
self.child.toggleClassName('active', false);
execAsync(`ydotool key ${key.keycode}:0`);
}]],
child: Label({
class_name: `mod ${key.label}`,
label: key.label,
}),
});
return Box({
children: [
button,
Separator(4),
],
});
};
const RegularKey = key => {
const widget = EventBox({
cursor: 'pointer',
class_name: 'key',
child: Label({
class_name: `normal ${key.label}`,
label: key.label,
connections: [
[Shift, self => {
if (!key.labelShift)
return;
self.label = Shift.value ? key.labelShift : key.label;
}],
[Caps, self => {
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;
}],
[AltGr, self => {
if (!key.labelAltGr)
return;
self.toggleClassName('altgr', AltGr.value);
self.label = AltGr.value ? key.labelAltGr : key.label;
}],
],
}),
});
const gesture = Gtk.GestureLongPress.new(widget);
gesture.delay_factor = 1.0;
// Long press
widget.connectTo(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.connectTo(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.value = true;
}, 'cancelled');
return Box({
children: [
widget,
Separator(4),
],
});
};

View file

@ -0,0 +1,29 @@
import { execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
import Gesture from './gesture.js';
import Keyboard from './keyboard.js';
import PopupWindow from '../misc/popup.js';
// ydotool stuff
execAsync('ydotoold').catch(print);
function releaseAllKeys() {
const keycodes = Array.from(Array(249).keys());
execAsync(['ydotool', 'key', ...keycodes.map(keycode => `${keycode}:0`)])
.then(console.log('Released all keys'))
.catch(print);
}
// Window
export default () => Gesture(PopupWindow({
name: 'osk',
exclusivity: 'exclusive',
anchor: ['left', 'bottom', 'right'],
onClose: releaseAllKeys,
closeOnUnfocus: 'none',
connections: [[Tablet, self => {
self.visible = Tablet.oskState;
}, 'osk-toggled']],
child: Keyboard(),
}));

View file

@ -21,3 +21,4 @@ undershoot {
@import "./widgets/overview";
@import "./widgets/applauncher";
@import "./widgets/osd";
@import "./widgets/osk";

View file

@ -0,0 +1,73 @@
.osk {
margin-left: 4px;
.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

@ -53,6 +53,7 @@ in {
home.packages = with pkgs; [
# ags
ydotool
sassc
coloryou
libnotify