hilda 2.0.16__py3-none-any.whl → 3.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hilda/hilda_client.py CHANGED
@@ -5,7 +5,6 @@ import importlib.util
5
5
  import json
6
6
  import logging
7
7
  import os
8
- import pickle
9
8
  import struct
10
9
  import sys
11
10
  import time
@@ -18,10 +17,9 @@ from functools import cached_property, wraps
18
17
  from pathlib import Path
19
18
  from typing import Any, Callable, Optional, Union
20
19
 
20
+ import click
21
21
  import hexdump
22
22
  import IPython
23
- from humanfriendly import prompts
24
- from humanfriendly.terminal.html import html_to_ansi
25
23
  from IPython.core.magic import register_line_magic # noqa: F401
26
24
  from pygments import highlight
27
25
  from pygments.formatters import TerminalTrueColorFormatter
@@ -30,11 +28,13 @@ from tqdm import tqdm
30
28
  from traitlets.config import Config
31
29
 
32
30
  from hilda import objective_c_class
31
+ from hilda.breakpoints import BreakpointList, HildaBreakpoint, WhereType
33
32
  from hilda.common import CfSerializable, selection_prompt
34
33
  from hilda.exceptions import AccessingMemoryError, AccessingRegisterError, AddingLldbSymbolError, \
35
- BrokenLocalSymbolsJarError, ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
34
+ ConvertingFromNSObjectError, ConvertingToNsObjectError, CreatingObjectiveCSymbolError, \
36
35
  DisableJetsamMemoryChecksError, EvaluatingExpressionError, HildaException, InvalidThreadIndexError, \
37
36
  SymbolAbsentError
37
+ from hilda.ipython_extensions.keybindings import get_keybindings
38
38
  from hilda.lldb_importer import lldb
39
39
  from hilda.objective_c_symbol import ObjectiveCSymbol
40
40
  from hilda.registers import Registers
@@ -42,6 +42,7 @@ from hilda.snippets.mach import CFRunLoopServiceMachPort_hooks
42
42
  from hilda.symbol import Symbol
43
43
  from hilda.symbols_jar import SymbolsJar
44
44
  from hilda.ui.ui_manager import UiManager
45
+ from hilda.watchpoints import WatchpointList
45
46
 
46
47
  lldb.KEYSTONE_SUPPORT = True
47
48
  try:
@@ -50,31 +51,12 @@ except ImportError:
50
51
  lldb.KEYSTONE_SUPPORT = False
51
52
  print('failed to import keystone. disabling some features')
52
53
 
53
- hilda_art = Path(__file__).resolve().parent.joinpath('hilda_ascii_art.html').read_text()
54
-
55
- GREETING = f"""
56
- {hilda_art}
57
-
58
- <b>Hilda has been successfully loaded! 😎
59
- Usage:
60
- <span style="color: magenta">p</span> Global to access all features.
61
- <span style="color: magenta">F1</span> Show UI.
62
- <span style="color: magenta">F2</span> Toggle enabling of stdout & stderr.
63
- <span style="color: magenta">F7</span> Step Into.
64
- <span style="color: magenta">F8</span> Step Over.
65
- <span style="color: magenta">F9</span> Continue.
66
- <span style="color: magenta">F10</span> Stop.
67
-
68
- Have a nice flight ✈️! Starting an IPython shell...
69
- """
70
-
71
54
 
72
55
  def disable_logs() -> None:
73
56
  logging.getLogger('asyncio').disabled = True
74
57
  logging.getLogger('parso.cache').disabled = True
75
58
  logging.getLogger('parso.cache.pickle').disabled = True
76
59
  logging.getLogger('parso.python.diff').disabled = True
77
- logging.getLogger('humanfriendly.prompts').disabled = True
78
60
  logging.getLogger('blib2to3.pgen2.driver').disabled = True
79
61
  logging.getLogger('hilda.launch_lldb').setLevel(logging.INFO)
80
62
 
@@ -82,6 +64,17 @@ def disable_logs() -> None:
82
64
  SerializableSymbol = namedtuple('SerializableSymbol', 'address type_ filename')
83
65
 
84
66
 
