Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shortcuts may provide a custom action for the intent #133

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions lib/src/terminal_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ class TerminalView extends StatefulWidget {

/// Shortcuts for this terminal. This has higher priority than input handler
/// of the terminal If not provided, [defaultTerminalShortcuts] will be used.
final Map<ShortcutActivator, Intent>? shortcuts;
//final Map<ShortcutActivator, Intent>? shortcuts;
final List<TerminalShortcut>? shortcuts;

/// True if no input should send to the terminal.
final bool readOnly;
Expand All @@ -132,7 +133,9 @@ class TerminalView extends StatefulWidget {
class TerminalViewState extends State<TerminalView> {
late FocusNode _focusNode;

late final ShortcutManager _shortcutManager;
late ShortcutManager _shortcutManager;

late List<TerminalShortcut> _shortcuts;

final _customTextEditKey = GlobalKey<CustomTextEditState>();

Expand All @@ -154,9 +157,7 @@ class TerminalViewState extends State<TerminalView> {
_focusNode = widget.focusNode ?? FocusNode();
_controller = widget.controller ?? TerminalController();
_scrollController = widget.scrollController ?? ScrollController();
_shortcutManager = ShortcutManager(
shortcuts: widget.shortcuts ?? defaultTerminalShortcuts,
);
_initShortcuts();
super.initState();
}

Expand All @@ -180,6 +181,12 @@ class TerminalViewState extends State<TerminalView> {
}
_scrollController = widget.scrollController ?? ScrollController();
}
if (oldWidget.shortcuts != widget.shortcuts) {
// The current ShortcutManager has to be disposed
// before the new one is created.
_shortcutManager.dispose();
_initShortcuts();
}
super.didUpdateWidget(oldWidget);
}

Expand All @@ -198,6 +205,18 @@ class TerminalViewState extends State<TerminalView> {
super.dispose();
}

// Initialize the specified shortcut or set the default shortcuts.
void _initShortcuts() {
// Convert the list of shortcuts to a map suitable for the shortcut manager.
_shortcuts = widget.shortcuts ?? TerminalShortcut.defaults;
final shortcutsMap = Map.fromEntries(_shortcuts
.map((shortcut) => MapEntry(shortcut.activator, shortcut.intent)));
// Create a shortcut manager.
_shortcutManager = ShortcutManager(
shortcuts: shortcutsMap,
);
}

@override
Widget build(BuildContext context) {
Widget child = Scrollable(
Expand Down Expand Up @@ -261,6 +280,7 @@ class TerminalViewState extends State<TerminalView> {
child = TerminalActions(
terminal: widget.terminal,
controller: _controller,
shortcuts: _shortcuts,
child: child,
);

Expand Down
52 changes: 8 additions & 44 deletions lib/src/ui/shortcut/actions.dart
Original file line number Diff line number Diff line change
@@ -1,67 +1,31 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:xterm/src/core/buffer/cell_offset.dart';
import 'package:xterm/src/core/buffer/range_line.dart';
import 'package:xterm/src/terminal.dart';
import 'package:xterm/src/ui/controller.dart';
import 'package:xterm/src/ui/shortcut/shortcuts.dart';

class TerminalActions extends StatelessWidget {
const TerminalActions({
super.key,
required this.terminal,
required this.controller,
required this.shortcuts,
required this.child,
});

final Terminal terminal;

final TerminalController controller;

final List<TerminalShortcut> shortcuts;

final Widget child;

@override
Widget build(BuildContext context) {
return Actions(
actions: {
PasteTextIntent: CallbackAction<PasteTextIntent>(
onInvoke: (intent) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
if (text != null) {
terminal.paste(text);
controller.clearSelection();
}
return null;
},
),
CopySelectionTextIntent: CallbackAction<CopySelectionTextIntent>(
onInvoke: (intent) async {
final selection = controller.selection;

if (selection == null) {
return;
}

final text = terminal.buffer.getText(selection);

await Clipboard.setData(ClipboardData(text: text));

return null;
},
),
SelectAllTextIntent: CallbackAction<SelectAllTextIntent>(
onInvoke: (intent) {
controller.setSelection(
BufferRangeLine(
CellOffset(0, terminal.buffer.height - terminal.viewHeight),
CellOffset(terminal.viewWidth, terminal.buffer.height - 1),
),
);
return null;
},
),
},
child: child,
// Convert the list of shortcuts to a callback map.
final actions = Map.fromEntries(
shortcuts.map((e) => e.toActionMapEntry(terminal, controller)),
);
return Actions(actions: actions, child: child);
}
}
149 changes: 123 additions & 26 deletions lib/src/ui/shortcut/shortcuts.dart
Original file line number Diff line number Diff line change
@@ -1,34 +1,131 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

Map<ShortcutActivator, Intent> get defaultTerminalShortcuts {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return _defaultShortcuts;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _defaultAppleShortcuts;

import 'package:xterm/src/core/buffer/cell_offset.dart';
import 'package:xterm/src/core/buffer/range_line.dart';
import 'package:xterm/src/terminal.dart';
import 'package:xterm/src/ui/controller.dart';

class TerminalShortcut<T extends Intent> {
/// The activator which triggers the the intent.
final ShortcutActivator activator;

/// The intent that is triggered bt the activator.
final T intent;

/// The action to run when the shortcut is invoked.
final Object? Function(T, Terminal, TerminalController) action;

const TerminalShortcut(this.activator, this.intent, this.action);

/// Use the default modifier key for the current platform so assemble a
/// key combination for the shortcut. On iOS the macOS the shortcut will be
/// triggered my META + [key], otherwise CTRL + [key] triggers the shortcut.
factory TerminalShortcut.platformDefault(
LogicalKeyboardKey key,
T intent,
Object? Function(T, Terminal, TerminalController) action,
) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return TerminalShortcut(
SingleActivator(key, control: true),
intent,
action,
);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return TerminalShortcut(
SingleActivator(key, meta: true),
intent,
action,
);
}
}
}

final _defaultShortcuts = {
SingleActivator(LogicalKeyboardKey.keyC, control: true):
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.keyV, control: true):
const PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.keyA, control: true):
const SelectAllTextIntent(SelectionChangedCause.keyboard),
};
/// Convert the shortcut to a [MapEntry] for passing it to an [Action] Widget.
MapEntry<Type, Action<T>> toActionMapEntry(
Terminal terminal,
TerminalController terminalController,
) {
return MapEntry(
intent.runtimeType,
CallbackAction<T>(
onInvoke: (intent) => action(intent, terminal, terminalController),
),
);
}

final _defaultAppleShortcuts = {
SingleActivator(LogicalKeyboardKey.keyC, meta: true):
/// Generate a list of default shortcuts for the current platform.
static final List<TerminalShortcut> defaults = <TerminalShortcut>[
TerminalShortcut<CopySelectionTextIntent>.platformDefault(
LogicalKeyboardKey.keyC,
CopySelectionTextIntent.copy,
SingleActivator(LogicalKeyboardKey.keyV, meta: true):
TerminalShortcut.defaultCopy,
),
TerminalShortcut<PasteTextIntent>.platformDefault(
LogicalKeyboardKey.keyV,
const PasteTextIntent(SelectionChangedCause.keyboard),
SingleActivator(LogicalKeyboardKey.keyA, meta: true):
TerminalShortcut.defaultPaste,
),
TerminalShortcut<SelectAllTextIntent>.platformDefault(
LogicalKeyboardKey.keyA,
const SelectAllTextIntent(SelectionChangedCause.keyboard),
};
TerminalShortcut.defaultSelectAll,
),
];

/// Default handler for [CopySelectionTextIntent].
static Object? defaultCopy(
CopySelectionTextIntent intent,
Terminal terminal,
TerminalController controller,
) async {
final selection = controller.selection;

if (selection == null) {
return null;
}

final text = terminal.buffer.getText(selection);

await Clipboard.setData(ClipboardData(text: text));

return null;
}

/// Default handler for [PasteTextIntent].
static Object? defaultPaste(
PasteTextIntent intent,
Terminal terminal,
TerminalController controller,
) async {
final data = await Clipboard.getData(Clipboard.kTextPlain);
final text = data?.text;
if (text != null) {
terminal.paste(text);
controller.clearSelection();
}

return null;
}

/// Default handler for [SelectAllTextIntent].
static Object? defaultSelectAll(
SelectAllTextIntent intent,
Terminal terminal,
TerminalController controller,
) async {
controller.setSelection(
BufferRangeLine(
CellOffset(0, terminal.buffer.height - terminal.viewHeight),
CellOffset(terminal.viewWidth, terminal.buffer.height - 1),
),
);
return null;
}
}