class NodeTree(TreeControl): nodenums = Reactive("") nnodes = Reactive(0) def __init__(self, label=None, *args): super().__init__(label, *args) self.nnodes = 0 self.original_label = label self.root.tree.guide_style = "green4" self._tree.style = Style(color="deep_sky_blue1") def __reinit__(self): # reinitialize tree for new nodes self._tree.label = self.root self.data = {} self.id = NodeID(0) self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {} self._tree = Tree(self.original_label) self.root: TreeNode[NodeDataType] = TreeNode(None, self.id, self, self._tree, self.original_label, {}) self._tree.label = self.root self.nodes[NodeID(self.id)] = self.root self.root.tree.guide_style = "blue" self._tree.style = Style(color="deep_sky_blue1") async def handle_tree_click(self, message: TreeClick[dict]): """Called in response to a tree click.""" new_active_node = message.node.data.get("node_id") # convert active node string to bytes bs.net.actnode(new_active_node) async def watch_nnodes(self, nnodes): # reinitialize if nodes exist if list(self.nodes.keys())[1:]: self.__reinit__() # reload nodes for node in self.nodenums: node_id = self.node_ids[self.nodenums.index(node)] await self.add(self.root.id, node, { "node_num": node, "node_id": node_id }) await self.root.expand() def set_nodedict(self, nodedict): self.nodenums = [] self.node_ids = [] for node_id, node in nodedict.items(): nodenum, node_id = node['num'], node_id self.nodenums.append(f'Node {nodenum}') self.node_ids.append(node_id) self.nnodes = len(nodedict)
class TextBox(Widget, FocusMixin): """textbox to subscribe to events, simply add `on_<name>_change` function to your widget/app e.g.: def __init__(self): self.tb = TextBox('hello_world') def on_hello_world_change(self, event): self.log(event.value) """ value: Reactive[str] = Reactive('') title: Reactive[str] = Reactive('') def __init__(self, name: str, title=''): super().__init__(name) self.title = title self.name = name self._height = 3 self._text_change_event = ValueEvent.make_new(f'{self.name}_change', bound=type(self)) def render(self): return Panel( Align.left(self._display_str), title=self.title, border_style='blue', box=box.DOUBLE if self.has_focus else box.ROUNDED, # style=None, height=self._height, ) @property def _display_str(self): return self.value async def on_key(self, event): k = event.key changed = True if k in {'delete', 'ctrl+h'}: self.value = self.value[:-1] elif k == 'escape': self.value = '' elif k in valid_chars: self.value += k else: changed = False if changed: await self.post_message(self._text_change_event(value=self.value))
class TextBoxOrig(Widget, FocusMixin): """base text box. replaced by the new `TextBox`""" style: Reactive[str] = Reactive('') data: Reactive[str] = Reactive('') height: Reactive[int | None] = Reactive(3) width: Reactive[int | None] = Reactive(None) title: Reactive[str] = Reactive('') def __init__(self, title='textbox', width=40, height=3): super().__init__() self.width = width self.height = height self.title = title self._cbs = [] async def on_key(self, event): k = event.key if k in {'delete', 'ctrl+h'}: self.data = self.data[:-1] elif k in valid_chars: self.data += k else: return False return True @property def _display_str(self): return self.data[-self.width:] def render(self): return Panel( Align.left(self._display_str, vertical='top'), title=self.title, border_style='blue', box=box.HEAVY if self.has_focus else box.ROUNDED, style=self.style, height=self.height, width=self.width, ) def register(self, *cb: Callable[[str], None]): """register callbacks to be alerted when data is updated""" self._cbs.extend(cb) def watch_data(self, value: str) -> None: """update registered callbacks""" for cb in self._cbs: cb(value)
class SmoothApp(App): """Demonstrates smooth animation. Press 'b' to see it in action.""" async def on_load(self) -> None: """Bind keys here.""" await self.bind("b", "toggle_sidebar", "Toggle sidebar") await self.bind("q", "quit", "Quit") show_bar = Reactive(False) def watch_show_bar(self, show_bar: bool) -> None: """Called when show_bar changes.""" self.bar.animate("layout_offset_x", 0 if show_bar else -40) def action_toggle_sidebar(self) -> None: """Called when user hits 'b' key.""" self.show_bar = not self.show_bar async def on_mount(self) -> None: """Build layout here.""" footer = Footer() self.bar = Placeholder(name="left") await self.view.dock(footer, edge="bottom") await self.view.dock(Placeholder(), Placeholder(), edge="top") await self.view.dock(self.bar, edge="left", size=40, z=1) self.bar.layout_offset_x = -40
class Traffic(Widget): table: Union[Reactive[Table], Table] = Reactive(Table()) def __init__(self, name=None): super().__init__(name) self.table = Table() def render(self) -> RenderableType: return self.table def set_traffic(self, trafict: dict, active_node_num: float): # add rows to table self.table = Table( title=f'[bold blue]Traffic for node # {active_node_num}', box=box.MINIMAL_DOUBLE_HEAD) # add the columns for col in trafict.keys(): self.table.add_column(col, header_style=Style(color="magenta")) ntraf = len(trafict['id']) if ntraf > 0: for i in range(ntraf): # limit traffic to 100 aircraft on screen if i > 100: break row = [] for col in trafict.keys(): table_value = trafict[col][i] row.append(table_value) self.table.add_row(*row, style=Style(color='bright_green', bgcolor="bright_black"))
class FeatureList(View, layout=VerticalLayout, can_focus=True): name = 'feature list' selection = Reactive(None) def __init__(self, feature_set: CompiledFeatureSet) -> None: super().__init__() self.feature_set = feature_set self.selection = None self.features = [] for i, feature in enumerate(self.feature_set.features): name = feature.name if not feature.take_value: rely_on = self.feature_set.get_dependencies(name) if rely_on: rely_on = " rely on \\[" + ', '.join(rely_on) + ']' else: rely_on = "" wg = CheckboxItem( self, key=feature.name, explain=feature.description + rely_on, enabled=self.feature_set.values.get(name), selected=False, ) self.features.append(wg) self.layout.add(wg) @property def features_num(self): return len(self.features) def handle_button_pressed(self, message: ButtonPressed) -> None: self.feature_set.set(message.sender.key, not message.sender.enabled) for feature in self.features: feature.enabled = self.feature_set.values[feature.key] def on_key(self, event: events.Key): if self.selection is None: if event.key == events.Keys.Up: self.selection = self.features_num - 1 elif event.key == events.Keys.Down: self.selection = 0 if event.key == events.Keys.Up or event.key == events.Keys.Down: self.features[self.selection].selected = True elif event.key == events.Keys.Up or event.key == events.Keys.Down: self.features[self.selection].selected = False if event.key == events.Keys.Up: self.selection = (self.selection + self.features_num - 1) % self.features_num if event.key == events.Keys.Down: self.selection = (self.selection + 1) % self.features_num self.features[self.selection].selected = True elif event.key == events.Keys.Enter or event.key == " ": self.handle_button_pressed( ButtonPressed(self.features[self.selection]))
class ToTrack(Widget): clicker: Reactive[Clickable] = Reactive(None) button: Reactive[ResetButton] = Reactive(None) def __init__(self, name: str | None = None, color='dark_green') -> None: super().__init__(name) self.clicker = Clickable(name) self.color = color self.button = ResetButton() def render(self): # g = Group(self.clicker, self.button) # return Panel( # g, # style=Style(bgcolor='white'), # ) st = Style(bgcolor=self.color) grid = Table(show_header=False, box=box.ROUNDED, show_lines=False, expand=True, title=self.name, style=st) grid.add_row(self.clicker, style=st) grid.add_row(self.button, style=st) return grid def on_click(self, event: events.Click) -> None: self.log(f'I HAVE CLICKED: {self._refresh_button_clicked(event)}') if self._refresh_button_clicked(event): self.clicker.reset() else: self.clicker.count += 1 self.refresh() def _refresh_button_clicked(self, event: events.Click): x, y = self.size self.log('>>>>>>>', event.x, event.y, x, y) return event.x in {35, 36} and event.y == 17
class Numbers(Widget): """The digital display of the calculator.""" value = Reactive("0") def render(self) -> RenderableType: """Build a Rich renderable to render the calculator display.""" return Padding( Align.right(FigletText(self.value), vertical="middle"), (0, 1), style="white on rgb(51,51,51)", )
class Echobox(ScrollView): text = Reactive("") async def watch_text(self, text): await self.update( Panel(Text(text), height=max(8, 2 + text.count('\n')), box=box.SIMPLE, style=Style(bgcolor="grey53"))) def set_text(self, text: str): self.text = text
class MouseOverMixin: _mouse_over: Reactive[bool] = Reactive(False) async def on_enter(self, event: events.Enter) -> None: self._mouse_over = True async def on_leave(self, event: events.Leave) -> None: self._mouse_over = False @property def mouse_over(self): return self._mouse_over
class NodeInfo(Widget): table: Union[Reactive[Table], Table] = Reactive(Table()) def __init__(self, name=None): super().__init__(name) self.table = Table() self.regular_style = Style(color='bright_green', bgcolor="bright_black") self.actnode_style = Style(color='bright_yellow', bgcolor="black") def render(self) -> RenderableType: return self.table def set_nodes(self, nodedict: dict): # add rows to table self.table = Table(title='[bold blue]Nodes', box=box.MINIMAL_DOUBLE_HEAD) self.table.add_column('Node #', header_style=Style(color="magenta")) self.table.add_column('Scenario', header_style=Style(color="magenta")) self.table.add_column('Time', header_style=Style(color="magenta")) self.table.add_column('Aircraft', header_style=Style(color="magenta")) self.table.add_column('Current conflicts', header_style=Style(color="magenta")) self.table.add_column('Total conflicts', header_style=Style(color="magenta")) self.table.add_column('Current LOS', header_style=Style(color="magenta")) self.table.add_column('Total LOS', header_style=Style(color="magenta")) for node_id, node in nodedict.items(): if bs.net.act == node_id: self.table.add_row(node['num'], node['scenename'], node['time'], f'{node["nair"]}', f'{node["nconf_cur"]}', f'{node["nconf_tot"]}', f'{node["nlos_cur"]}', f'{node["nlos_tot"]}', style=self.actnode_style) else: self.table.add_row(node['num'], node['scenename'], node['time'], f'{node["nair"]}', f'{node["nconf_cur"]}', f'{node["nconf_tot"]}', f'{node["nlos_cur"]}', f'{node["nlos_tot"]}', style=self.regular_style)
class Textline(Widget): text = Reactive("") style = Style(bgcolor="grey37") def __init__(self, text="", name=None): super().__init__(name) self.text = text def render(self) -> RenderableType: return self.text def set_text(self, text): self.text = text
class Hover(Widget): mouse_over = Reactive(False) def render(self) -> Panel: return Panel("Hello [b]World[/b]", style=("on red" if self.mouse_over else "")) def on_enter(self) -> None: self.mouse_over = True def on_leave(self) -> None: self.mouse_over = False
class EchoInfo(Widget, can_focus=True): has_focus = Reactive(False) mouse_over = Reactive(False) style = Reactive("") height = Reactive(None) def __init__(self, name=None, height=None): super().__init__(name=name) self.height = height self.pretty_text = Text('BlueSky Console Client', style=Style(color='blue', bold=True), justify='center') def __rich_repr__(self) -> rich.repr.Result: yield "name", self.name def render(self) -> RenderableType: return Panel( Align.center(self.pretty_text, vertical="middle"), # title=self.name, border_style="green" if self.mouse_over else "blue", box=box.HEAVY if self.has_focus else box.ROUNDED, style=self.style, height=self.height, ) async def on_focus(self, event: events.Focus): self.has_focus = True async def on_blur(self, event: events.Blur): self.has_focus = False async def on_enter(self, event: events.Enter): self.mouse_over = True async def on_leave(self, event: events.Leave): self.mouse_over = False
class FocusMixin: _has_focus: Reactive[bool] = Reactive(False) async def on_focus(self, event: events.Focus) -> None: self.log('>>>>>> HAS FOCUS NOW') self._has_focus = True async def on_blur(self, event: events.Blur) -> None: self.log('>>>>>> BLURRED NOW') self._has_focus = False @property def has_focus(self): return self._has_focus
class CheckboxItem(Widget): enabled = Reactive(False) selected = Reactive(False) def __init__(self, parent: Widget, *, key: str, explain: str, enabled: bool, selected: bool = False) -> None: super().__init__() self._parent = parent self.key = key self.explain = explain self.enabled = enabled self.selected = selected def __rich_repr__(self) -> rich.repr.Result: yield "enabled", self.enabled yield "label", self.key def render(self) -> Table: grid = Table.grid(padding=1) grid.add_column() grid.add_row( "\\[x]" if self.enabled else "[ ]", self.key, self.explain, style=Style(reverse=self.selected), ) return grid async def on_click(self, event: events.Click) -> None: event.prevent_default().stop() await self.emit(ButtonPressed(self))
class Clickable(Widget): """click tally""" count: Reactive[int] = Reactive(95) title: Reactive[str] = Reactive('') def __init__(self, name): super().__init__(name) self.title = name self._button = ResetButton() def render(self): return Align.center(num_to_str(self.count), vertical='middle', height=15) # def __rich_console__(self, console: Console, options: ConsoleOptions): # yield Align.center(num_to_str(self.count), vertical='middle') def reset(self): self.log('RESETTING') self.count = 0 async def on_click(self, event: events.Click) -> None: self.count += 1
class CheckBox(Button): checked: Reactive[bool] = Reactive(False) def __init__(self, label, name: str = '', check_style: CheckStyle = None): name = name or label super().__init__(label, name or label) self.checked = False self.check_style = check_style or CheckStyle.random() self._event = ValueEvent.make_new(f'{self.name}_change', bound=type(self)) def render(self): t = Table('check', 'label', show_header=False, border_style=None, box=None) t.add_row(self.check_style[self.checked], self.label) return t async def on_click(self, event: events.Click) -> None: self.checked = not self.checked await self.post_message(self._event(self.checked))
class Tile(Widget): mouse_over = Reactive(False) def __init__(self, name: str | None = None, num: int | None = None) -> None: super().__init__(name) self._text: str = "" self._num = num def render(self) -> Panel: return Panel( Align.center(FigletText(self._text), vertical="middle"), style=("on red" if self.mouse_over else ""), ) async def on_enter(self) -> None: self.mouse_over = True async def on_leave(self) -> None: self.mouse_over = False async def on_click(self, event: events.Click) -> None: await self.app.make_turn(self._num)
class ConsoleUI(App): cmdtext: Union[Reactive[str], str] = Reactive("") echotext: Union[Reactive[str], str] = Reactive("") infotext: Union[Reactive[str], str] = Reactive("") nodedict = Reactive(dict()) nodetimes = Reactive("0") trafsimt = Reactive("") ntraf = Reactive(0) nnodes = Reactive(0) active_node_num = Reactive(0) cmdbox: Textline echobox: Echobox infoline: Textline echoinfo: EchoInfo nodeinfo: NodeInfo traffic: Traffic tree: NodeTree instance: App def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) ConsoleUI.instance = self def echo(self, text, flags=None): self.echotext = text + '\n' + self.echotext def set_infoline(self, text): self.infotext = text def set_nodes(self, nodes, nodetimes): self.nodedict = nodes self.nnodes = len(nodes) self.nodetimes = nodetimes def set_traffic(self, gen_data, traffic, active_node): self.trafdict = traffic self.trafsimt = str(gen_data['simt']) self.ntraf = len(traffic['id']) self.active_node_num = self.nodedict[active_node][ 'num'] if self.nodedict else 1 async def on_key(self, key: events.Key): if key.key == Keys.ControlH: self.cmdtext = self.cmdtext[:-1] elif key.key == Keys.Delete: self.cmdtext = "" elif key.key == Keys.Enter: self.echotext = self.cmdtext + '\n' + self.echotext bs.stack.stack(self.cmdtext) self.cmdtext = "" elif key.key == Keys.Escape: await self.action_quit() elif key.key in string.printable: self.cmdtext += key.key async def watch_infotext(self, infotext): self.infoline.set_text(f"[black]Current node:[/black] {infotext}") async def watch_cmdtext(self, cmdtext): self.cmdbox.set_text(f"[blue]>>[/blue] {cmdtext}") async def watch_nodetimes(self, nodetimes): self.nodeinfo.set_nodes(self.nodedict) self.tree.set_nodedict(self.nodedict) async def watch_nnodes(self, nnodes): await self.nodebody.update(self.nodeinfo) async def watch_echotext(self, echotext): self.echobox.set_text(echotext) async def watch_ntraf(self, ntraf): await self.trafbody.update(self.traffic) async def watch_trafsimt(self, trafsimt): self.traffic.set_traffic(self.trafdict, self.active_node_num) async def watch_active_node_num(self, active_node_num): self.nodeinfo.set_nodes(self.nodedict) async def on_mount(self, event: events.Mount): self.cmdbox = Textline("[blue]>>[/blue]") self.echobox = Echobox( Panel(Text(), height=8, box=box.SIMPLE, style=Style(bgcolor="grey53"))) self.infoline = Textline("[black]Current node: [/black]") self.echoinfo = EchoInfo("BlueSky") self.nodeinfo = NodeInfo(name="nodeinfo") self.traffic = Traffic(name="traffic") self.tree = NodeTree("Switch Nodes", {}) await self.bind(Keys.Escape, "quit", "Quit") await self.bind(Keys.ControlT, "view.toggle('trafficbody')", "Show traffic") await self.bind(Keys.ControlB, "view.toggle('nodedock')", "Show batch") await self.view.dock(Footer(), edge="bottom", size=1) await self.view.dock(self.cmdbox, edge="bottom", size=1) echorow = DockView() await echorow.dock(self.echoinfo, edge="right", size=20) await echorow.dock(self.echobox, edge="left") await self.view.dock(echorow, edge="bottom", size=8) await self.view.dock(self.infoline, edge="bottom", size=1) self.trafbody = ScrollView(self.traffic, name="trafficbody") await self.view.dock(self.trafbody, edge='top') nodedock = DockView(name='nodedock') self.nodebody = ScrollView(self.nodeinfo, name="nodeinfobody") await nodedock.dock(self.nodebody, edge='left', size=90) # Add the node tree to a scroll view self.treebody = ScrollView(self.tree) await nodedock.dock(self.treebody) await self.view.dock(nodedock, edge="right") await self.set_focus(self.cmdbox) self.set_interval(0.2, bs.net.update, name='Network')
class EmojiResults(Widget, FocusMixin): """filtered panel of emoji""" width: Reactive[int | None] = Reactive(None) data: Reactive[str] = Reactive('') style: Reactive[str] = Reactive('') offset: Reactive[int] = Reactive(0) def __init__(self, width=50): super().__init__() self._emoji = [] self._title_override = '' self.border_style = 'red' self.width = width self._orig_border_style = self.border_style # keeps border updating functions from overlapping self._last_copied_cxl_ref = [False] @property def _title_str(self): return self._title_override or f'emoji results ({self._num_matching})' @property def _num_matching(self): return sum(self.data.lower() in s.lower() for s in _emoji) @property def height(self): return self.size.height def render(self): return Panel( Align.left(self._emoji_str), title=self._title_str, border_style=self.border_style, style=self.style, height=min(self.height, len(self._emoji) + 2), width=self.width, ) @property def _emoji_str(self) -> str: self._emoji = self._get_emoji(self.data, limit=self.height, offset=self.offset) return '\n'.join(s[:self.width - 10] for s in self._emoji) async def on_key(self, event): k = event.key if k == 'down': self.down() elif k == 'up': self.up() else: self.offset = 0 async def on_mouse_scroll_up(self, event): self.down(3) async def on_mouse_scroll_down(self, event): self.up(3) async def on_click(self, event: events.Click) -> None: """copy emoji to clipboard""" info = val = None try: idx = event.y - 1 if idx < self.height - 2: # subtract 2 for the borders val = self._emoji[idx].split()[0] pyperclip.copy(val) info = f'copied {val} to clipboard' border_style = 'green' except IndexError: pass except Exception: val = 'error' info = 'unable to copy' border_style = 'purple' if info: self._alert_copied(info, border_style) # noqa self.log(f'bleep {val}') def _alert_copied(self, info, border_style): """change border/title to reflect that copying has occurred""" self._title_override = info self.border_style = border_style self.refresh() self._last_copied_cxl_ref[0] = True self._last_copied_cxl_ref = canceled = [False] def _reset_title(): if canceled[0]: return self._title_override = '' self.border_style = self._orig_border_style self.refresh() self.set_timer(1, _reset_title) def down(self, amount=1): if len(self._emoji) > 1: self.offset += amount def up(self, amount=1): self.offset -= amount def validate_offset(self, v): return max(v, 0) @staticmethod def _get_emoji(s='', limit=0, offset=0): s = s.lower() limited = list if limit > 0: limited = partial(take, limit) emoji = (f'{e} {name}' for name, e in _emoji.items() if s in name.lower()) take(offset, emoji) return limited(emoji)
class Calculator(GridView): """A working calculator app.""" DARK = "white on rgb(51,51,51)" LIGHT = "black on rgb(165,165,165)" YELLOW = "white on rgb(255,159,7)" BUTTON_STYLES = { "AC": LIGHT, "C": LIGHT, "+/-": LIGHT, "%": LIGHT, "/": YELLOW, "X": YELLOW, "-": YELLOW, "+": YELLOW, "=": YELLOW, } display = Reactive("0") show_ac = Reactive(True) def watch_display(self, value: str) -> None: """Called when self.display is modified.""" # self.numbers is a widget that displays the calculator result # Setting the attribute value changes the display # This allows us to write self.display = "100" to update the display self.numbers.value = value def compute_show_ac(self) -> bool: """Compute show_ac reactive value.""" # Condition to show AC button over C return self.value in ("", "0") and self.display == "0" def watch_show_ac(self, show_ac: bool) -> None: """When the show_ac attribute change we need to update the buttons.""" # Show AC and hide C or vice versa self.c.visible = not show_ac self.ac.visible = show_ac def on_mount(self) -> None: """Event when widget is first mounted (added to a parent view).""" # Attributes to store the current calculation self.left = Decimal("0") self.right = Decimal("0") self.value = "" self.operator = "+" # The calculator display self.numbers = Numbers() self.numbers.style_border = "bold" def make_button(text: str, style: str) -> Button: """Create a button with the given Figlet label.""" return Button(FigletText(text), style=style, name=text) # Make all the buttons self.buttons = { name: make_button(name, self.BUTTON_STYLES.get(name, self.DARK)) for name in "+/-,%,/,7,8,9,X,4,5,6,-,1,2,3,+,.,=".split(",") } # Buttons that have to be treated specially self.zero = make_button("0", self.DARK) self.ac = make_button("AC", self.LIGHT) self.c = make_button("C", self.LIGHT) self.c.visible = False # Set basic grid settings self.grid.set_gap(2, 1) self.grid.set_gutter(1) self.grid.set_align("center", "center") # Create rows / columns / areas self.grid.add_column("col", max_size=30, repeat=4) self.grid.add_row("numbers", max_size=15) self.grid.add_row("row", max_size=15, repeat=5) self.grid.add_areas( clear="col1,row1", numbers="col1-start|col4-end,numbers", zero="col1-start|col2-end,row5", ) # Place out widgets in to the layout self.grid.place(clear=self.c) self.grid.place(*self.buttons.values(), clear=self.ac, numbers=self.numbers, zero=self.zero) def handle_button_pressed(self, message: ButtonPressed) -> None: """A message sent by the button widget""" assert isinstance(message.sender, Button) button_name = message.sender.name def do_math() -> None: """Does the math: LEFT OPERATOR RIGHT""" self.log(self.left, self.operator, self.right) try: if self.operator == "+": self.left += self.right elif self.operator == "-": self.left -= self.right elif self.operator == "/": self.left /= self.right elif self.operator == "X": self.left *= self.right self.display = str(self.left) self.value = "" self.log("=", self.left) except Exception: self.display = "Error" if button_name.isdigit(): self.display = self.value = self.value.lstrip("0") + button_name elif button_name == "+/-": self.display = self.value = str(Decimal(self.value or "0") * -1) elif button_name == "%": self.display = self.value = str( Decimal(self.value or "0") / Decimal(100)) elif button_name == ".": if "." not in self.value: self.display = self.value = (self.value or "0") + "." elif button_name == "AC": self.value = "" self.left = self.right = Decimal(0) self.operator = "+" self.display = "0" elif button_name == "C": self.value = "" self.display = "0" elif button_name in ("+", "-", "/", "X"): self.right = Decimal(self.value or "0") do_math() self.operator = button_name elif button_name == "=": if self.value: self.right = Decimal(self.value) do_math()