67
+ @dataclass
68
+ class HelpSnippet:
69
+ """ Small snippet line to occur from `HildaClient.show_help()` """
70
+
71
+ key: str
72
+ description: str
73
+
74
+ def __str__(self) -> str:
75
+ return click.style(self.key.ljust(8), bold=True, fg='magenta') + click.style(self.description, bold=True)
76
+
77
+
85
78
  @dataclass
86
79
  class Configs:
87
80
  """ Configuration settings for evaluation and monitoring. """
@@ -125,39 +118,18 @@ def stop_is_needed(func: Callable):
125
118
  return wrapper
126
119
 
127
120
 
128
- class HildaBreakpoint:
129
- def __init__(self, hilda_client: 'HildaClient', lldb_breakpoint: lldb.SBBreakpoint,
130
- address: Union[str, int], forced: bool = False, options: Optional[typing.Mapping] = None,
131
- callback: Optional[Callable] = None) -> None:
132
- self._hilda_client = hilda_client
133
- self.address = address
134
- self.forced = forced
135
- self.options = options
136
- self.callback = callback
137
- self.lldb_breakpoint = lldb_breakpoint
138
-
139
- def __repr__(self) -> str:
140
- return (f'<{self.__class__.__name__} LLDB:{self.lldb_breakpoint} FORCED:{self.forced} OPTIONS:{self.options} '
141
- f'CALLBACK:{self.callback}>')
142
-
143
- def __str__(self) -> str:
144
- return repr(self)
145
-
146
- def remove(self) -> None:
147
- self._hilda_client.remove_hilda_breakpoint(self.lldb_breakpoint.id)
148
-
149
-
150
121
  class HildaClient:
151
122
  RETVAL_BIT_COUNT = 64
152
123
 
153
- def __init__(self, debugger: lldb.SBDebugger):
124
+ def __init__(self, debugger: lldb.SBDebugger) -> None:
154
125
  self.logger = logging.getLogger(__name__)
155
126
  self.endianness = '<'
156
127
  self.debugger = debugger
157
128
  self.target = debugger.GetSelectedTarget()
158
129
  self.process = self.target.GetProcess()
159
130
  self.symbols = SymbolsJar.create(self)
160
- self.breakpoints = {}
131
+ self.breakpoints = BreakpointList(self)
132
+ self.watchpoints = WatchpointList(self)
161
133
  self.captured_objects = {}
162
134
  self.registers = Registers(self)
163
135
  self.arch = self.target.GetTriple().split('-')[0]
@@ -177,14 +149,15 @@ class HildaClient:
177
149
  self.log_info(f'Target: {self.target}')
178
150
  self.log_info(f'Process: {self.process}')
179
151
 
180
- def hd(self, buf):
152
+ def hd(self, buf: bytes) -> None:
181
153
  """
182
- Print an hexdump of given buffer
154
+ Print hexdump representation for given buffer.
155
+
183
156
  :param buf: buffer to print in hexdump form
184
157
  """
185
- print(hexdump.hexdump(buf))
158
+ hexdump.hexdump(buf)
186
159
 
187
- def lsof(self) -> dict:
160
+ def lsof(self) -> dict[int, Any]:
188
161
  """
189
162
  Get dictionary of all open FDs
190
163
  :return: Mapping between open FDs and their paths
@@ -201,7 +174,7 @@ class HildaClient:
201
174
  if i == depth:
202
175
  break
203
176
  row = ''
204
- row += html_to_ansi(f'<span style="color: cyan">0x{frame.addr.GetFileAddress():x}</span> ')
177
+ row += click.style(f'0x{frame.addr.GetFileAddress():x} ', fg='cyan')
205
178
  row += str(frame)
206
179
  if i == 0:
207
180
  # first line
@@ -222,7 +195,7 @@ class HildaClient:
222
195
  if result:
223
196
  raise DisableJetsamMemoryChecksError()
224
197
 
225
- def symbol(self, address):
198
+ def symbol(self, address: int) -> Symbol:
226
199
  """
227
200
  Get symbol object for a given address
228
201
  :param address:
@@ -230,7 +203,7 @@ class HildaClient:
230
203
  """
231
204
  return Symbol.create(address, self)
232
205
 
233
- def objc_symbol(self, address) -> ObjectiveCSymbol:
206
+ def objc_symbol(self, address: int) -> ObjectiveCSymbol:
234
207
  """
235
208
  Get objc symbol wrapper for given address
236
209
  :param address:
@@ -241,11 +214,12 @@ class HildaClient:
241
214
  except HildaException as e:
