1package input
2
3// Manual Testing: Dead Key / Compose Support
4//
5// To verify native dead key support works correctly:
6//
7// 1. Set keyboard layout to "US International with dead keys"
8// 2. Open dumber, navigate to a page with a text input (e.g. Google search)
9// 3. Click into the text input field
10// 4. Type: ' then e -- should produce e with acute accent
11// 5. Type: " then u -- should produce u with diaeresis
12// 6. Type: ` then a -- should produce a with grave accent
13// 7. Type: ~ then n -- should produce n with tilde
14// 8. Verify Ctrl+L still opens omnibox
15// 9. Verify Alt+1-9 still switches tabs
16// 10. Verify modal modes (Ctrl+T for tab mode) still work
17// 11. In omnibox, verify long-press accent picker still works (hold 'e' for 400ms)
18// 12. Verify Escape closes omnibox / exits modal modes
19// 13. Verify session manager overlay captures all keyboard input
20
21import (
22 "context"
23 "sync"
24 "time"
25
26 "github.com/bnema/dumber/internal/domain/entity"
27 "github.com/bnema/dumber/internal/logging"
28 "github.com/bnema/puregotk/v4/gdk"
29 "github.com/bnema/puregotk/v4/glib"
30 "github.com/bnema/puregotk/v4/gtk"
31 "github.com/rs/zerolog"
32)
33
34// ActionHandler is called when a keyboard action is triggered.
35// It receives the context and the action to perform.
36// Return an error if the action fails.
37type ActionHandler func(ctx context.Context, action Action) error
38
39// AccentHandler handles long-press accent detection.
40// Called for character keys that may have accent variants.
41type AccentHandler interface {
42 // OnKeyPressed is called when a key is pressed.
43 // Returns true if the accent handler is handling this key (picker visible or will show).
44 OnKeyPressed(ctx context.Context, char rune, shiftHeld bool) bool
45 // OnKeyReleased is called when a key is released.
46 OnKeyReleased(ctx context.Context, char rune)
47 // IsPickerVisible returns true if the accent picker is currently visible.
48 IsPickerVisible() bool
49 // Cancel cancels any pending long-press detection.
50 Cancel(ctx context.Context)
51}
52
53// KeyboardHandler processes keyboard events and dispatches actions.
54// It manages modal input modes and routes key events to the appropriate handlers.
55type KeyboardHandler struct {
56 shortcuts *ShortcutSet
57 modal *ModalState
58 workspace *entity.WorkspaceConfig
59 session *entity.SessionConfig
60
61 // Action handler callback
62 onAction ActionHandler
63 // Optional routing callback that determines how a key should be handled.
64 // Returns RouteHandleShortcuts (default), RoutePassToWidget (let focused
65 // widget handle it), or RouteAccentDetection (long-press accent for GTK entries).
66 routeKey func(KeyContext) KeyRoute
67 // Optional accent handler for dead keys support
68 accentHandler AccentHandler
69 // Optional escape hook for app-level overlays
70 onEscape func(ctx context.Context) bool
71
72 // GTK controller (nil until attached)
73 controller *gtk.EventControllerKey
74
75 // Callback retention: must stay reachable by Go GC.
76 keyPressedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType) bool
77 keyReleasedCb func(gtk.EventControllerKey, uint, uint, gdk.ModifierType)
78
79 activePressedActions map[Action]uint
80 ctx context.Context
81 mu sync.RWMutex
82}
83
84// NewKeyboardHandler creates a new keyboard handler.
85func NewKeyboardHandler(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) *KeyboardHandler {
86 log := logging.FromContext(ctx)
87
88 log.Debug().Msg("creating keyboard handler")
89
90 h := &KeyboardHandler{
91 shortcuts: NewShortcutSet(ctx, workspace, session),
92 modal: NewModalState(ctx),
93 workspace: workspace,
94 session: session,
95 activePressedActions: make(map[Action]uint),
96 ctx: ctx,
97 }
98
99 return h
100}
101
102// SetOnAction sets the callback for when actions are triggered.
103func (h *KeyboardHandler) SetOnAction(fn ActionHandler) {
104 h.mu.Lock()
105 defer h.mu.Unlock()
106 h.onAction = fn
107}
108
109// ReloadShortcuts rebuilds the shortcut set from new config values.
110// This enables hot-reloading of keybindings without restarting.
111func (h *KeyboardHandler) ReloadShortcuts(ctx context.Context, workspace *entity.WorkspaceConfig, session *entity.SessionConfig) {
112 h.mu.Lock()
113 defer h.mu.Unlock()
114
115 log := logging.FromContext(ctx)
116 log.Debug().Msg("reloading shortcuts from config")
117
118 h.shortcuts = NewShortcutSet(ctx, workspace, session)
119 h.workspace = workspace
120 h.session = session
121}
122
123// SetOnModeChange sets the callback for mode changes (for UI updates).
124func (h *KeyboardHandler) SetOnModeChange(fn func(from, to Mode)) {
125 h.modal.SetOnModeChange(func(from, to Mode) {
126 // Switch controller phase: capture during modal, bubble for normal.
127 // This ensures plain modal keys (s, h, etc.) reach the handler
128 // before WebView, while normal typing still goes through WebKit IM.
129 if to == ModeNormal {
130 h.setControllerPhase(gtk.PhaseBubbleValue)
131 } else if from == ModeNormal {
132 h.setControllerPhase(gtk.PhaseCaptureValue)
133 }
134 // Forward to app-level callback
135 if fn != nil {
136 fn(from, to)
137 }
138 })
139}
140
141// setControllerPhase changes the propagation phase of the key controller.
142// Capture phase during modal modes so plain keys reach the handler before WebView.
143// Bubble phase in normal mode for WebKit IM/dead-key support.
144//
145// This is called synchronously from onModeChange so the phase switch takes
146// effect before the next key event. The caller must ensure GTK thread safety;
147// modal timeouts dispatch to the GTK main thread via glib.IdleAdd before
148// calling ExitMode, so the onModeChange callback always runs on the GTK thread.
149func (h *KeyboardHandler) setControllerPhase(phase gtk.PropagationPhase) {
150 if h.controller == nil {
151 return
152 }
153 h.controller.SetPropagationPhase(phase)
154}
155
156// SetRouteKey sets the callback that determines how each key event should be routed.
157// The callback receives key context and returns a KeyRoute indicating whether the
158// key should be handled by the shortcut system, passed to the focused widget
159// (for IM/compose support), or routed through accent detection.
160//
161// If not set, all keys are routed through the shortcut system (RouteHandleShortcuts).
162func (h *KeyboardHandler) SetRouteKey(fn func(KeyContext) KeyRoute) {
163 h.mu.Lock()
164 defer h.mu.Unlock()
165 h.routeKey = fn
166}
167
168// SetAccentHandler sets the handler for long-press accent detection.
169func (h *KeyboardHandler) SetAccentHandler(handler AccentHandler) {
170 h.mu.Lock()
171 defer h.mu.Unlock()
172 h.accentHandler = handler
173}
174
175// SetOnEscape sets an optional callback invoked for plain Escape in normal mode.
176// Return true to consume the key and stop further handling.
177func (h *KeyboardHandler) SetOnEscape(fn func(ctx context.Context) bool) {
178 h.mu.Lock()
179 defer h.mu.Unlock()
180 h.onEscape = fn
181}
182
183// Mode returns the current input mode.
184func (h *KeyboardHandler) Mode() Mode {
185 return h.modal.Mode()
186}
187
188// AttachTo attaches the keyboard handler to a GTK window.
189// The handler runs in the bubble phase so that WebKit's IM pipeline (including
190// dead-key / compose sequences) processes key events first. App-level shortcuts
191// are still intercepted because WebKit only consumes events it recognizes as
192// text input; unhandled events bubble up to this controller.
193func (h *KeyboardHandler) AttachTo(window *gtk.ApplicationWindow) {
194 log := logging.FromContext(h.ctx)
195
196 if window == nil {
197 log.Error().Msg("cannot attach keyboard handler to nil window")
198 return
199 }
200
201 h.controller = gtk.NewEventControllerKey()
202 if h.controller == nil {
203 log.Error().Msg("failed to create event controller key")
204 return
205 }
206
207 // Bubble phase: let WebKit IM handle key events first (dead keys, compose
208 // sequences, input methods). Events not consumed by WebKit bubble up here
209 // so app shortcuts still work.
210 h.controller.SetPropagationPhase(gtk.PhaseBubbleValue)
211
212 // Wire GTK main thread scheduler for modal timeouts. Timer goroutines
213 // must dispatch ExitMode to the GTK thread because onModeChange may
214 // call setControllerPhase (a GTK operation).
215 h.modal.SetMainThreadScheduler(func(fn func()) {
216 cb := glib.SourceFunc(func(_ uintptr) bool {
217 fn()
218 return false
219 })
220 glib.IdleAdd(&cb, 0)
221 })
222
223 // Connect key pressed handler (retain callback to prevent GC).
224 // The callback receives: keyval (translated key), keycode (hardware key position), state (modifiers)
225 h.keyPressedCb = func(_ gtk.EventControllerKey, keyval uint, keycode uint, state gdk.ModifierType) bool {
226 return h.handleKeyPress(keyval, keycode, state)
227 }
228 h.controller.ConnectKeyPressed(&h.keyPressedCb)
229
230 // Connect key released handler for accent detection.
231 h.keyReleasedCb = func(_ gtk.EventControllerKey, keyval uint, _ uint, _ gdk.ModifierType) {
232 h.handleKeyRelease(keyval)
233 }
234 h.controller.ConnectKeyReleased(&h.keyReleasedCb)
235
236 // Add controller to window
237 window.AddController(&h.controller.EventController)
238
239 log.Debug().Msg("keyboard handler attached to window")
240}
241
242// Detach removes the keyboard handler.
243// Note: GTK handles cleanup when the widget is destroyed,
244// but we clear our reference here.
245func (h *KeyboardHandler) Detach() {
246 h.mu.Lock()
247 defer h.mu.Unlock()
248 h.controller = nil
249 h.keyPressedCb = nil
250 h.keyReleasedCb = nil
251 h.activePressedActions = nil
252}
253
254// handleKeyPress processes a key press event.
255// Returns true if the event was handled and should not propagate further.
256func (h *KeyboardHandler) handleKeyPress(keyval, keycode uint, state gdk.ModifierType) bool {
257 log := logging.FromContext(h.ctx)
258
259 h.mu.RLock()
260 routeKey := h.routeKey
261 accentHandler := h.accentHandler
262 onEscape := h.onEscape
263 h.mu.RUnlock()
264
265 // Accent picker takes absolute priority when visible -- it has its own key controller
266 if accentHandler != nil && accentHandler.IsPickerVisible() {
267 return false
268 }
269
270 modifiers := Modifier(state) & modifierMask
271
272 // Escape in normal mode: check app-level escape hook first
273 if h.modal.Mode() == ModeNormal && keyval == uint(gdk.KEY_Escape) && modifiers == 0 {
274 if onEscape != nil && onEscape(h.ctx) {
275 return true
276 }
277 }
278
279 // Determine routing for this key event
280 route := RouteHandleShortcuts // default: process through shortcut system
281 if routeKey != nil && h.modal.Mode() == ModeNormal {
282 route = routeKey(KeyContext{
283 Keyval: keyval,
284 Keycode: keycode,
285 Modifiers: modifiers,
286 })
287 }
288
289 switch route {
290 case RoutePassToWidget:
291 // Let the focused widget handle this key directly (WebView IM, etc.)
292 // No accent detection here -- WebView has its own JS-based accent detection.
293 log.Trace().Uint("keyval", keyval).Msg("routing key to focused widget")
294 return false
295
296 case RouteAccentDetection:
297 // Try long-press accent detection for GTK Entry widgets (omnibox, find bar).
298 // If the accent handler doesn't consume the key, pass it to the widget.
299 if h.tryAccentDetection(accentHandler, keyval, modifiers) {
300 return true
301 }
302 log.Trace().Uint("keyval", keyval).Msg("accent detection declined, routing to widget")
303 return false
304
305 case RouteHandleShortcuts:
306 // Fall through to shortcut processing below
307 }
308
309 // --- Shortcut processing path ---
310
311 // Normalize uppercase letters for consistent binding lookup
312 keyval = normalizeKeyval(keyval)
313
314 binding := KeyBinding{Keyval: keyval, Modifiers: modifiers}
315 mode := h.modal.Mode()
316 action, found := h.lookupAction(log, binding, mode, modifiers, keycode)
317
318 if !found {
319 return mode != ModeNormal // Consume unrecognized keys in modal mode
320 }
321 if h.suppressHeldAction(action, keyval) {
322 log.Trace().
323 Str("action", string(action)).
324 Msg("held keyboard action repeat suppressed")
325 return true
326 }
327
328 return h.dispatchAction(action, mode)
329}
330
331// tryAccentDetection starts long-press detection for accent-eligible keys.
332// It is used only for the RouteAccentDetection route (GTK Entry widgets such as
333// the omnibox and find bar). RoutePassToWidget does not call tryAccentDetection;
334// WebView long-press accent behavior is handled entirely by the injected JS bridge
335// (accentDetectionScript) and the Go-side accent key detection script, not here.
336// Returns true if the key should be suppressed (i.e. RouteAccentDetection consumed it).
337func (h *KeyboardHandler) tryAccentDetection(accentHandler AccentHandler, keyval uint, modifiers Modifier) bool {
338 if accentHandler == nil {
339 return false
340 }
341 // Only consider keys without Ctrl/Alt modifiers (Shift is OK for uppercase)
342 if modifiers != 0 && modifiers != ModShift {
343 return false
344 }
345 if char := KeyvalToRune(keyval); char != 0 {
346 return accentHandler.OnKeyPressed(h.ctx, char, modifiers == ModShift)
347 }
348 return false
349}
350
351// normalizeKeyval converts uppercase A-Z to lowercase for consistent binding lookup.
352func normalizeKeyval(keyval uint) uint {
353 if keyval >= uint('A') && keyval <= uint('Z') {
354 return keyval + (uint('a') - uint('A'))
355 }
356 return keyval
357}
358
359// dispatchAction dispatches the action and handles mode-related logic.
360func (h *KeyboardHandler) dispatchAction(action Action, mode Mode) bool {
361 if h.handleModeAction(action) {
362 return true
363 }
364
365 h.mu.RLock()
366 handler := h.onAction
367 h.mu.RUnlock()
368
369 if handler != nil {
370 if err := handler(h.ctx, action); err != nil {
371 log := logging.FromContext(h.ctx)
372 log.Error().Err(err).Str("action", string(action)).Msg("action handler error")
373 }
374 }
375
376 if mode == ModeResize && isResizeAction(action) {
377 h.modal.ResetTimeout(h.ctx)
378 }
379
380 if mode != ModeNormal && ShouldAutoExitMode(action) {
381 h.modal.ExitMode(h.ctx)
382 }
383
384 return true // Consumed the key
385}
386
387func (h *KeyboardHandler) suppressHeldAction(action Action, keyval uint) bool {
388 if h == nil || !isRepeatedKeyboardActionSuppressed(action) {
389 return false
390 }
391
392 h.mu.Lock()
393 defer h.mu.Unlock()
394 if h.activePressedActions == nil {
395 return true
396 }
397 if pressedKeyval, ok := h.activePressedActions[action]; ok && pressedKeyval == keyval {
398 return true
399 }
400 h.activePressedActions[action] = keyval
401 return false
402}
403
404func isRepeatedKeyboardActionSuppressed(action Action) bool {
405 if isRepeatedGlobalShortcutSuppressed(action) {
406 return true
407 }
408 switch action {
409 case ActionEnterTabMode,
410 ActionEnterPaneMode,
411 ActionEnterSessionMode,
412 ActionEnterResizeMode,
413 ActionNewTab,
414 ActionRenameTab,
415 ActionSplitRight,
416 ActionSplitLeft,
417 ActionSplitUp,
418 ActionSplitDown,
419 ActionStackPane,
420 ActionMovePaneToTab,
421 ActionMovePaneToNextTab:
422 return true
423 default:
424 return false
425 }
426}
427
428func (h *KeyboardHandler) lookupAction(
429 log *zerolog.Logger,
430 binding KeyBinding,
431 mode Mode,
432 modifiers Modifier,
433 keycode uint,
434) (Action, bool) {
435 action, found := h.shortcuts.Lookup(binding, mode)
436
437 // Fallback: check hardware keycode for Alt+number shortcuts on non-QWERTY layouts.
438 if !found && mode == ModeNormal && modifiers == ModAlt {
439 if keycodeAction, ok := KeycodeToTabAction[keycode]; ok {
440 action = keycodeAction
441 found = true
442 if log != nil {
443 log.Debug().
444 Uint("keycode", keycode).
445 Str("action", string(action)).
446 Msg("tab switch via hardware keycode fallback (non-QWERTY layout)")
447 }
448 }
449 }
450
451 if !found && mode == ModeNormal && (modifiers == ModAlt || modifiers == (ModAlt|ModShift)) {
452 if bracketActions, ok := KeycodeToBracketAction[keycode]; ok {
453 if modifiers == (ModAlt | ModShift) {
454 action = bracketActions.WithShift
455 } else {
456 action = bracketActions.NoShift
457 }
458 found = action != ""
459 if found && log != nil {
460 log.Debug().
461 Uint("keycode", keycode).
462 Str("action", string(action)).
463 Msg("consume/expel via hardware keycode fallback (non-QWERTY layout)")
464 }
465 }
466 }
467
468 return action, found
469}
470
471func isResizeAction(action Action) bool {
472 switch action {
473 case ActionResizeIncreaseLeft,
474 ActionResizeIncreaseRight,
475 ActionResizeIncreaseUp,
476 ActionResizeIncreaseDown,
477 ActionResizeDecreaseLeft,
478 ActionResizeDecreaseRight,
479 ActionResizeDecreaseUp,
480 ActionResizeDecreaseDown,
481 ActionResizeIncrease,
482 ActionResizeDecrease:
483 return true
484 default:
485 return false
486 }
487}
488
489func (h *KeyboardHandler) handleModeAction(action Action) bool {
490 h.mu.RLock()
491 workspace := h.workspace
492 session := h.session
493 h.mu.RUnlock()
494
495 switch action {
496 case ActionEnterTabMode:
497 var ms int
498 if workspace != nil {
499 ms = workspace.TabMode.TimeoutMilliseconds
500 }
501 h.modal.EnterTabMode(h.ctx, time.Duration(ms)*time.Millisecond)
502 return true
503 case ActionEnterPaneMode:
504 var ms int
505 if workspace != nil {
506 ms = workspace.PaneMode.TimeoutMilliseconds
507 }
508 h.modal.EnterPaneMode(h.ctx, time.Duration(ms)*time.Millisecond)
509 return true
510 case ActionEnterSessionMode:
511 var ms int
512 if session != nil {
513 ms = session.SessionMode.TimeoutMilliseconds
514 }
515 h.modal.EnterSessionMode(h.ctx, time.Duration(ms)*time.Millisecond)
516 return true
517 case ActionEnterResizeMode:
518 if h.modal.Mode() == ModeResize {
519 h.modal.ExitMode(h.ctx)
520 return true
521 }
522 var ms int
523 if workspace != nil {
524 ms = workspace.ResizeMode.TimeoutMilliseconds
525 }
526 h.modal.EnterResizeMode(h.ctx, time.Duration(ms)*time.Millisecond)
527 return true
528 case ActionExitMode:
529 h.modal.ExitMode(h.ctx)
530 return true
531 default:
532 return false
533 }
534}
535
536// EnterTabMode programmatically enters tab mode.
537// Useful for testing or programmatic mode changes.
538func (h *KeyboardHandler) EnterTabMode() {
539 h.mu.RLock()
540 workspace := h.workspace
541 h.mu.RUnlock()
542 var ms int
543 if workspace != nil {
544 ms = workspace.TabMode.TimeoutMilliseconds
545 }
546 h.modal.EnterTabMode(h.ctx, time.Duration(ms)*time.Millisecond)
547}
548
549// EnterPaneMode programmatically enters pane mode.
550// Useful for testing or programmatic mode changes.
551func (h *KeyboardHandler) EnterPaneMode() {
552 h.mu.RLock()
553 workspace := h.workspace
554 h.mu.RUnlock()
555 var ms int
556 if workspace != nil {
557 ms = workspace.PaneMode.TimeoutMilliseconds
558 }
559 h.modal.EnterPaneMode(h.ctx, time.Duration(ms)*time.Millisecond)
560}
561
562// EnterSessionMode programmatically enters session mode.
563// Useful for testing or programmatic mode changes.
564func (h *KeyboardHandler) EnterSessionMode() {
565 h.mu.RLock()
566 session := h.session
567 h.mu.RUnlock()
568 var ms int
569 if session != nil {
570 ms = session.SessionMode.TimeoutMilliseconds
571 }
572 h.modal.EnterSessionMode(h.ctx, time.Duration(ms)*time.Millisecond)
573}
574
575// ExitMode programmatically exits modal mode.
576// Useful for testing or programmatic mode changes.
577func (h *KeyboardHandler) ExitMode() {
578 h.modal.ExitMode(h.ctx)
579}
580
581// DispatchAction processes an action externally triggered (e.g., by a global
582// shortcut). Mode-enter actions update modal state; other actions are forwarded
583// to the registered action handler.
584func (h *KeyboardHandler) DispatchAction(action Action) {
585 mode := h.modal.Mode()
586 h.dispatchAction(action, mode)
587}
588
589// handleKeyRelease processes a key release event for accent detection.
590func (h *KeyboardHandler) handleKeyRelease(keyval uint) {
591 h.mu.Lock()
592 accentHandler := h.accentHandler
593 if len(h.activePressedActions) > 0 {
594 keyval = normalizeKeyval(keyval)
595 for action, pressedKeyval := range h.activePressedActions {
596 if pressedKeyval == keyval {
597 delete(h.activePressedActions, action)
598 }
599 }
600 }
601 h.mu.Unlock()
602
603 if accentHandler == nil {
604 return
605 }
606
607 // Convert keyval to rune for the accent handler
608 if char := KeyvalToRune(keyval); char != 0 {
609 accentHandler.OnKeyReleased(h.ctx, char)
610 }
611}
612
613// KeyvalToRune converts a GTK keyval to a lowercase rune.
614// Returns 0 if the keyval is not a printable character with potential accents.
615func KeyvalToRune(keyval uint) rune {
616 // Handle lowercase a-z
617 if keyval >= uint('a') && keyval <= uint('z') {
618 return rune(keyval)
619 }
620 // Handle uppercase A-Z (convert to lowercase for accent lookup)
621 if keyval >= uint('A') && keyval <= uint('Z') {
622 return rune(keyval + ('a' - 'A'))
623 }
624 return 0
625}
626
627// KeyRoute determines how a key event should be routed.
628type KeyRoute int
629
630const (
631 // RouteHandleShortcuts means the key should be processed by the shortcut system.
632 // This is the default route -- app-level shortcuts, modal mode keys, etc.
633 RouteHandleShortcuts KeyRoute = iota
634
635 // RoutePassToWidget means the key should propagate to the focused widget.
636 // Used when overlays (session manager, tab picker) are visible, or when a
637 // WebView should receive text input for native IM/compose/dead-key processing.
638 // Long-press accent detection still runs first for accessibility: users with
639 // motor disabilities may not be able to execute rapid compose sequences.
640 RoutePassToWidget
641
642 // RouteAccentDetection means the key should go through long-press accent
643 // detection, then pass to the focused widget. Used for GTK Entry widgets
644 // (omnibox, find bar) and as a universal accessibility fallback.
645 RouteAccentDetection
646)
647
648// KeyContext provides key event information for routing decisions.
649type KeyContext struct {
650 Keyval uint // Translated key value
651 Keycode uint // Hardware keycode
652 Modifiers Modifier // Active modifiers (masked)
653}
654
655// IsShortcutModified returns true if the modifiers indicate a shortcut combination
656// (Ctrl or Alt pressed), as opposed to plain text input (no modifier or Shift-only).
657// Dead keys and compose sequences only use no-modifier or Shift, so this correctly
658// identifies keys that should be intercepted vs passed through to the IM pipeline.
659func IsShortcutModified(modifiers Modifier) bool {
660 return modifiers&(ModCtrl|ModAlt) != 0
661}
662
663// IsTextInputKey returns true if the keyval represents a text input key
664// (printable character or dead key) as opposed to a function/navigation key.
665// This is used to determine whether a key should be passed through to the
666// WebKit IM pipeline for compose/dead-key processing.
667func IsTextInputKey(keyval uint) bool {
668 // Dead keys used by compose sequences (US International, etc.)
669 // GDK dead key range: KEY_dead_grave (0xfe50) to KEY_dead_greek (0xfe8c)
670 if keyval >= 0xfe50 && keyval <= 0xfe8c {
671 return true
672 }
673
674 // Printable ASCII range (space through tilde)
675 if keyval >= 0x020 && keyval <= 0x07e {
676 return true
677 }
678 // Latin-1 supplement (non-breaking space through ÿ)
679 if keyval >= 0x0a0 && keyval <= 0x0ff {
680 return true
681 }
682 // Extended Latin and other Unicode characters mapped in GDK keyval space
683 if keyval >= 0x100 && keyval <= 0x20ff {
684 return true
685 }
686
687 return false
688}