import curses import curses.textpad """TODOS: opening the drawing pad should create a queue to prevent race conditions Moving the cursor off the screen should draw it along the edge rather than the cursor freezing on the edge create_window_mode needs x and y clamped to the anchors to prevent crash """ PADY = 1000 # Absolute pad height PADX = 1000 # Absolute pad width SCREEN_WIDTH_IDX = None # Must be set after calling `curses.wrapper` SCREEN_HEIGHT_IDX = None # Must be set after calling `curses.wrapper` SCROLL_X, SCROLL_Y = 0, 0 # Pad will always be scrolled to top left def color_pair(fg, bg): """`color_pair` is a helper that replaces calls to `curses.color_pair` """ return curses.color_pair(fg + bg * 8) def refresh_stdscr(stdscr): """`refresh_stdscr` is a helper to refresh the pad. """ global SCREEN_WIDTH_IDX, SCREEN_HEIGHT_IDX, SCROLL_X, SCROLL_Y stdscr.refresh( SCROLL_Y, SCROLL_X, 0, 0, # Pad is always rendered from top-left side of screen SCREEN_HEIGHT_IDX, SCREEN_WIDTH_IDX) def write_note_mode(stdscr, selected_area_window, fg_color, bg_color): """`write_note_mode` is called by `create_window_mode` to allow the user to type text into the new window they created. It overwrites selected_area_window over stdscr """ selected_area_window.bkgd(" ", color_pair(fg_color, bg_color)) textbox = curses.textpad.Textbox(selected_area_window) textbox.edit() selected_area_window.overwrite(stdscr) return stdscr def set_color_mode(stdscr, selected_area_window): """`set_color_mode` is called by `create_window_mode` to change stdscr's color before passing it to `set_color_mode` """ fg_color, bg_color = 0, 0 while True: selected_area_window.bkgd( "X", color_pair(fg_color, bg_color)) selected_area_window.touchwin() selected_area_window.refresh() # Handle keypresses: keypress = stdscr.getch() match keypress: # Quit case 113: # 'q' return stdscr, selected_area_window, 0, 0 # Vim keys (h j k l) for changing color: # Down and Up - j and k - change foreground color # Left and Right - h and l - change background color case 104: # 'h' bg_color = (bg_color - 1) % 8 case 106: # 'j' fg_color = (fg_color - 1) % 8 case 107: # 'k' fg_color = (fg_color + 1) % 8 case 108: # 'l' bg_color = (bg_color + 1) % 8 # Choose currently selected color case 10: # 'Enter' return stdscr, selected_area_window, fg_color, bg_color return stdscr def create_window_mode(stdscr): """`create_window_mode` allows the user to draw a subwindow, then it passes control to `style_window_mode` """ # Initialize variables: global SCROLL_X, SCROLL_Y fg_color, bg_color = 0, 0 anchor_y, anchor_x = stdscr.getyx() # Coords used as anchor y, x = anchor_y, anchor_x selected_area_window = curses.newwin(1, 1, anchor_y, anchor_x) # Main loop: WINDOW_MODE_RUNNING = True while WINDOW_MODE_RUNNING: # Draw updates: selected_area_window.bkgd('*', color_pair(fg_color, bg_color)) refresh_stdscr(stdscr) # Update pad before derwin selected_area_window.touchwin() # Required to update color selected_area_window.refresh() # Handle keypresses: keypress = stdscr.getch() match keypress: # Quit: case 113: # 'q' return stdscr # Move to `write_note_mode`: case 10: # 'Enter' stdscr = write_note_mode( stdscr, selected_area_window, fg_color, bg_color) return stdscr # Move to `set_color_mode`: case 99: stdscr, selected_area_window, fg, bg = \ set_color_mode(stdscr, selected_area_window) fg_color, bg_color = fg, bg # Screen navigation: case 104: # 'h' x = max(x - 1, anchor_x) # Clamp to left selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 72: # 'H' x = max(x - 5, anchor_x) # Clamp to left selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 106: # 'j' y = min(y + 1, SCREEN_HEIGHT_IDX) # Clamp to bottom selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 74: # 'J' y = min(y + 5, SCREEN_HEIGHT_IDX) # Clamp to bottom selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 107: # 'k' y = max(y - 1, 0) # Clamp to top selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 75: # 'K' y = max(y - 5, 0) # Clamp to top selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 108: # 'l' x = min(x + 1, SCREEN_WIDTH_IDX) # Clamp to right selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) case 76: # 'L' x = min(x + 5, SCREEN_WIDTH_IDX) # Clamp to right selected_area_window.resize(y - anchor_y + 1, x - anchor_x + 1) stdscr.move(y, x) def main(): curses.wrapper(_main) def _main(stdscr): # Constants: global SCREEN_WIDTH_IDX, SCREEN_HEIGHT_IDX, PADY, PADX, SCROLL_X, SCROLL_Y SCREEN_WIDTH_IDX = curses.COLS - 1 # Adjusted for index SCREEN_HEIGHT_IDX = curses.LINES - 1 # Adjusted for index running = True # App state # Open old window data: try: with open("old.window", "rb") as file: file.seek(0, 2) # Seek end of file if file.tell(): file.seek(0) # Return to beginning stdscr = curses.getwin(file) # Load window except FileNotFoundError: # Ignore missing stdscr = curses.newpad(PADY, PADX) # Create new window # Initialize screen settings: stdscr.keypad(True) curses.start_color() for fg in range(0, 8): # Initialize all colors as pairs immediately for bg in range(0, 8): curses.init_pair(fg + bg * 8 + 1, fg, bg) y, x = stdscr.getyx() # Main loop: while running: # Draw state: refresh_stdscr(stdscr) # Handle input: keypress = stdscr.getch() match keypress: case 113: # 'q' with open("old.window", "wb+") as file: stdscr.putwin(file) running = False case 10: # 'ENTER' stdscr = create_window_mode(stdscr) # Screen navigation: case 104: # 'h' x = max(x - 1, 0) # Clamp to left edge of screen stdscr.move(y, x) case 72: # 'H' x = max(x - 5, 0) # Clamp to left edge of screen stdscr.move(y, x) case 106: # 'j' y = min(y + 1, PADX) # Clamp to bottom edge of pad stdscr.move(y, x) case 74: # 'J' y = min(y + 5, PADY) # Clamp to bottom edge of pad stdscr.move(y, x) case 107: # 'k' y = max(y - 1, 0) # Clamp to top edge of screen stdscr.move(y, x) case 75: # 'K' y = max(y - 5, 0) # Clamp to top edge of screen stdscr.move(y, x) case 108: # 'l' x = min(x + 1, PADX) # Clamp to right edge of pad stdscr.move(y, x) case 76: # 'L' x = min(x + 5, PADY) # Clamp to right edge of pad stdscr.move(y, x) # Scroll pane case curses.KEY_UP: # 'ARROW UP' SCROLL_Y = max(SCROLL_Y - 1, 0) # Clamp to top case curses.KEY_LEFT:# 'ARROW LEFT' SCROLL_X = max(SCROLL_X - 1, 0) # Clamp to left case curses.KEY_DOWN:# 'ARROW DOWN' SCROLL_Y = min(SCROLL_Y + 1, SCREEN_HEIGHT_IDX) # Clamp to bottom case curses.KEY_RIGHT: # 'ARROW RIGHT' SCROLL_X = min(SCROLL_X + 1, SCREEN_WIDTH_IDX) # Clamp to right case _: pass if __name__ == "__main__": main()