242
215
  raise CreatingObjectiveCSymbolError from e
243
216
 
244
- def inject(self, filename):
217
+ def inject(self, filename: str) -> SymbolsJar:
245
218
  """
246
- Inject a single library into currently running process
247
- :param filename:
248
- :return: module object
219
+ Inject a single library into currently running process.
220
+
221
+ :param filename: library to inject (dylib)
222
+ :return: SymbolsJar
249
223
  """
250
224
  module = self.target.FindModule(lldb.SBFileSpec(os.path.basename(filename), False))
251
225
  if module.file.basename is not None:
@@ -459,7 +433,8 @@ class HildaClient:
459
433
 
460
434
  def set_register(self, name: str, value: Union[float, int]) -> None:
461
435
  """
462
- Set value for register by its name
436
+ Set value for register by its name.
437
+
463
438
  :param name: Register name
464
439
  :param value: Register value
465
440
  """
@@ -473,7 +448,8 @@ class HildaClient:
473
448
 
474
449
  def objc_call(self, obj: int, selector: str, *params):
475
450
  """
476
- Simulate a call to an objc selector
451
+ Simulate a call to an objc selector.
452
+
477
453
  :param obj: obj to pass into `objc_msgSend`
478
454
  :param selector: selector to execute
479
455
  :param params: any other additional parameters the selector requires
@@ -489,7 +465,7 @@ class HildaClient:
489
465
  with self.stopped():
490
466
  return self.evaluate_expression(call_expression)
491
467
 
492
- def call(self, address, argv: list = None):
468
+ def call(self, address, argv: Optional[list] = None):
493
469
  """
494
470
  Call function at given address with given parameters
495
471
  :param address:
@@ -506,107 +482,11 @@ class HildaClient:
506
482
  """
507
483
  Monitor every time a given address is called
508
484
 
509
- The following options are available:
510
- regs={reg1: format}
511
- will print register values
512
-
513
- Available formats:
514
- x: hex
515
- s: string
516
- cf: use CFCopyDescription() to get more informative description of the object
517
- po: use LLDB po command
518
- std::string: for std::string
519
-
520
- User defined function, will be called like `format_function(hilda_client, value)`.
521
-
522
- For example:
523
- regs={'x0': 'x'} -> x0 will be printed in HEX format
524
- expr={lldb_expression: format}
525
- lldb_expression can be for example '$x0' or '$arg1'
526
- format behaves just like 'regs' option
527
- retval=format
528
- Print function's return value. The format is the same as regs format.
529
- stop=True
530
- force a stop at every hit
531
- bt=True
532
- print backtrace
533
- cmd=[cmd1, cmd2]
534
- run several LLDB commands, one by another
535
- force_return=value
536
- force a return from function with the specified value
537
- name=some_value
538
- use `some_name` instead of the symbol name automatically extracted from the calling frame
539
- override=True
540
- override previous break point at same location
541
-
542
-
543
- :param address:
544
- :param condition: set as a conditional breakpoint using an lldb expression
545
- :param options:
546
- :return:
485
+ Alias of self.breakpoints.add_monitor()
547
486
  """
487
+ return self.breakpoints.add_monitor(address, condition, **options)
548
488
 
