mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-14 18:04:48 +00:00
Compare commits
1 Commits
fix/tui-se
...
oc-basecod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73a4f5a654 |
3
.github/workflows/publish.yml
vendored
3
.github/workflows/publish.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
||||
- build-cli
|
||||
- version
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
if: github.repository == 'anomalyco/opencode'
|
||||
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
|
||||
env:
|
||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -591,6 +591,7 @@ jobs:
|
||||
path: packages/opencode/dist
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
if: github.ref_name != 'beta'
|
||||
with:
|
||||
name: opencode-cli-signed-windows
|
||||
path: packages/opencode/dist
|
||||
|
||||
@@ -59,6 +59,7 @@ import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||
import { FormatError, FormatUnknownError } from "@/cli/error"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
@@ -310,7 +311,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
// - Ctrl+C copies and dismisses selection
|
||||
// - Esc dismisses selection
|
||||
// - Most other key input dismisses selection and is passed through
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
if (Keybind.matchParsedKey("ctrl+c", evt)) {
|
||||
if (!Selection.copy(renderer, toast)) {
|
||||
renderer.clearSelection()
|
||||
return
|
||||
|
||||
@@ -47,7 +47,7 @@ export function DialogMcp() {
|
||||
|
||||
const keybinds = createMemo(() => [
|
||||
{
|
||||
keybind: Keybind.parse("space")[0],
|
||||
keybind: Keybind.parseOne("space"),
|
||||
title: "toggle",
|
||||
onTrigger: async (option: DialogSelectOption<string>) => {
|
||||
// Prevent toggling while an operation is already in progress
|
||||
|
||||
@@ -162,7 +162,7 @@ export function DialogSessionList() {
|
||||
},
|
||||
},
|
||||
{
|
||||
keybind: Keybind.parse("ctrl+w")[0],
|
||||
keybind: Keybind.parseOne("ctrl+w"),
|
||||
title: "new workspace",
|
||||
side: "right",
|
||||
disabled: !Flag.OPENCODE_EXPERIMENTAL_WORKSPACES,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createSignal } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
import { win32FlushInputBuffer } from "../win32"
|
||||
import { getScrollAcceleration } from "../util/scroll"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
|
||||
export function ErrorComponent(props: {
|
||||
error: Error
|
||||
@@ -25,7 +26,7 @@ export function ErrorComponent(props: {
|
||||
}
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.ctrl && evt.name === "c") {
|
||||
if (Keybind.matchParsedKey("ctrl+c", evt)) {
|
||||
handleExit()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,16 +31,6 @@ import { batch, createEffect, on } from "solid-js"
|
||||
import { Log } from "@/util/log"
|
||||
import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
|
||||
|
||||
type SessionDiffSummary = Pick<Snapshot.FileDiff, "file" | "additions" | "deletions">
|
||||
|
||||
function summarizeDiff(diff?: Snapshot.FileDiff[]): SessionDiffSummary[] {
|
||||
return (diff ?? []).map((item) => ({
|
||||
file: item.file,
|
||||
additions: item.additions,
|
||||
deletions: item.deletions,
|
||||
}))
|
||||
}
|
||||
|
||||
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
name: "Sync",
|
||||
init: () => {
|
||||
@@ -65,7 +55,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
[sessionID: string]: SessionStatus
|
||||
}
|
||||
session_diff: {
|
||||
[sessionID: string]: SessionDiffSummary[]
|
||||
[sessionID: string]: Snapshot.FileDiff[]
|
||||
}
|
||||
todo: {
|
||||
[sessionID: string]: Todo[]
|
||||
@@ -203,7 +193,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
break
|
||||
|
||||
case "session.diff":
|
||||
setStore("session_diff", event.properties.sessionID, summarizeDiff(event.properties.diff))
|
||||
setStore("session_diff", event.properties.sessionID, event.properties.diff)
|
||||
break
|
||||
|
||||
case "session.deleted": {
|
||||
@@ -513,7 +503,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
||||
for (const message of messages.data!) {
|
||||
draft.part[message.info.id] = message.parts
|
||||
}
|
||||
draft.session_diff[sessionID] = summarizeDiff(diff.data)
|
||||
draft.session_diff[sessionID] = diff.data ?? []
|
||||
}),
|
||||
)
|
||||
fullSyncedSessions.add(sessionID)
|
||||
|
||||
@@ -195,8 +195,8 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
||||
useKeyboard((evt) => {
|
||||
setStore("input", "keyboard")
|
||||
|
||||
if (evt.name === "up" || (evt.ctrl && evt.name === "p")) move(-1)
|
||||
if (evt.name === "down" || (evt.ctrl && evt.name === "n")) move(1)
|
||||
if (evt.name === "up" || Keybind.matchParsedKey("ctrl+p", evt)) move(-1)
|
||||
if (evt.name === "down" || Keybind.matchParsedKey("ctrl+n", evt)) move(1)
|
||||
if (evt.name === "pageup") move(-10)
|
||||
if (evt.name === "pagedown") move(10)
|
||||
if (evt.name === "home") moveTo(0)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStore } from "solid-js/store"
|
||||
import { useToast } from "./toast"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { Selection } from "@tui/util/selection"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
|
||||
export function Dialog(
|
||||
props: ParentProps<{
|
||||
@@ -72,12 +73,13 @@ function init() {
|
||||
})
|
||||
|
||||
const renderer = useRenderer()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (store.stack.length === 0) return
|
||||
if (evt.defaultPrevented) return
|
||||
if ((evt.name === "escape" || (evt.ctrl && evt.name === "c")) && renderer.getSelection()?.getSelectedText()) return
|
||||
if (evt.name === "escape" || (evt.ctrl && evt.name === "c")) {
|
||||
const isCtrlC = Keybind.matchParsedKey("ctrl+c", evt)
|
||||
|
||||
if ((evt.name === "escape" || isCtrlC) && renderer.getSelection()?.getSelectedText()) return
|
||||
if (evt.name === "escape" || isCtrlC) {
|
||||
if (renderer.getSelection()) {
|
||||
renderer.clearSelection()
|
||||
}
|
||||
|
||||
@@ -6,15 +6,70 @@ export namespace Keybind {
|
||||
* Keybind info derived from OpenTUI's ParsedKey with our custom `leader` field.
|
||||
* This ensures type compatibility and catches missing fields at compile time.
|
||||
*/
|
||||
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super"> & {
|
||||
export type Info = Pick<ParsedKey, "name" | "ctrl" | "meta" | "shift" | "super" | "baseCode"> & {
|
||||
leader: boolean // our custom field
|
||||
}
|
||||
|
||||
function getBaseCodeName(baseCode: number | undefined): string | undefined {
|
||||
if (baseCode === undefined || baseCode < 32 || baseCode === 127) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const name = String.fromCodePoint(baseCode)
|
||||
|
||||
if (name.length === 1 && name >= "A" && name <= "Z") {
|
||||
return name.toLowerCase()
|
||||
}
|
||||
|
||||
return name
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function match(a: Info | undefined, b: Info): boolean {
|
||||
if (!a) return false
|
||||
const normalizedA = { ...a, super: a.super ?? false }
|
||||
const normalizedB = { ...b, super: b.super ?? false }
|
||||
return isDeepEqual(normalizedA, normalizedB)
|
||||
if (isDeepEqual(normalizedA, normalizedB)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const modifiersA = {
|
||||
ctrl: normalizedA.ctrl,
|
||||
meta: normalizedA.meta,
|
||||
shift: normalizedA.shift,
|
||||
super: normalizedA.super,
|
||||
leader: normalizedA.leader,
|
||||
}
|
||||
const modifiersB = {
|
||||
ctrl: normalizedB.ctrl,
|
||||
meta: normalizedB.meta,
|
||||
shift: normalizedB.shift,
|
||||
super: normalizedB.super,
|
||||
leader: normalizedB.leader,
|
||||
}
|
||||
|
||||
if (!isDeepEqual(modifiersA, modifiersB)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
normalizedA.name === normalizedB.name ||
|
||||
getBaseCodeName(normalizedA.baseCode) === normalizedB.name ||
|
||||
getBaseCodeName(normalizedB.baseCode) === normalizedA.name
|
||||
)
|
||||
}
|
||||
|
||||
export function parseOne(key: string): Info {
|
||||
const parsed = parse(key)
|
||||
|
||||
if (parsed.length !== 1) {
|
||||
throw new Error(`Expected exactly one keybind, got ${parsed.length}: ${key}`)
|
||||
}
|
||||
|
||||
return parsed[0]!
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,10 +83,23 @@ export namespace Keybind {
|
||||
meta: key.meta,
|
||||
shift: key.shift,
|
||||
super: key.super ?? false,
|
||||
baseCode: key.baseCode,
|
||||
leader,
|
||||
}
|
||||
}
|
||||
|
||||
export function matchParsedKey(binding: Info | string | undefined, key: ParsedKey, leader = false): boolean {
|
||||
const bindings = typeof binding === "string" ? parse(binding) : binding ? [binding] : []
|
||||
|
||||
if (!bindings.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parsed = fromParsedKey(key, leader)
|
||||
|
||||
return bindings.some((item) => match(item, parsed))
|
||||
}
|
||||
|
||||
export function toString(info: Info | undefined): string {
|
||||
if (!info) return ""
|
||||
const parts: string[] = []
|
||||
|
||||
@@ -10,106 +10,59 @@ export namespace Message {
|
||||
})),
|
||||
)
|
||||
|
||||
export class Source extends Schema.Class<Source>("Message.Source")({
|
||||
start: Schema.Number,
|
||||
end: Schema.Number,
|
||||
text: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class FileAttachment extends Schema.Class<FileAttachment>("Message.File.Attachment")({
|
||||
uri: Schema.String,
|
||||
export class File extends Schema.Class<File>("Message.File")({
|
||||
url: Schema.String,
|
||||
mime: Schema.String,
|
||||
name: Schema.String.pipe(Schema.optional),
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
source: Source.pipe(Schema.optional),
|
||||
}) {
|
||||
static create(url: string) {
|
||||
return new FileAttachment({
|
||||
uri: url,
|
||||
return new File({
|
||||
url,
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentAttachment extends Schema.Class<AgentAttachment>("Message.Agent.Attachment")({
|
||||
name: Schema.String,
|
||||
source: Source.pipe(Schema.optional),
|
||||
export class UserContent extends Schema.Class<UserContent>("Message.User.Content")({
|
||||
text: Schema.String,
|
||||
synthetic: Schema.Boolean.pipe(Schema.optional),
|
||||
agent: Schema.String.pipe(Schema.optional),
|
||||
files: Schema.Array(File).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export class User extends Schema.Class<User>("Message.User")({
|
||||
id: ID,
|
||||
type: Schema.Literal("user"),
|
||||
text: Schema.String,
|
||||
files: Schema.Array(FileAttachment).pipe(Schema.optional),
|
||||
agents: Schema.Array(AgentAttachment).pipe(Schema.optional),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
content: UserContent,
|
||||
}) {
|
||||
static create(input: { text: User["text"]; files?: User["files"]; agents?: User["agents"] }) {
|
||||
static create(content: Schema.Schema.Type<typeof UserContent>) {
|
||||
const msg = new User({
|
||||
id: ID.create(),
|
||||
type: "user",
|
||||
...input,
|
||||
time: {
|
||||
created: Effect.runSync(DateTime.now),
|
||||
},
|
||||
content,
|
||||
})
|
||||
return msg
|
||||
}
|
||||
|
||||
static file(url: string) {
|
||||
return new File({
|
||||
url,
|
||||
mime: "text/plain",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class Synthetic extends Schema.Class<Synthetic>("Message.Synthetic")({
|
||||
id: ID,
|
||||
type: Schema.Literal("synthetic"),
|
||||
text: Schema.String,
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class Request extends Schema.Class<Request>("Message.Request")({
|
||||
id: ID,
|
||||
type: Schema.Literal("start"),
|
||||
model: Schema.Struct({
|
||||
id: Schema.String,
|
||||
providerID: Schema.String,
|
||||
variant: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class Text extends Schema.Class<Text>("Message.Text")({
|
||||
id: ID,
|
||||
type: Schema.Literal("text"),
|
||||
text: Schema.String,
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
completed: Schema.DateTimeUtc.pipe(Schema.optional),
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export class Complete extends Schema.Class<Complete>("Message.Complete")({
|
||||
id: ID,
|
||||
type: Schema.Literal("complete"),
|
||||
time: Schema.Struct({
|
||||
created: Schema.DateTimeUtc,
|
||||
}),
|
||||
cost: Schema.Number,
|
||||
tokens: Schema.Struct({
|
||||
total: Schema.Number,
|
||||
input: Schema.Number,
|
||||
output: Schema.Number,
|
||||
reasoning: Schema.Number,
|
||||
cache: Schema.Struct({
|
||||
read: Schema.Number,
|
||||
write: Schema.Number,
|
||||
}),
|
||||
}),
|
||||
}) {}
|
||||
|
||||
export const Info = Schema.Union([User, Text])
|
||||
export type Info = Schema.Schema.Type<typeof Info>
|
||||
export namespace User {}
|
||||
}
|
||||
|
||||
const msg = Message.User.create({
|
||||
text: "Hello world",
|
||||
files: [Message.File.create("file://example.com/file.txt")],
|
||||
})
|
||||
|
||||
console.log(JSON.stringify(msg, null, 2))
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Context, Layer, Schema, Effect } from "effect"
|
||||
import { Message } from "./message"
|
||||
import { Struct } from "effect"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import { Session } from "@/session"
|
||||
import { SessionID } from "@/session/schema"
|
||||
|
||||
export namespace SessionV2 {
|
||||
export const ID = SessionID
|
||||
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
export class PromptInput extends Schema.Class<PromptInput>("Session.PromptInput")({
|
||||
...Struct.omit(Message.User.fields, ["time", "type"]),
|
||||
id: Schema.optionalKey(Message.ID),
|
||||
sessionID: SessionV2.ID,
|
||||
}) {}
|
||||
|
||||
export class CreateInput extends Schema.Class<CreateInput>("Session.CreateInput")({
|
||||
id: Schema.optionalKey(SessionV2.ID),
|
||||
}) {}
|
||||
|
||||
export class Info extends Schema.Class<Info>("Session.Info")({
|
||||
id: SessionV2.ID,
|
||||
model: Schema.Struct({
|
||||
id: Schema.String,
|
||||
providerID: Schema.String,
|
||||
modelID: Schema.String,
|
||||
}).pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export interface Interface {
|
||||
fromID: (id: SessionV2.ID) => Effect.Effect<Info>
|
||||
create: (input: CreateInput) => Effect.Effect<Info>
|
||||
prompt: (input: PromptInput) => Effect.Effect<Message.User>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("Session.Service") {}
|
||||
|
||||
export const layer = Layer.effect(Service)(
|
||||
Effect.gen(function* () {
|
||||
const session = yield* Session.Service
|
||||
|
||||
const create: Interface["create"] = Effect.fn("Session.create")(function* (input) {
|
||||
throw new Error("Not implemented")
|
||||
})
|
||||
|
||||
const prompt: Interface["prompt"] = Effect.fn("Session.prompt")(function* (input) {
|
||||
throw new Error("Not implemented")
|
||||
})
|
||||
|
||||
const fromID: Interface["fromID"] = Effect.fn("Session.fromID")(function* (id) {
|
||||
const match = yield* session.get(id)
|
||||
return fromV1(match)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
create,
|
||||
prompt,
|
||||
fromID,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
function fromV1(input: Session.Info): Info {
|
||||
return new Info({
|
||||
id: SessionV2.ID.make(input.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,6 @@ describe("SyncProvider", () => {
|
||||
expect(sync.data.message.ses_1[0]?.id).toBe("msg_1")
|
||||
expect(sync.data.part.msg_1[0]).toMatchObject({ type: "text", text: "part-ws_a" })
|
||||
expect(sync.data.session_diff.ses_1[0]?.file).toBe("ws_a.ts")
|
||||
expect(sync.data.session_diff.ses_1[0]).not.toHaveProperty("patch")
|
||||
|
||||
log.length = 0
|
||||
project.workspace.set("ws_b")
|
||||
@@ -286,7 +285,6 @@ describe("SyncProvider", () => {
|
||||
expect(sync.data.message.ses_1[0]?.id).toBe("msg_1")
|
||||
expect(sync.data.part.msg_1[0]).toMatchObject({ type: "text", text: "part-ws_b" })
|
||||
expect(sync.data.session_diff.ses_1[0]?.file).toBe("ws_b.ts")
|
||||
expect(sync.data.session_diff.ses_1[0]).not.toHaveProperty("patch")
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
||||
@@ -162,6 +162,24 @@ describe("Keybind.match", () => {
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should match ctrl shortcuts by baseCode from alternate layouts", () => {
|
||||
const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
|
||||
const b: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "ㅊ", baseCode: 99 }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should still match the reported character when baseCode is also present", () => {
|
||||
const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "ㅊ" }
|
||||
const b: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "ㅊ", baseCode: 99 }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should not match a different shortcut just because baseCode exists", () => {
|
||||
const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "x" }
|
||||
const b: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "ㅊ", baseCode: 99 }
|
||||
expect(Keybind.match(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
test("should match super+shift combination", () => {
|
||||
const a: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
|
||||
const b: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
|
||||
@@ -419,3 +437,68 @@ describe("Keybind.parse", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Keybind.parseOne", () => {
|
||||
test("should parse a single keybind", () => {
|
||||
expect(Keybind.parseOne("ctrl+x")).toEqual({
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
leader: false,
|
||||
name: "x",
|
||||
})
|
||||
})
|
||||
|
||||
test("should reject multiple keybinds", () => {
|
||||
expect(() => Keybind.parseOne("ctrl+x,ctrl+y")).toThrow("Expected exactly one keybind")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Keybind.fromParsedKey", () => {
|
||||
test("should preserve baseCode from ParsedKey", () => {
|
||||
const result = Keybind.fromParsedKey({
|
||||
name: "ㅊ",
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
option: false,
|
||||
number: false,
|
||||
sequence: "ㅊ",
|
||||
raw: "\x1b[12618::99;5u",
|
||||
eventType: "press",
|
||||
source: "kitty",
|
||||
baseCode: 99,
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "ㅊ",
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
super: false,
|
||||
leader: false,
|
||||
baseCode: 99,
|
||||
})
|
||||
})
|
||||
|
||||
test("should ignore leader unless explicitly requested", () => {
|
||||
const key = {
|
||||
name: "ㅊ",
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
option: false,
|
||||
number: false,
|
||||
sequence: "ㅊ",
|
||||
raw: "\x1b[12618::99;5u",
|
||||
eventType: "press" as const,
|
||||
source: "kitty" as const,
|
||||
baseCode: 99,
|
||||
}
|
||||
|
||||
expect(Keybind.matchParsedKey("ctrl+c", key)).toBe(true)
|
||||
expect(Keybind.matchParsedKey("ctrl+x,ctrl+c", key)).toBe(true)
|
||||
expect(Keybind.matchParsedKey("ctrl+x,ctrl+y", key)).toBe(false)
|
||||
expect(Keybind.matchParsedKey("ctrl+c", key, true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user