#!/usr/bin/env python """ A simple example of a Notepad-like text editor. """ import datetime from asyncio import Future, ensure_future from prompt_toolkit.application import Application from prompt_toolkit.application.current import get_app from prompt_toolkit.completion import PathCompleter from prompt_toolkit.filters import Condition from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.containers import ( ConditionalContainer, Float, HSplit, VSplit, Window, WindowAlign, ) from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.dimension import D from prompt_toolkit.layout.layout import Layout from prompt_toolkit.layout.menus import CompletionsMenu from prompt_toolkit.lexers import DynamicLexer, PygmentsLexer from prompt_toolkit.search import start_search from prompt_toolkit.styles import Style from prompt_toolkit.widgets import ( Button, Dialog, Label, MenuContainer, MenuItem, SearchToolbar, TextArea, ) class ApplicationState: """ Application state. For the simplicity, we store this as a global, but better would be to instantiate this as an object and pass at around. """ show_status_bar = True current_path = None def get_statusbar_text(): return " Press Ctrl-C to open menu. " def get_statusbar_right_text(): return " {}:{} ".format( text_field.document.cursor_position_row + 1, text_field.document.cursor_position_col + 1, ) search_toolbar = SearchToolbar() text_field = TextArea( lexer=DynamicLexer( lambda: PygmentsLexer.from_filename( ApplicationState.current_path or ".txt", sync_from_start=False ) ), scrollbar=True, line_numbers=True, search_field=search_toolbar, ) class TextInputDialog: def __init__(self, title="", label_text="", completer=None): self.future = Future() def accept_text(buf): get_app().layout.focus(ok_button) buf.complete_state = None return True def accept(): self.future.set_result(self.text_area.text) def cancel(): self.future.set_result(None) self.text_area = TextArea( completer=completer, multiline=False, width=D(preferred=40), accept_handler=accept_text, ) ok_button = Button(text="OK", handler=accept) cancel_button = Button(text="Cancel", handler=cancel) self.dialog = Dialog( title=title, body=HSplit([Label(text=label_text), self.text_area]), buttons=[ok_button, cancel_button], width=D(preferred=80), modal=True, ) def __pt_container__(self): return self.dialog class MessageDialog: def __init__(self, title, text): self.future = Future() def set_done(): self.future.set_result(None) ok_button = Button(text="OK", handler=(lambda: set_done())) self.dialog = Dialog( title=title, body=HSplit([Label(text=text)]), buttons=[ok_button], width=D(preferred=80), modal=True, ) def __pt_container__(self): return self.dialog body = HSplit( [ text_field, search_toolbar, ConditionalContainer( content=VSplit( [ Window( FormattedTextControl(get_statusbar_text), style="class:status" ), Window( FormattedTextControl(get_statusbar_right_text), style="class:status.right", width=9, align=WindowAlign.RIGHT, ), ], height=1, ), filter=Condition(lambda: ApplicationState.show_status_bar), ), ] ) # Global key bindings. bindings = KeyBindings() @bindings.add("c-c") def _(event): "Focus menu." event.app.layout.focus(root_container.window) # # Handlers for menu items. # def do_open_file(): async def coroutine(): open_dialog = TextInputDialog( title="Open file", label_text="Enter the path of a file:", completer=PathCompleter(), ) path = await show_dialog_as_float(open_dialog) ApplicationState.current_path = path if path is not None: try: with open(path, "rb") as f: text_field.text = f.read().decode("utf-8", errors="ignore") except OSError as e: show_message("Error", f"{e}") ensure_future(coroutine()) def do_about(): show_message("About", "Text editor demo.\nCreated by Jonathan Slenders.") def show_message(title, text): async def coroutine(): dialog = MessageDialog(title, text) await show_dialog_as_float(dialog) ensure_future(coroutine()) async def show_dialog_as_float(dialog): "Coroutine." float_ = Float(content=dialog) root_container.floats.insert(0, float_) app = get_app() focused_before = app.layout.current_window app.layout.focus(dialog) result = await dialog.future app.layout.focus(focused_before) if float_ in root_container.floats: root_container.floats.remove(float_) return result def do_new_file(): text_field.text = "" def do_exit(): get_app().exit() def do_time_date(): text = datetime.datetime.now().isoformat() text_field.buffer.insert_text(text) def do_go_to(): async def coroutine(): dialog = TextInputDialog(title="Go to line", label_text="Line number:") line_number = await show_dialog_as_float(dialog) try: line_number = int(line_number) except ValueError: show_message("Invalid line number") else: text_field.buffer.cursor_position = ( text_field.buffer.document.translate_row_col_to_index( line_number - 1, 0 ) ) ensure_future(coroutine()) def do_undo(): text_field.buffer.undo() def do_cut(): data = text_field.buffer.cut_selection() get_app().clipboard.set_data(data) def do_copy(): data = text_field.buffer.copy_selection() get_app().clipboard.set_data(data) def do_delete(): text_field.buffer.cut_selection() def do_find(): start_search(text_field.control) def do_find_next(): search_state = get_app().current_search_state cursor_position = text_field.buffer.get_search_position( search_state, include_current_position=False ) text_field.buffer.cursor_position = cursor_position def do_paste(): text_field.buffer.paste_clipboard_data(get_app().clipboard.get_data()) def do_select_all(): text_field.buffer.cursor_position = 0 text_field.buffer.start_selection() text_field.buffer.cursor_position = len(text_field.buffer.text) def do_status_bar(): ApplicationState.show_status_bar = not ApplicationState.show_status_bar # # The menu container. # root_container = MenuContainer( body=body, menu_items=[ MenuItem( "File", children=[ MenuItem("New...", handler=do_new_file), MenuItem("Open...", handler=do_open_file), MenuItem("Save"), MenuItem("Save as..."), MenuItem("-", disabled=True), MenuItem("Exit", handler=do_exit), ], ), MenuItem( "Edit", children=[ MenuItem("Undo", handler=do_undo), MenuItem("Cut", handler=do_cut), MenuItem("Copy", handler=do_copy), MenuItem("Paste", handler=do_paste), MenuItem("Delete", handler=do_delete), MenuItem("-", disabled=True), MenuItem("Find", handler=do_find), MenuItem("Find next", handler=do_find_next), MenuItem("Replace"), MenuItem("Go To", handler=do_go_to), MenuItem("Select All", handler=do_select_all), MenuItem("Time/Date", handler=do_time_date), ], ), MenuItem( "View", children=[MenuItem("Status Bar", handler=do_status_bar)], ), MenuItem( "Info", children=[MenuItem("About", handler=do_about)], ), ], floats=[ Float( xcursor=True, ycursor=True, content=CompletionsMenu(max_height=16, scroll_offset=1), ), ], key_bindings=bindings, ) style = Style.from_dict( { "status": "reverse", "shadow": "bg:#440044", } ) layout = Layout(root_container, focused_element=text_field) application = Application( layout=layout, enable_page_navigation_bindings=True, style=style, mouse_support=True, full_screen=True, ) def run(): application.run() if __name__ == "__main__": run()