@@ -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 `True`, the cursor will be visible when `read_only==True`.
380+ If `False`, the cursor will be hidden when `read_only==True`, and the TextArea will
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
@@ -1564,7 +1598,9 @@ def _redo_batch(self, edits: Sequence[Edit]) -> None:
15641598
15651599 async def _on_key (self , event : events .Key ) -> None :
15661600 """Handle key presses which correspond to document inserts."""
1601+
15671602 self ._restart_blink ()
1603+
15681604 if self .read_only :
15691605 return
15701606
@@ -1775,6 +1811,8 @@ def scroll_cursor_visible(
17751811 Returns:
17761812 The offset that was scrolled to bring the cursor into view.
17771813 """
1814+ if not self ._has_cursor :
1815+ return Offset (0 , 0 )
17781816 self ._recompute_cursor_offset ()
17791817
17801818 x , y = self ._cursor_offset
@@ -1804,6 +1842,8 @@ def move_cursor(
18041842 so that we jump back to the same width the next time we move to a row
18051843 that is wide enough.
18061844 """
1845+ if not self ._has_cursor :
1846+ return
18071847 if select :
18081848 start , _end = self .selection
18091849 self .selection = Selection (start , location )
@@ -1948,6 +1988,9 @@ def action_cursor_left(self, select: bool = False) -> None:
19481988 Args:
19491989 select: If True, select the text while moving.
19501990 """
1991+ if not self ._has_cursor :
1992+ self .scroll_left ()
1993+ return
19511994 target = (
19521995 self .get_cursor_left_location ()
19531996 if select or self .selection .is_empty
@@ -1973,6 +2016,9 @@ def action_cursor_right(self, select: bool = False) -> None:
19732016 Args:
19742017 select: If True, select the text while moving.
19752018 """
2019+ if not self ._has_cursor :
2020+ self .scroll_right ()
2021+ return
19762022 target = (
19772023 self .get_cursor_right_location ()
19782024 if select or self .selection .is_empty
@@ -1994,6 +2040,9 @@ def action_cursor_down(self, select: bool = False) -> None:
19942040 Args:
19952041 select: If True, select the text while moving.
19962042 """
2043+ if not self ._has_cursor :
2044+ self .scroll_down ()
2045+ return
19972046 target = self .get_cursor_down_location ()
19982047 self .move_cursor (target , record_width = False , select = select )
19992048
@@ -2011,6 +2060,9 @@ def action_cursor_up(self, select: bool = False) -> None:
20112060 Args:
20122061 select: If True, select the text while moving.
20132062 """
2063+ if not self ._has_cursor :
2064+ self .scroll_up ()
2065+ return
20142066 target = self .get_cursor_up_location ()
20152067 self .move_cursor (target , record_width = False , select = select )
20162068
@@ -2024,6 +2076,9 @@ def get_cursor_up_location(self) -> Location:
20242076
20252077 def action_cursor_line_end (self , select : bool = False ) -> None :
20262078 """Move the cursor to the end of the line."""
2079+ if not self ._has_cursor :
2080+ self .scroll_end ()
2081+ return
20272082 location = self .get_cursor_line_end_location ()
20282083 self .move_cursor (location , select = select )
20292084
@@ -2037,6 +2092,9 @@ def get_cursor_line_end_location(self) -> Location:
20372092
20382093 def action_cursor_line_start (self , select : bool = False ) -> None :
20392094 """Move the cursor to the start of the line."""
2095+ if not self ._has_cursor :
2096+ self .scroll_home ()
2097+ return
20402098 target = self .get_cursor_line_start_location (smart_home = True )
20412099 self .move_cursor (target , select = select )
20422100
@@ -2061,6 +2119,8 @@ def action_cursor_word_left(self, select: bool = False) -> None:
20612119 Args:
20622120 select: Whether to select while moving the cursor.
20632121 """
2122+ if not self .show_cursor :
2123+ return
20642124 if self .cursor_at_start_of_text :
20652125 return
20662126 target = self .get_cursor_word_left_location ()
@@ -2086,7 +2146,8 @@ def get_cursor_word_left_location(self) -> Location:
20862146
20872147 def action_cursor_word_right (self , select : bool = False ) -> None :
20882148 """Move the cursor right by a single word, skipping leading whitespace."""
2089-
2149+ if not self .show_cursor :
2150+ return
20902151 if self .cursor_at_end_of_text :
20912152 return
20922153
@@ -2121,6 +2182,9 @@ def get_cursor_word_right_location(self) -> Location:
21212182
21222183 def action_cursor_page_up (self ) -> None :
21232184 """Move the cursor and scroll up one page."""
2185+ if not self .show_cursor :
2186+ self .scroll_page_up ()
2187+ return
21242188 height = self .content_size .height
21252189 _ , cursor_location = self .selection
21262190 target = self .navigator .get_location_at_y_offset (
@@ -2132,6 +2196,9 @@ def action_cursor_page_up(self) -> None:
21322196
21332197 def action_cursor_page_down (self ) -> None :
21342198 """Move the cursor and scroll down one page."""
2199+ if not self .show_cursor :
2200+ self .scroll_page_down ()
2201+ return
21352202 height = self .content_size .height
21362203 _ , cursor_location = self .selection
21372204 target = self .navigator .get_location_at_y_offset (
@@ -2289,6 +2356,9 @@ def action_delete_left(self) -> None:
22892356
22902357 If there's a selection, then the selected range is deleted."""
22912358
2359+ if self .read_only :
2360+ return
2361+
22922362 selection = self .selection
22932363 start , end = selection
22942364
@@ -2301,6 +2371,8 @@ def action_delete_right(self) -> None:
23012371 """Deletes the character to the right of the cursor and keeps the cursor at the same location.
23022372
23032373 If there's a selection, then the selected range is deleted."""
2374+ if self .read_only :
2375+ return
23042376
23052377 selection = self .selection
23062378 start , end = selection
@@ -2312,6 +2384,8 @@ def action_delete_right(self) -> None:
23122384
23132385 def action_delete_line (self ) -> None :
23142386 """Deletes the lines which intersect with the selection."""
2387+ if self .read_only :
2388+ return
23152389 self ._delete_cursor_line ()
23162390
23172391 def _delete_cursor_line (self ) -> EditResult | None :
0 commit comments