549
- def callback(hilda, frame, bp_loc, options):
550
- """
551
- :param HildaClient hilda: Hilda client.
552
- :param lldb.SBFrame frame: LLDB Frame object.
553
- :param lldb.SBBreakpointLocation bp_loc: LLDB Breakpoint location object.
554
- :param dict options: User defined options.
555
- """
556
- bp = bp_loc.GetBreakpoint()
557
-
558
- symbol = hilda.symbol(hilda.frame.addr.GetLoadAddress(hilda.target)) # type: Symbol
559
-
560
- # by default, attempt to resolve the symbol name through lldb
561
- name = str(symbol.lldb_symbol)
562
- if options.get('name', False):
563
- name = options['name']
564
-
565
- log_message = f'🚨 #{bp.id} 0x{symbol:x} {name} - Thread #{self.thread.idx}:{hex(self.thread.id)}'
566
-
567
- if 'regs' in options:
568
- log_message += '\nregs:'
569
- for name, fmt in options['regs'].items():
570
- value = hilda.symbol(frame.FindRegister(name).unsigned)
571
- log_message += f'\n\t{name} = {hilda._monitor_format_value(fmt, value)}'
572
-
573
- if 'expr' in options:
574
- log_message += '\nexpr:'
575
- for name, fmt in options['expr'].items():
576
- value = hilda.symbol(hilda.evaluate_expression(name))
577
- log_message += f'\n\t{name} = {hilda._monitor_format_value(fmt, value)}'
578
-
579
- force_return = options.get('force_return')
580
- if force_return is not None:
581
- hilda.force_return(force_return)
582
- log_message += f'\nforced return: {force_return}'
583
-
584
- if options.get('bt'):
585
- # bugfix: for callstacks from xpc events
586
- hilda.finish()
587
- for frame in hilda.bt():
588
- log_message += f'\n\t{frame[0]} {frame[1]}'
589
-
590
- retval = options.get('retval')
591
- if retval is not None:
592
- # return from function
593
- hilda.finish()
594
- value = hilda.evaluate_expression('$arg1')
595
- log_message += f'\nreturned: {hilda._monitor_format_value(retval, value)}'
596
-
597
- hilda.log_info(log_message)
598
-
599
- for cmd in options.get('cmd', []):
600
- hilda.lldb_handle_command(cmd)
601
-
602
- if options.get('stop', False):
603
- hilda.log_info('Process remains stopped and focused on current thread')
604
- else:
605
- hilda.cont()
606
-
607
- return self.bp(address, callback, condition=condition, **options)
608
-
609
- def show_current_source(self):
489
+ def show_current_source(self) -> None:
610
490
  """ print current source code if possible """
611
491
  self.lldb_handle_command('f')
612
492
 
@@ -632,26 +512,7 @@ class HildaClient:
632
512
  if self.ui_manager.active:
633
513
  self.ui_manager.show()
634
514
 
635
- def remove_all_hilda_breakpoints(self, remove_forced=False):
636
- """
637
- Remove all breakpoints created by Hilda
638
- :param remove_forced: include removed of "forced" breakpoints
639
- """
640
- breakpoints = list(self.breakpoints.items())
641
- for bp_id, bp in breakpoints:
642
- if remove_forced or not bp.forced:
643
- self.remove_hilda_breakpoint(bp_id)
644
-
645
- def remove_hilda_breakpoint(self, bp_id: int) -> None:
646
- """
647
- Remove a single breakpoint placed by Hilda
648
- :param bp_id: Breakpoint's ID
649
- """
650
- self.target.BreakpointDelete(bp_id)
651
- del self.breakpoints[bp_id]
652
- self.log_info(f'BP #{bp_id} has been removed')
653
-
654
- def force_return(self, value=0):
515
+ def force_return(self, value: int = 0) -> None:
655
516
  """
656
517
  Prematurely return from a stack frame, short-circuiting exection of newer frames and optionally
657
518
  yielding a specified value.
@@ -661,11 +522,11 @@ class HildaClient:
661
522
  self.finish()
662
523
  self.set_register('x0', value)
663
524
 
664
- def proc_info(self):
525
+ def proc_info(self) -> None:
665
526
  """ Print information about currently running mapped process. """
666
527
  print(self.process)
667
528
 
668
- def print_proc_entitlements(self):
529
+ def print_proc_entitlements(self) -> None:
669
530
  """ Get the plist embedded inside the process' __LINKEDIT section. """
670
531
  linkedit_section = self.target.modules[0].FindSection('__LINKEDIT')
671
532
  linkedit_data = self.symbol(linkedit_section.GetLoadAddress(self.target)).peek(linkedit_section.size)
@@ -675,112 +536,24 @@ class HildaClient:
675
536
  entitlements = str(linkedit_data[linkedit_data.find(b'<?xml'):].split(b'\xfa', 1)[0], 'utf8')
676
537
  print(highlight(entitlements, XmlLexer(), TerminalTrueColorFormatter()))
677
538
 
