@@ -172,7 +172,7 @@ class TextArea(ScrollView):
172172 &.-read-only .text-area--cursor {
173173 background: $warning-darken-1;
174174 }
175- }
175+ }
176176}
177177"""
178178
@@ -373,6 +373,15 @@ class TextArea(ScrollView):
373373 The document can still be edited programmatically via the API.
374374 """
375375
376+ show_cursor : Reactive [bool ] = reactive (True )
377+ """Show the cursor in read only mode?
378+
379+ If `False`, the cursor will be visible when `read_only==True`.
380+ If `True`, the cursor will not be visible when `read_only==True`, and the TextArea may be
381+ scroll like other containers.
382+
383+ """
384+
376385 compact : reactive [bool ] = reactive (False , toggle_class = "-textual-compact" )
377386 """Enable compact display?"""
378387
@@ -423,6 +432,7 @@ def __init__(
423432 soft_wrap : bool = True ,
424433 tab_behavior : Literal ["focus" , "indent" ] = "focus" ,
425434 read_only : bool = False ,
435+ show_cursor : bool = True ,
426436 show_line_numbers : bool = False ,
427437 line_number_start : int = 1 ,
428438 max_checkpoints : int = 50 ,
@@ -443,6 +453,7 @@ def __init__(
443453 soft_wrap: Enable soft wrapping.
444454 tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
445455 read_only: Enable read-only mode. This prevents edits using the keyboard.
456+ show_cursor: Show the cursor in read only mode (no effect otherwise).
446457 show_line_numbers: Show line numbers on the left edge.
447458 line_number_start: What line number to start on.
448459 max_checkpoints: The maximum number of undo history checkpoints to retain.
@@ -509,6 +520,7 @@ def __init__(
509520
510521 self .set_reactive (TextArea .soft_wrap , soft_wrap )
511522 self .set_reactive (TextArea .read_only , read_only )
523+ self .set_reactive (TextArea .show_cursor , show_cursor )
512524 self .set_reactive (TextArea .show_line_numbers , show_line_numbers )
513525 self .set_reactive (TextArea .line_number_start , line_number_start )
514526 self .set_reactive (TextArea .highlight_cursor_line , highlight_cursor_line )
@@ -542,6 +554,7 @@ def code_editor(
542554 soft_wrap : bool = False ,
543555 tab_behavior : Literal ["focus" , "indent" ] = "indent" ,
544556 read_only : bool = False ,
557+ show_cursor : bool = True ,
545558 show_line_numbers : bool = True ,
546559 line_number_start : int = 1 ,
547560 max_checkpoints : int = 50 ,
@@ -564,6 +577,8 @@ def code_editor(
564577 theme: The theme to use.
565578 soft_wrap: Enable soft wrapping.
566579 tab_behavior: If 'focus', pressing tab will switch focus. If 'indent', pressing tab will insert a tab.
580+ read_only: Enable read-only mode. This prevents edits using the keyboard.
581+ show_cursor: Show the cursor in read only mode (no effect otherwise).
567582 show_line_numbers: Show line numbers on the left edge.
568583 line_number_start: What line number to start on.
569584 name: The name of the `TextArea` widget.
@@ -581,6 +596,7 @@ def code_editor(
581596 soft_wrap = soft_wrap ,
582597 tab_behavior = tab_behavior ,
583598 read_only = read_only ,
599+ show_cursor = show_cursor ,
584600 show_line_numbers = show_line_numbers ,
585601 line_number_start = line_number_start ,
586602 max_checkpoints = max_checkpoints ,
@@ -1101,6 +1117,24 @@ def _refresh_size(self) -> None:
11011117 width , height = self .document .get_size (self .indent_width )
11021118 self .virtual_size = Size (width + self .gutter_width + 1 , height )
11031119
1120+ @property
1121+ def _draw_cursor (self ) -> bool :
1122+ """Draw the cursor?"""
1123+ if self .read_only :
1124+ # If we are in read only mode, we don't want the cursor to blink
1125+ return self .show_cursor and self .has_focus
1126+ draw_cursor = (
1127+ self .has_focus
1128+ and not self .cursor_blink
1129+ or (self .cursor_blink and self ._cursor_visible )
1130+ )
1131+ return draw_cursor
1132+
1133+ @property
1134+ def _has_cursor (self ) -> bool :
1135+ """Is there a usable cursor?"""
1136+ return not (self .read_only and not self .show_cursor )
1137+
11041138 def get_line (self , line_index : int ) -> Text :
11051139 """Retrieve the line at the given line index.
11061140
@@ -1158,6 +1192,7 @@ def render_line(self, y: int) -> Strip:
11581192 self .soft_wrap ,
11591193 self .show_line_numbers ,
11601194 self .read_only ,
1195+ self .show_cursor ,
11611196 )
11621197 if (cached_line := self ._line_cache .get (cache_key )) is not None :
11631198 return cached_line
@@ -1213,12 +1248,13 @@ def _render_line(self, y: int) -> Strip:
12131248 selection_top_row , selection_top_column = selection_top
12141249 selection_bottom_row , selection_bottom_column = selection_bottom
12151250
1216- cursor_line_style = theme .cursor_line_style if theme else None
1217- if (
1218- cursor_line_style
1219- and cursor_row == line_index
1220- and self .highlight_cursor_line
1221- ):
1251+ highlight_cursor_line = self .highlight_cursor_line and self ._has_cursor
1252+ cursor_line_style = (
1253+ theme .cursor_line_style if (theme and highlight_cursor_line ) else None
1254+ )
1255+ has_cursor = self ._has_cursor
1256+
1257+ if has_cursor and cursor_line_style and cursor_row == line_index :
12221258 line .stylize (cursor_line_style )
12231259
12241260 # Selection styling
@@ -1271,16 +1307,14 @@ def _render_line(self, y: int) -> Strip:
12711307 matching_bracket = self ._matching_bracket_location
12721308 match_cursor_bracket = self .match_cursor_bracket
12731309 draw_matched_brackets = (
1274- match_cursor_bracket and matching_bracket is not None and start == end
1310+ has_cursor
1311+ and match_cursor_bracket
1312+ and matching_bracket is not None
1313+ and start == end
12751314 )
12761315
12771316 if cursor_row == line_index :
1278- draw_cursor = (
1279- self .has_focus
1280- and not self .cursor_blink
1281- or (self .cursor_blink and self ._cursor_visible )
1282- and not self .read_only
1283- )
1317+ draw_cursor = self ._draw_cursor
12841318 if draw_matched_brackets :
12851319 matching_bracket_style = theme .bracket_matching_style if theme else None
12861320 if matching_bracket_style :
@@ -1312,7 +1346,7 @@ def _render_line(self, y: int) -> Strip:
13121346 # Build the gutter text for this line
13131347 gutter_width = self .gutter_width
13141348 if self .show_line_numbers :
1315- if cursor_row == line_index and self . highlight_cursor_line :
1349+ if cursor_row == line_index and highlight_cursor_line :
13161350 gutter_style = theme .cursor_line_gutter_style
13171351 else :
13181352 gutter_style = theme .gutter_style
@@ -1565,8 +1599,6 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None:
15651599 async def _on_key (self , event : events .Key ) -> None :
15661600 """Handle key presses which correspond to document inserts."""
15671601 self ._restart_blink ()
1568- if self .read_only :
1569- return
15701602
15711603 key = event .key
15721604 insert_values = {
@@ -1584,6 +1616,9 @@ async def _on_key(self, event: events.Key) -> None:
15841616 insert_values ["tab" ] = " " * self ._find_columns_to_next_tab_stop ()
15851617
15861618 if event .is_printable or key in insert_values :
1619+ if self .read_only :
1620+ self .app .bell ()
1621+ return
15871622 event .stop ()
15881623 event .prevent_default ()
15891624 insert = insert_values .get (key , event .character )
@@ -1775,6 +1810,8 @@ def scroll_cursor_visible(
17751810 Returns:
17761811 The offset that was scrolled to bring the cursor into view.
17771812 """
1813+ if not self ._has_cursor :
1814+ return Offset (0 , 0 )
17781815 self ._recompute_cursor_offset ()
17791816
17801817 x , y = self ._cursor_offset
@@ -1804,6 +1841,8 @@ def move_cursor(
18041841 so that we jump back to the same width the next time we move to a row
18051842 that is wide enough.
18061843 """
1844+ if not self ._has_cursor :
1845+ return
18071846 if select :
18081847 start , _end = self .selection
18091848 self .selection = Selection (start , location )
@@ -1948,6 +1987,9 @@ def action_cursor_left(self, select: bool = False) -> None:
19481987 Args:
19491988 select: If True, select the text while moving.
19501989 """
1990+ if not self ._has_cursor :
1991+ self .scroll_left ()
1992+ return
19511993 target = (
19521994 self .get_cursor_left_location ()
19531995 if select or self .selection .is_empty
@@ -1973,6 +2015,9 @@ def action_cursor_right(self, select: bool = False) -> None:
19732015 Args:
19742016 select: If True, select the text while moving.
19752017 """
2018+ if not self ._has_cursor :
2019+ self .scroll_right ()
2020+ return
19762021 target = (
19772022 self .get_cursor_right_location ()
19782023 if select or self .selection .is_empty
@@ -1994,6 +2039,9 @@ def action_cursor_down(self, select: bool = False) -> None:
19942039 Args:
19952040 select: If True, select the text while moving.
19962041 """
2042+ if not self ._has_cursor :
2043+ self .scroll_down ()
2044+ return
19972045 target = self .get_cursor_down_location ()
19982046 self .move_cursor (target , record_width = False , select = select )
19992047
@@ -2011,6 +2059,9 @@ def action_cursor_up(self, select: bool = False) -> None:
20112059 Args:
20122060 select: If True, select the text while moving.
20132061 """
2062+ if not self ._has_cursor :
2063+ self .scroll_up ()
2064+ return
20142065 target = self .get_cursor_up_location ()
20152066 self .move_cursor (target , record_width = False , select = select )
20162067
@@ -2024,6 +2075,9 @@ def get_cursor_up_location(self) -> Location:
20242075
20252076 def action_cursor_line_end (self , select : bool = False ) -> None :
20262077 """Move the cursor to the end of the line."""
2078+ if not self ._has_cursor :
2079+ self .scroll_x = self .max_scroll_x
2080+ return
20272081 location = self .get_cursor_line_end_location ()
20282082 self .move_cursor (location , select = select )
20292083
@@ -2037,6 +2091,9 @@ def get_cursor_line_end_location(self) -> Location:
20372091
20382092 def action_cursor_line_start (self , select : bool = False ) -> None :
20392093 """Move the cursor to the start of the line."""
2094+ if not self ._has_cursor :
2095+ self .scroll_x = 0
2096+ return
20402097 target = self .get_cursor_line_start_location (smart_home = True )
20412098 self .move_cursor (target , select = select )
20422099
@@ -2061,6 +2118,8 @@ def action_cursor_word_left(self, select: bool = False) -> None:
20612118 Args:
20622119 select: Whether to select while moving the cursor.
20632120 """
2121+ if not self .show_cursor :
2122+ return
20642123 if self .cursor_at_start_of_text :
20652124 return
20662125 target = self .get_cursor_word_left_location ()
@@ -2086,7 +2145,8 @@ def get_cursor_word_left_location(self) -> Location:
20862145
20872146 def action_cursor_word_right (self , select : bool = False ) -> None :
20882147 """Move the cursor right by a single word, skipping leading whitespace."""
2089-
2148+ if not self .show_cursor :
2149+ return
20902150 if self .cursor_at_end_of_text :
20912151 return
20922152
@@ -2121,6 +2181,9 @@ def get_cursor_word_right_location(self) -> Location:
21212181
21222182 def action_cursor_page_up (self ) -> None :
21232183 """Move the cursor and scroll up one page."""
2184+ if not self .show_cursor :
2185+ self .scroll_page_up ()
2186+ return
21242187 height = self .content_size .height
21252188 _ , cursor_location = self .selection
21262189 target = self .navigator .get_location_at_y_offset (
@@ -2132,6 +2195,9 @@ def action_cursor_page_up(self) -> None:
21322195
21332196 def action_cursor_page_down (self ) -> None :
21342197 """Move the cursor and scroll down one page."""
2198+ if not self .show_cursor :
2199+ self .scroll_page_down ()
2200+ return
21352201 height = self .content_size .height
21362202 _ , cursor_location = self .selection
21372203 target = self .navigator .get_location_at_y_offset (
0 commit comments