mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-05 09:22:04 +02:00
Add dynamic text replacement. Closes #223.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
forwardRef,
|
||||
useRef,
|
||||
useMemo,
|
||||
type ChangeEvent,
|
||||
type FormEvent,
|
||||
type KeyboardEvent,
|
||||
} from 'react';
|
||||
@@ -12,6 +13,11 @@ import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import { toast } from './ui/sonner';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
getTextReplaceEnabled,
|
||||
getTextReplaceMapJson,
|
||||
applyTextReplacements,
|
||||
} from '../utils/textReplace';
|
||||
|
||||
// MeshCore message size limits (empirically determined from LoRa packet constraints)
|
||||
// Direct delivery allows ~156 bytes; multi-hop requires buffer for path growth.
|
||||
@@ -139,6 +145,31 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
|
||||
[text, sending, disabled, onSend]
|
||||
);
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target;
|
||||
const raw = input.value;
|
||||
// Skip replacement during IME / dead-key composition to avoid garbling interim input
|
||||
if (!e.nativeEvent || (e.nativeEvent as InputEvent).isComposing) {
|
||||
setText(raw);
|
||||
return;
|
||||
}
|
||||
if (getTextReplaceEnabled()) {
|
||||
const result = applyTextReplacements(
|
||||
raw,
|
||||
input.selectionStart ?? raw.length,
|
||||
getTextReplaceMapJson()
|
||||
);
|
||||
if (result) {
|
||||
setText(result.text);
|
||||
// Schedule cursor restore after React flushes the new value
|
||||
const pos = result.cursor;
|
||||
requestAnimationFrame(() => input.setSelectionRange(pos, pos));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setText(raw);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
@@ -173,7 +204,7 @@ export const MessageInput = forwardRef<MessageInputHandle, MessageInputProps>(fu
|
||||
data-1p-ignore="true"
|
||||
data-bwignore="true"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder || 'Type a message...'}
|
||||
disabled={disabled || sending}
|
||||
|
||||
@@ -99,7 +99,12 @@ export function TelemetryPane({
|
||||
{data.recv_errors.toLocaleString()}
|
||||
{data.packets_received > 0 && (
|
||||
<Secondary>
|
||||
({((data.recv_errors / (data.packets_received + data.recv_errors)) * 100).toFixed(2)}%)
|
||||
(
|
||||
{(
|
||||
(data.recv_errors / (data.packets_received + data.recv_errors)) *
|
||||
100
|
||||
).toFixed(2)}
|
||||
%)
|
||||
</Secondary>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -33,6 +33,13 @@ import {
|
||||
setSavedFontScale,
|
||||
} from '../../utils/fontScale';
|
||||
import { getAutoFocusInputEnabled, setAutoFocusInputEnabled } from '../../utils/autoFocusInput';
|
||||
import {
|
||||
getTextReplaceEnabled,
|
||||
setTextReplaceEnabled as saveTextReplaceEnabled,
|
||||
getTextReplaceMapJson,
|
||||
setTextReplaceMapJson,
|
||||
DEFAULT_MAP_JSON,
|
||||
} from '../../utils/textReplace';
|
||||
import {
|
||||
BATTERY_DISPLAY_CHANGE_EVENT,
|
||||
getShowBatteryPercent,
|
||||
@@ -232,6 +239,9 @@ export function SettingsLocalSection({
|
||||
const [batteryPercent, setBatteryPercent] = useState(getShowBatteryPercent);
|
||||
const [batteryVoltage, setBatteryVoltage] = useState(getShowBatteryVoltage);
|
||||
const [statusDotPulse, setStatusDotPulse] = useState(getStatusDotPulseEnabled);
|
||||
const [textReplaceEnabled, setTextReplaceEnabled] = useState(getTextReplaceEnabled);
|
||||
const [textReplaceJson, setTextReplaceJson] = useState(getTextReplaceMapJson);
|
||||
const [textReplaceError, setTextReplaceError] = useState<string | null>(null);
|
||||
const [fontScale, setFontScale] = useState(getSavedFontScale);
|
||||
const [fontScaleSlider, setFontScaleSlider] = useState(getSavedFontScale);
|
||||
const [fontScaleInput, setFontScaleInput] = useState(() => String(getSavedFontScale()));
|
||||
@@ -439,6 +449,63 @@ export function SettingsLocalSection({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-border/60 p-3 space-y-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="text-replace"
|
||||
checked={textReplaceEnabled}
|
||||
onCheckedChange={(checked) => {
|
||||
const v = checked === true;
|
||||
setTextReplaceEnabled(v);
|
||||
saveTextReplaceEnabled(v);
|
||||
}}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="text-replace">Replace as you Type</Label>
|
||||
<p className="text-[0.8125rem] text-muted-foreground">
|
||||
Automatically replace characters as you type in the message input. Define
|
||||
replacements as a JSON object mapping source strings to their replacements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{textReplaceEnabled && (
|
||||
<div className="space-y-2 pl-7">
|
||||
<textarea
|
||||
value={textReplaceJson}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setTextReplaceJson(val);
|
||||
setTextReplaceError(setTextReplaceMapJson(val));
|
||||
}}
|
||||
spellCheck={false}
|
||||
rows={10}
|
||||
className={cn(
|
||||
'w-full rounded-md border bg-background px-3 py-2 text-sm font-mono',
|
||||
textReplaceError ? 'border-destructive' : 'border-input'
|
||||
)}
|
||||
aria-label="Text replacement map (JSON)"
|
||||
/>
|
||||
{textReplaceError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{textReplaceError} Changes are not saved until this is resolved.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setTextReplaceJson(DEFAULT_MAP_JSON);
|
||||
setTextReplaceMapJson(DEFAULT_MAP_JSON);
|
||||
setTextReplaceError(null);
|
||||
}}
|
||||
className="inline-flex h-8 items-center justify-center rounded-md border border-input px-3 text-sm font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user