678
- def bp(self, address_or_name: Union[int, str], callback: Optional[Callable] = None, condition: str = None,
679
- forced=False, module_name: Optional[str] = None, **options) -> HildaBreakpoint:
539
+ def bp(self, address_or_name: WhereType, callback: Optional[Callable] = None, condition: Optional[str] = None,
540
+ guarded: bool = False, description: Optional[str] = None, **options) -> HildaBreakpoint:
680
541
  """
681
542
  Add a breakpoint
682
- :param address_or_name:
543
+
544
+ Alias of self.breakpoints.add()
545
+
546
+ :param address_or_name: Where to place the breakpoint
683
547
  :param condition: set as a conditional breakpoint using lldb expression
684
548
  :param callback: callback(hilda, *args) to be called
685
- :param forced: whether the breakpoint should be protected frm usual removal.
686
- :param module_name: Specify module name to place the BP in (used with `address_or_name` when using a name)
549
+ :param guarded: whether the breakpoint should be protected frm usual removal.
550
+ :param description: Attach a breakpoint description
687
551
  :param options: can contain an `override` keyword to specify if to override an existing BP
688
552
  :return: native LLDB breakpoint
689
553
  """
690
- if address_or_name in [bp.address for bp in self.breakpoints.values()]:
691
- override = True if options.get('override', True) else False
692
- if override or prompts.prompt_for_confirmation('A breakpoint already exist in given location. '
693
- 'Would you like to delete the previous one?', True):
694
- breakpoints = list(self.breakpoints.items())
695
- for bp_id, bp in breakpoints:
696
- if address_or_name == bp.address:
697
- self.remove_hilda_breakpoint(bp_id)
698
-
699
- if isinstance(address_or_name, int):
700
- bp = self.target.BreakpointCreateByAddress(address_or_name)
701
- elif isinstance(address_or_name, str):
702
- bp = self.target.BreakpointCreateByName(address_or_name)
703
-
704
- if condition is not None:
705
- bp.SetCondition(condition)
706
-
707
- # add into Hilda's internal list of breakpoints
708
- self.breakpoints[bp.id] = HildaBreakpoint(self, bp, address=address_or_name, forced=forced, options=options,
709
- callback=callback)
710
-
711
- if callback is not None:
712
- bp.SetScriptCallbackFunction('lldb.hilda_client.bp_callback_router')
713
-
714
- self.log_info(f'Breakpoint #{bp.id} has been set')
715
- return self.breakpoints[bp.id]
716
-
717
- def bp_callback_router(self, frame, bp_loc, *_):
718
- """
719
- Route the breakpoint callback the specific breakpoint callback.
720
- :param lldb.SBFrame frame: LLDB Frame object.
721
- :param lldb.SBBreakpointLocation bp_loc: LLDB Breakpoint location object.
722
- """
723
- bp_id = bp_loc.GetBreakpoint().GetID()
724
- self._bp_frame = frame
725
- try:
726
- self.breakpoints[bp_id].callback(self, frame, bp_loc, self.breakpoints[bp_id].options)
727
- finally:
728
- self._bp_frame = None
729
-
730
- def show_hilda_breakpoints(self):
731
- """ Show existing breakpoints created by Hilda. """
732
- for bp_id, bp in self.breakpoints.items():
733
- print(f'🚨 Breakpoint #{bp_id}: Forced: {bp.forced}')
734
- if isinstance(bp.address, int):
735
- print(f'\tAddress: 0x{bp.address:x}')
736
- elif isinstance(bp.address, str):
737
- print(f'\tName: {bp.address}')
738
- print(f'\tOptions: {bp.options}')
739
-
740
- def save(self, filename=None):
741
- """
742
- Save loaded symbols map (for loading later using the load() command)
743
- :param filename: optional filename for where to store
744
- """
745
- if filename is None:
746
- filename = self._get_saved_state_filename()
747
-
748
- self.log_info(f'saving current state info: {filename}')
749
- with open(filename, 'wb') as f:
750
- symbols_copy = {}
751
- for k, v in self.symbols.items():
752
- # converting the symbols into serializable objects
753
- symbols_copy[k] = SerializableSymbol(address=int(v),
754
- type_=v.type_,
755
- filename=v.filename)
756
- pickle.dump(symbols_copy, f)
554
+ return self.breakpoints.add(address_or_name, callback, condition, guarded, description=description, **options)
757
555
 
758
- def load(self, filename=None):
759
- """
760
- Load an existing symbols map (previously saved by the save() command)
761
- :param filename: filename to load from
762
- """
763
- if filename is None:
764
- filename = self._get_saved_state_filename()
765
-
766
- self.log_info(f'loading current state from: {filename}')
767
- with open(filename, 'rb') as f:
768
- symbols_copy = pickle.load(f)
769
-
770
- for k, v in tqdm(symbols_copy.items()):
771
- self.symbols[k] = self.symbol(v.address)
772
-
773
- # perform sanity test for symbol rand
774
- if self.symbols.rand() == 0 and self.symbols.rand() == 0:
775
- # rand returning 0 twice means the loaded file is probably outdated
776
- raise BrokenLocalSymbolsJarError()
777
-
778
- # assuming the first main image will always change
779
- self.rebind_symbols(image_range=[0, 0])
780
- self.init_dynamic_environment()
781
- self._symbols_loaded = True
782
-
783
- def po(self, expression, cast=None):
556
+ def po(self, expression: str, cast: Optional[str] = None) -> str:
784
557
  """
785
558
  Print given object using LLDB's po command
786
559
 
@@ -804,7 +577,7 @@ class HildaClient:
804
577
  raise EvaluatingExpressionError(res.GetError())
805
578
  return res.GetOutput().strip()
806
579
 
807
- def globalize_symbols(self):
580
+ def globalize_symbols(self) -> None:
808
581
  """
809
582
  Make all symbols in python's global scope
810
583
  """
@@ -817,18 +590,18 @@ class HildaClient:
817
590
  and '.' not in name:
818
591
  self._add_global(name, value, reserved_names)
819
592
 
820
- def jump(self, symbol: int):
593
+ def jump(self, symbol: int) -> None:
821
594
  """ jump to given symbol """
822
595
  self.lldb_handle_command(f'j *{symbol}')
823
596
 
824
- def lldb_handle_command(self, cmd):
597
+ def lldb_handle_command(self, cmd: str) -> None:
825
598
  """
826
599
  Execute an LLDB command
827
600
 
828
601
  For example:
829
602
  lldb_handle_command('register read')
830
603
 
831
- :param cmd:
604
+ :param cmd: LLDB command
832
605
  """
833
606
  self.debugger.HandleCommand(cmd)
834
607
 
@@ -968,7 +741,7 @@ class HildaClient:
968
741
  return self.thread.GetSelectedFrame()
969
742
 
970
743
  @contextmanager
971
- def stopped(self, interval=0):
744
+ def stopped(self, interval: int = 0):
972
745
  """
973
746
  Context-Manager for execution while process is stopped.
974
747
  If interval is supplied, then if the device is in running state, it will sleep for the interval
@@ -988,7 +761,7 @@ class HildaClient:
988
761
  self.cont()
989
762
 
990
763
  @contextmanager
991
- def safe_malloc(self, size):
764
+ def safe_malloc(self, size: int):
992
765
  """
993
766
  Context-Manager for allocating a block of memory which is freed afterwards
994
767
  :param size:
@@ -1097,19 +870,36 @@ class HildaClient:
1097
870
  return
1098
871
  client.finish()
1099
872
  client.log_info(f'Desired module has been loaded: {expression}. Process remains stopped')
1100
- bp = bp_loc.GetBreakpoint()
1101
- client.remove_hilda_breakpoint(bp.id)
873
+ bp_id = bp_loc.GetBreakpoint().GetID()
874
+ client.breakpoints.remove(bp_id)
1102
875
 
1103
876
  self.bp('dlopen', bp)
1104
877
  self.cont()
1105
878
 
879
+ def show_help(self, *_) -> None:
880
+ """
881
+ Show banner help message
882
+ """
883
+ help_snippets = [HelpSnippet(key='p', description='Global to access all features')]
884
+ for keybinding in get_keybindings(self):
885
+ help_snippets.append(HelpSnippet(key=keybinding.key.upper(), description=keybinding.description))
886
+
887
+ for help_snippet in help_snippets:
888
+ click.echo(help_snippet)
889
+
1106
890
  def interact(self, additional_namespace: Optional[typing.Mapping] = None,
1107
891
  startup_files: Optional[list[str]] = None) -> None:
1108
892
  """ Start an interactive Hilda shell """
1109
893
  if not self._dynamic_env_loaded:
1110
894
  self.init_dynamic_environment()
1111
- print('\n')
1112
- self.log_info(html_to_ansi(GREETING))
895
+
896
+ # Show greeting
897
+ click.secho('Hilda has been successfully loaded! 😎', bold=True)
898
+ click.secho('Usage:', bold=True)
899
+ self.show_help()
900
+ click.echo(click.style('Have a nice flight ✈️! Starting an IPython shell...', bold=True))
901
+
902
+ # Configure and start IPython shell
1113
903
  ipython_config = Config()
1114
904
  ipython_config.IPCompleter.use_jedi = True
1115
905
  ipython_config.BaseIPythonApplication.profile = 'hilda'
@@ -1215,21 +1005,6 @@ class HildaClient:
1215
1005
  else:
1216
1006
  return value[0].peek_str()
1217
1007
 
1218
- def _monitor_format_value(self, fmt, value):
1219
- if callable(fmt):
1220
- return fmt(self, value)
1221
- formatters = {
1222
- 'x': lambda val: f'0x{int(val):x}',
1223
- 's': lambda val: val.peek_str() if val else None,
1224
- 'cf': lambda val: val.cf_description,
1225
- 'po': lambda val: val.po(),
1226
- 'std::string': self._std_string
1227
- }
1228
- if fmt in formatters:
1229
- return formatters[fmt](value)
1230
- else:
1231
- return f'{value:x} (unsupported format)'
1232
-
1233
1008
  @cached_property
1234
1009
  def _object_identifier(self) -> Symbol:
1235
1010
  return self.symbols.objc_getClass('VMUObjectIdentifier').objc_call('alloc').objc_call(
@@ -1,24 +1,47 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable
3
+
1
4
  from prompt_toolkit.enums import DEFAULT_BUFFER
2
5
  from prompt_toolkit.filters import EmacsInsertMode, HasFocus, HasSelection, ViInsertMode
3
6
  from prompt_toolkit.keys import Keys
4
7
 
5
8
 
9
+ @dataclass
10
+ class Keybinding:
11
+ key: str
12
+ description: str
13
+ callback: Callable
14
+
15
+
16
+ def get_keybindings(hilda_client) -> list[Keybinding]:
17
+ """
18
+ Get list of keybindings
19
+
20
+ :param hilda.hilda_client.HildaClient hilda_client: Hilda client to bind the keys to operations
21
+ """
22
+ return [
23
+ Keybinding(key=Keys.F1, description='Show this help', callback=hilda_client.show_help),
24
+ Keybinding(key=Keys.F2, description='Show process state UI', callback=hilda_client.ui_manager.show),
25
+ Keybinding(key=Keys.F3, description='Toggle enabling of stdout & stderr',
26
+ callback=hilda_client.toggle_enable_stdout_stderr),
27
+ Keybinding(key=Keys.F7, description='Step Into', callback=hilda_client.step_into),
28
+ Keybinding(key=Keys.F8, description='Step Over', callback=hilda_client.step_over),
29
+ Keybinding(key=Keys.F9, description='Continue',
30
+ callback=lambda _: (hilda_client.log_info('Sending continue'), hilda_client.cont())),
31
+ Keybinding(key=Keys.F10, description='Stop',
32
+ callback=lambda _: (hilda_client.log_info('Sending stop'), hilda_client.stop())),
33
+ ]
34
+
35
+
6
36
  def load_ipython_extension(ipython):
7
37
  def register_keybindings():
8
- hilda = ipython.user_ns['p']
9
- keys_mapping = {Keys.F1: hilda.ui_manager.show,
10
- Keys.F2: hilda.toggle_enable_stdout_stderr,
11
- Keys.F7: hilda.step_into,
12
- Keys.F8: hilda.step_over,
13
- Keys.F9: lambda _: (hilda.log_info('Sending continue'), hilda.cont()),
14
- Keys.F10: lambda _: (hilda.log_info('Sending stop'), hilda.stop())}
15
-
38
+ hilda_client = ipython.user_ns['p']
16
39
  insert_mode = ViInsertMode() | EmacsInsertMode()
17
40
  registry = ipython.pt_app.key_bindings
18
41
 
19
- for key, callback in keys_mapping.items():
20
- registry.add_binding(key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(
21
- callback)
42
+ for keybind in get_keybindings(hilda_client):
43
+ registry.add_binding(
44
+ keybind.key, filter=(HasFocus(DEFAULT_BUFFER) & ~HasSelection() & insert_mode))(keybind.callback)
22
45
 
23
46
  register_keybindings()
24
47
  ipython.events.register('shell_initialized', register_keybindings)