Skip to content

Commit

Permalink
Article WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
shiro committed Apr 3, 2024
1 parent 19ef7c9 commit 2928911
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 8 deletions.
3 changes: 2 additions & 1 deletion src/Article.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Tooltip } from "@kobalte/core";
import cn from "classnames";
import { Component, lazy } from "solid-js";
import Spoiler from "~/Spoiler";
import Icon from "~/components/Icon";
import IconText from "~/components/IconText";

Expand Down Expand Up @@ -55,7 +56,7 @@ const Article: Component<Props> = (props) => {
<ul {...props} class={cn(props.className, "list-disc pl-8")} />
),
li: (props: any) => <li {...props} />,

Spoiler: (props: any) => <Spoiler>{props.children}</Spoiler>,
Embed: (props: any) => {
if (props.url?.includes("://github.com")) {
const [s, username, projectName] = props.url.match(
Expand Down
81 changes: 81 additions & 0 deletions src/Spoiler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Collapsible, Separator } from "@kobalte/core";
import { css } from "@linaria/core";
import cn from "classnames";
import { Component, JSX } from "solid-js";
import { color } from "~/style/commonStyle";

interface Props {
children?: JSX.Element;
style?: JSX.CSSProperties;
class?: string;
}

const Spoiler: Component<Props> = (props) => {
const { children, class: $class, ...rest } = $destructure(props);

return (
<Collapsible.Root class={cn($class, "mb-8 mt-8")} {...rest}>
<div class="flex items-center">
<Collapsible.Trigger
class={cn(
"inline-flex items-center justify-between bg-colors-primary-200 pl-4 pr-4 text-center outline-none",
ToggleButton
)}>
<span class="open-text">View collapsed content</span>
<span class="close-text">Hide collapsed content</span>
</Collapsible.Trigger>
<Separator.Root class="ml-2 flex-1 border-2 border-colors-text-100a" />
</div>
<Collapsible.Content class={cn("collapsible__content", Content)}>
{children}
</Collapsible.Content>
<Separator.Root class="mt-4 flex-1 border-2 border-colors-text-100a" />
</Collapsible.Root>
);
};

const ToggleButton = css`
.open-text {
display: block;
}
.close-text {
display: none;
}
&[data-expanded] {
background-color: ${color("colors/primary-300")} !important;
.open-text {
display: none;
}
.close-text {
display: block;
}
}
`;

const Content = css`
overflow: hidden;
animation: slideUp 300ms ease-out;
&[data-expanded] {
animation: slideDown 300ms ease-out;
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--kb-collapsible-content-height);
}
}
@keyframes slideUp {
from {
height: var(--kb-collapsible-content-height);
}
to {
height: 0;
}
}
`;

export default Spoiler;
1 change: 0 additions & 1 deletion src/articles/2024-03-25-starting-a-blog/article.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ For starts I'm planning to do some articles about building static sites like thi

<Embed
url="https://github.com/shiro/blog"
name="blog"
description="My personal blog website"
/>

Expand Down
165 changes: 164 additions & 1 deletion src/articles/2024-03-30-mapping-chord-key-combos-on-linux/article.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,139 @@
Recently I realized that lots of my key combinations require pretty unhealthy hand movements, so
I added chording key combos to my life.

Chording combos refer to pressing multiple keys at the same time (or in very quick succession) to
trigger some action rather that outputing the keys. The keys still function normally when not
pressed in a chord.

While I'm not exactly sure if chording is the official name for it, I've been using this
cool [vim plugin](https://github.com/kana/vim-arpeggio) that calls it that, so that's how I'm going to refer to it.

<Embed
url="https://github.com/kana/vim-arpeggio"
description="Vim plugin: Mappings for simultaneously pressed keys."
/>

Since I developed my own software for Linux key remapping a while ago (I'll talk about it in length another time), adding the functionality there seemed like the obvious way to go.
The project is called map2 and allows for blazingly fast key-remapping and even allows
running arbirary python code to achieve even the craziest ideas.

Something similar might be doable on Windows using AutoHotkey as well.

<Embed
url="https://github.com/shiro/map2"
description="Linux input remapping for your keyboard, mouse and more!"
/>

After setting the end-goal, I started writing some quick prototype code in Python.
The idea is simple:
- define some key-combos in an array
- bind a custom python callback on all keys that appear as chord inputs
- once a chord-key is hit, set a timeout which delays the event
- if another key is pressed, clear the timeoout and, if it completes a chord, remap it

Of course I knew from the get-go that the above is just the ideal path, there's lots of edge
cases to handle here.

Here's my initial prototype code that mostly works but doesn't cover all edge cases
(it's supposed to work, not look good):

<Spoiler>
```python title="arpeggio.py"
combos = {
frozenset({"a", "b"}): "c",
frozenset({"a", "d"}): "e",
}

chord_chars = set([x for y in combos for x in y])
interval = None
stack = []

def arpeggio(mapper, writer):
is_down = {}
def fn(key, state):
global chord_chars, interval, stack
nonlocal is_down

if key not in chord_chars:
if interval is not None:
interval.cancel()
interval = None

pressed = [k for k in is_down if is_down[k]]
if pressed != []:
writer.send("".join(map(lambda k: "{"+k+" up}", pressed)))

if len(stack) > 0:
writer.send("".join(stack))
stack = []

is_down = {}
return True

if state == "down":
stack = stack + [key]

def submit():
global interval, combos, stack
nonlocal is_down

if interval is not None:
interval.cancel()
interval = None

output = combos.get(frozenset(stack), None)
if output is not None:
writer.send(output)
stack = []
else:
if stack == [key]:
is_down[key] = True
stack = []
writer.send("{"+key+" down}")
elif len(stack) == 2:
writer.send("".join(stack))
stack = []

if len(stack) == 2:
submit()
elif interval is None:
interval = setInterval(50, submit)

if state == "repeat":
if is_down.get(key):
writer.send("{"+key+" repeat}")

if state == "up":
if interval is not None:
interval.cancel()
interval = None

if is_down.get(key):
if stack != []:
writer.send("".join(stack))
stack = []

is_down[key] = False
stack = []
return True


if stack != []:
writer.send("".join(stack))
stack = []

mapper.map_fallback(fn)
```
</Spoiler>

The Python prototype was a good first step and reassured me that using chords is the way
to go - it just feels nice and reduces finger travel time by a lot.

I could have ended it there, but since this seemed like something lots of people could
benefit from, I decided to do a native implementation and support it as part of the
core map2 API. After a fun weekend, I managed to hack together someting I was satisfied
with, here's the final API:


```python title="example.py"
import map2
Expand All @@ -25,4 +158,34 @@ mapper_kbd_arp.map([";", "g"], "G")
# and so on...
```

And that's it!
Curently we only support 2-key-chords, I also decided to write the code in a way
that allows for future multi-key support.

Since map2 has a pretty nice e2e test harness, it was also possible to test lots
of edge cases systematically, getting me to a working version without ever
running the script on my keyboard - this was one of those rare movements where
test-driven-development actually worked well!
Here's a sample:

```rust title="examples/tests/chords.rs"
#[pyo3_asyncio::tokio::test]
async fn simple_chord() -> PyResult<()> {
Python::with_gil(|py| -> PyResult<()> {
let m = pytests::include_python!();

reader_send_all(py, m, "reader", &keys("{a down}{b down}{a up}{b up}"));
sleep(py, 55);
assert_eq!(writer_read_all(py, m, "writer"), keys("c"),);
sleep(py, 55);
assert_empty!(py, m, WRITER);

Ok(())
})?;
Ok(())
}
```

At the momemt of writing this, the API is not documented in the docs since I decided
to use it for a month before doing that.


6 changes: 5 additions & 1 deletion src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ export const routes: RouteDefinition[] = [
component: (p) => {
// router bug: 'name' not in 'p', update when this is fixed
const name = p.location.pathname.replace(`${config.base}/articles/`, "");
return <Article name={name} />;
return (
<div class="mb-16">
<Article name={name} />
</div>
);
},
matchFilters: {
name: (name: string) => isValidArticle(name),
Expand Down
15 changes: 11 additions & 4 deletions src/style/global.style.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
primaryFontBold,
primaryFontBoldItalic,
primaryFontItalic,
smallText,
subText,
text,
} from "~/style/commonStyle";
import "~/style/fontPreamble.style";
Expand All @@ -13,7 +15,11 @@ import "~/style/reset.scss";
import "~/style/tw.style";
// import "~/style/styleLoadOrder";
import { css } from "@linaria/core";
import { baseText, bodyTextHeight } from "~/style/textStylesTS";
import {
baseText,
bodyTextHeight,
smallTextHeight,
} from "~/style/textStylesTS";

export const globals = css`
@layer tw-base {
Expand Down Expand Up @@ -197,8 +203,8 @@ export const globals = css`
}
pre.shiki {
margin-top: 16px;
margin-bottom: 16px;
margin-top: 32px;
margin-bottom: 32px;
background-color: ${color("colors/primary-50")};
padding: 8px;
.code-title {
Expand All @@ -210,7 +216,8 @@ export const globals = css`
background-color: ${color("colors/primary-200")};
}
.code-container .line {
min-height: ${bodyTextHeight}px;
${subText};
min-height: ${smallTextHeight}px;
white-space: pre-wrap !important;
}
.language-id {
Expand Down

0 comments on commit 2928911

Please sign in to comment.