mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' of https://github.com/sst/opencode into dev
This commit is contained in:
commit
afcd547a16
10 changed files with 174 additions and 57 deletions
7
.github/workflows/publish.yml
vendored
7
.github/workflows/publish.yml
vendored
|
|
@ -85,7 +85,6 @@ jobs:
|
|||
|
||||
publish-tauri:
|
||||
needs: publish
|
||||
if: inputs.bump || inputs.version
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
|
@ -130,7 +129,7 @@ jobs:
|
|||
- uses: ./.github/actions/setup-bun
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
|
@ -162,7 +161,7 @@ jobs:
|
|||
|
||||
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
|
||||
- run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/truly-portable-appimage
|
||||
if: startsWith(matrix.settings.host, 'ubuntu')
|
||||
if: contains(matrix.settings.host, 'ubuntu')
|
||||
|
||||
- name: Build and upload artifacts
|
||||
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
|
||||
|
|
@ -180,7 +179,7 @@ jobs:
|
|||
with:
|
||||
projectPath: packages/tauri
|
||||
uploadWorkflowArtifacts: true
|
||||
tauriScript: ${{ (startsWith(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
|
||||
args: --target ${{ matrix.settings.target }}
|
||||
updaterJsonPreferNsis: true
|
||||
releaseId: ${{ needs.publish.outputs.releaseId }}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ export function DialogModel(props: { providerID?: string }) {
|
|||
<DialogSelect
|
||||
keybind={[
|
||||
{
|
||||
keybind: { ctrl: true, name: "a", meta: false, shift: false, leader: false },
|
||||
keybind: Keybind.parse("ctrl+a")[0],
|
||||
title: connected() ? "Connect provider" : "View all providers",
|
||||
onTrigger() {
|
||||
dialog.replace(() => <DialogProvider />)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useSync } from "@tui/context/sync"
|
|||
import { Identifier } from "@/id/id"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { Keybind } from "@/util/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||
import { useCommandDialog } from "../dialog-command"
|
||||
|
|
@ -85,7 +86,7 @@ const TEXTAREA_ACTIONS = [
|
|||
] as const
|
||||
|
||||
function mapTextareaKeybindings(
|
||||
keybinds: Record<string, { ctrl: boolean; meta: boolean; shift: boolean; leader: boolean; name: string }[]>,
|
||||
keybinds: Record<string, Keybind.Info[]>,
|
||||
action: (typeof TEXTAREA_ACTIONS)[number],
|
||||
): KeyBinding[] {
|
||||
const configKey = `input_${action.replace(/-/g, "_")}`
|
||||
|
|
@ -96,6 +97,7 @@ function mapTextareaKeybindings(
|
|||
ctrl: binding.ctrl || undefined,
|
||||
meta: binding.meta || undefined,
|
||||
shift: binding.shift || undefined,
|
||||
super: binding.super || undefined,
|
||||
action,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,21 +73,11 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex
|
|||
return store.leader
|
||||
},
|
||||
parse(evt: ParsedKey): Keybind.Info {
|
||||
if (evt.name === "\x1F")
|
||||
return {
|
||||
ctrl: true,
|
||||
name: "_",
|
||||
shift: false,
|
||||
leader: false,
|
||||
meta: false,
|
||||
}
|
||||
return {
|
||||
ctrl: evt.ctrl,
|
||||
name: evt.name,
|
||||
shift: evt.shift,
|
||||
leader: store.leader,
|
||||
meta: evt.meta,
|
||||
// Handle special case for Ctrl+Underscore (represented as \x1F)
|
||||
if (evt.name === "\x1F") {
|
||||
return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader)
|
||||
}
|
||||
return Keybind.fromParsedKey(evt, store.leader)
|
||||
},
|
||||
match(key: keyof KeybindsConfig, evt: ParsedKey) {
|
||||
const keybind = keybinds()[key]
|
||||
|
|
|
|||
|
|
@ -512,8 +512,8 @@ export namespace Config {
|
|||
input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"),
|
||||
input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"),
|
||||
input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"),
|
||||
input_undo: z.string().optional().default("ctrl+-").describe("Undo in input"),
|
||||
input_redo: z.string().optional().default("ctrl+.").describe("Redo in input"),
|
||||
input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"),
|
||||
input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"),
|
||||
input_word_forward: z
|
||||
.string()
|
||||
.optional()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,35 @@
|
|||
import { isDeepEqual } from "remeda"
|
||||
import type { ParsedKey } from "@opentui/core"
|
||||
|
||||
export namespace Keybind {
|
||||
export type Info = {
|
||||
ctrl: boolean
|
||||
meta: boolean
|
||||
shift: boolean
|
||||
leader: boolean
|
||||
name: string
|
||||
/**
|
||||
* 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"> & {
|
||||
leader: boolean // our custom field
|
||||
}
|
||||
|
||||
export function match(a: Info, b: Info): boolean {
|
||||
return isDeepEqual(a, b)
|
||||
// Normalize super field (undefined and false are equivalent)
|
||||
const normalizedA = { ...a, super: a.super ?? false }
|
||||
const normalizedB = { ...b, super: b.super ?? false }
|
||||
return isDeepEqual(normalizedA, normalizedB)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenTUI's ParsedKey to our Keybind.Info format.
|
||||
* This helper ensures all required fields are present and avoids manual object creation.
|
||||
*/
|
||||
export function fromParsedKey(key: ParsedKey, leader = false): Info {
|
||||
return {
|
||||
name: key.name,
|
||||
ctrl: key.ctrl,
|
||||
meta: key.meta,
|
||||
shift: key.shift,
|
||||
super: key.super ?? false,
|
||||
leader,
|
||||
}
|
||||
}
|
||||
|
||||
export function toString(info: Info): string {
|
||||
|
|
@ -18,6 +37,7 @@ export namespace Keybind {
|
|||
|
||||
if (info.ctrl) parts.push("ctrl")
|
||||
if (info.meta) parts.push("alt")
|
||||
if (info.super) parts.push("super")
|
||||
if (info.shift) parts.push("shift")
|
||||
if (info.name) {
|
||||
if (info.name === "delete") parts.push("del")
|
||||
|
|
@ -58,6 +78,9 @@ export namespace Keybind {
|
|||
case "option":
|
||||
info.meta = true
|
||||
break
|
||||
case "super":
|
||||
info.super = true
|
||||
break
|
||||
case "shift":
|
||||
info.shift = true
|
||||
break
|
||||
|
|
|
|||
|
|
@ -68,6 +68,31 @@ describe("Keybind.toString", () => {
|
|||
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: true, name: "" }
|
||||
expect(Keybind.toString(info)).toBe("<leader>")
|
||||
})
|
||||
|
||||
test("should convert super modifier to string", () => {
|
||||
const info: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
|
||||
expect(Keybind.toString(info)).toBe("super+z")
|
||||
})
|
||||
|
||||
test("should convert super+shift modifier to string", () => {
|
||||
const info: Keybind.Info = { ctrl: false, meta: false, shift: true, super: true, leader: false, name: "z" }
|
||||
expect(Keybind.toString(info)).toBe("super+shift+z")
|
||||
})
|
||||
|
||||
test("should handle super with ctrl modifier", () => {
|
||||
const info: Keybind.Info = { ctrl: true, meta: false, shift: false, super: true, leader: false, name: "a" }
|
||||
expect(Keybind.toString(info)).toBe("ctrl+super+a")
|
||||
})
|
||||
|
||||
test("should handle super with all modifiers", () => {
|
||||
const info: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "x" }
|
||||
expect(Keybind.toString(info)).toBe("ctrl+alt+super+shift+x")
|
||||
})
|
||||
|
||||
test("should handle undefined super field (omitted)", () => {
|
||||
const info: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
|
||||
expect(Keybind.toString(info)).toBe("ctrl+c")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Keybind.match", () => {
|
||||
|
|
@ -118,6 +143,36 @@ describe("Keybind.match", () => {
|
|||
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, leader: false, name: "a" }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should match super modifier keybinds", () => {
|
||||
const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
|
||||
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should not match super vs non-super", () => {
|
||||
const a: Keybind.Info = { ctrl: false, meta: false, shift: false, super: true, leader: false, name: "z" }
|
||||
const b: Keybind.Info = { ctrl: false, meta: false, shift: false, super: false, leader: false, name: "z" }
|
||||
expect(Keybind.match(a, b)).toBe(false)
|
||||
})
|
||||
|
||||
test("should match undefined super with false super", () => {
|
||||
const a: Keybind.Info = { ctrl: true, meta: false, shift: false, leader: false, name: "c" }
|
||||
const b: Keybind.Info = { ctrl: true, meta: false, shift: false, super: false, leader: false, name: "c" }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
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" }
|
||||
expect(Keybind.match(a, b)).toBe(true)
|
||||
})
|
||||
|
||||
test("should not match when only super differs", () => {
|
||||
const a: Keybind.Info = { ctrl: true, meta: true, shift: true, super: true, leader: false, name: "a" }
|
||||
const b: Keybind.Info = { ctrl: true, meta: true, shift: true, super: false, leader: false, name: "a" }
|
||||
expect(Keybind.match(a, b)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Keybind.parse", () => {
|
||||
|
|
@ -314,4 +369,53 @@ describe("Keybind.parse", () => {
|
|||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("should parse super modifier", () => {
|
||||
const result = Keybind.parse("super+z")
|
||||
expect(result).toEqual([
|
||||
{
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
super: true,
|
||||
leader: false,
|
||||
name: "z",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("should parse super with shift modifier", () => {
|
||||
const result = Keybind.parse("super+shift+z")
|
||||
expect(result).toEqual([
|
||||
{
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
super: true,
|
||||
leader: false,
|
||||
name: "z",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("should parse multiple keybinds with super", () => {
|
||||
const result = Keybind.parse("ctrl+-,super+z")
|
||||
expect(result).toEqual([
|
||||
{
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
leader: false,
|
||||
name: "-",
|
||||
},
|
||||
{
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
super: true,
|
||||
leader: false,
|
||||
name: "z",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7333,12 +7333,12 @@
|
|||
},
|
||||
"input_undo": {
|
||||
"description": "Undo in input",
|
||||
"default": "ctrl+-",
|
||||
"default": "ctrl+-,super+z",
|
||||
"type": "string"
|
||||
},
|
||||
"input_redo": {
|
||||
"description": "Redo in input",
|
||||
"default": "ctrl+.",
|
||||
"default": "ctrl+.,super+shift+z",
|
||||
"type": "string"
|
||||
},
|
||||
"input_word_forward": {
|
||||
|
|
|
|||
|
|
@ -307,19 +307,18 @@
|
|||
--border-weaker-focus: var(--smoke-light-alpha-6);
|
||||
--button-ghost-hover: var(--smoke-light-alpha-2);
|
||||
--button-ghost-hover2: var(--smoke-light-alpha-3);
|
||||
--avatar-background-pink: #FEEEF8;
|
||||
--avatar-background-mint: #E1FBF4 ;
|
||||
--avatar-background-orange: #FFF1E7 ;
|
||||
--avatar-background-purple: #F9F1FE;
|
||||
--avatar-background-cyan: #E7F9FB;
|
||||
--avatar-background-lime: #EEFADC;
|
||||
--avatar-text-pink: #CD1D8D;
|
||||
--avatar-text-mint: #147D6F ;
|
||||
--avatar-text-orange: #ED5F00 ;
|
||||
--avatar-text-purple: #8445BC;
|
||||
--avatar-text-cyan: #0894B3;
|
||||
--avatar-text-lime: #5D770D;
|
||||
|
||||
--avatar-background-pink: #feeef8;
|
||||
--avatar-background-mint: #e1fbf4;
|
||||
--avatar-background-orange: #fff1e7;
|
||||
--avatar-background-purple: #f9f1fe;
|
||||
--avatar-background-cyan: #e7f9fb;
|
||||
--avatar-background-lime: #eefadc;
|
||||
--avatar-text-pink: #cd1d8d;
|
||||
--avatar-text-mint: #147d6f;
|
||||
--avatar-text-orange: #ed5f00;
|
||||
--avatar-text-purple: #8445bc;
|
||||
--avatar-text-cyan: #0894b3;
|
||||
--avatar-text-lime: #5d770d;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
|
|
@ -564,18 +563,18 @@
|
|||
--border-weaker-focus: var(--smoke-dark-alpha-6);
|
||||
--button-ghost-hover: var(--smoke-dark-alpha-2);
|
||||
--button-ghost-hover2: var(--smoke-dark-alpha-3);
|
||||
--avatar-background-pink: #501B3F;
|
||||
--avatar-background-mint: #033A34;
|
||||
--avatar-background-orange: #5F2A06;
|
||||
--avatar-background-pink: #501b3f;
|
||||
--avatar-background-mint: #033a34;
|
||||
--avatar-background-orange: #5f2a06;
|
||||
--avatar-background-purple: #432155;
|
||||
--avatar-background-cyan: #0F3058;
|
||||
--avatar-background-lime: #2B3711;
|
||||
--avatar-text-pink: #E34BA9;
|
||||
--avatar-text-mint: #95F3D9 ;
|
||||
--avatar-text-orange: #FF802B ;
|
||||
--avatar-text-purple: #9D5BD2;
|
||||
--avatar-text-cyan: #369EFF;
|
||||
--avatar-text-lime: #C4F042;
|
||||
--avatar-background-cyan: #0f3058;
|
||||
--avatar-background-lime: #2b3711;
|
||||
--avatar-text-pink: #e34ba9;
|
||||
--avatar-text-mint: #95f3d9;
|
||||
--avatar-text-orange: #ff802b;
|
||||
--avatar-text-purple: #9d5bd2;
|
||||
--avatar-text-cyan: #369eff;
|
||||
--avatar-text-lime: #c4f042;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
|
|||
"input_delete_to_line_start": "ctrl+u",
|
||||
"input_backspace": "backspace,shift+backspace",
|
||||
"input_delete": "ctrl+d,delete,shift+delete",
|
||||
"input_undo": "ctrl+-,cmd+z",
|
||||
"input_redo": "ctrl+.,cmd+shift+z",
|
||||
"input_undo": "ctrl+-,super+z",
|
||||
"input_redo": "ctrl+.,super+shift+z",
|
||||
"input_word_forward": "alt+f,alt+right,ctrl+right",
|
||||
"input_word_backward": "alt+b,alt+left,ctrl+left",
|
||||
"input_select_word_forward": "alt+shift+f,alt+shift+right",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue