返回 CodeWhale
posture.rs
1 //! The two rows beneath Codewhale's composer, extracted from the current
2 //! TUI's `phase_strip.rs::TidelineFooter` and `infoline.rs::InfoLine`.
3 //!
4 //! The posture bar says permission, mode and current activity. The metrics
5 //! line says model, context and measured numbers. Both take caller facts;
6 //! neither discovers routes, counts work, starts a clock or dispatches keys.
7
8 use std::borrow::Cow;
9
10 use ratatui::{
11 buffer::Buffer,
12 layout::{Margin, Rect},
13 style::{Modifier, Style},
14 };
15 use unicode_segmentation::UnicodeSegmentation;
16
17 use crate::{Paint, Role, Theme, TuiInk, glyphs, text};
18
19 const ITEM_JOIN: &str = " ";
20 const COUNT_JOIN: &str = ", ";
21 const HELP_GAP: usize = 2;
22 const SHED_TURN_CLOCK: u8 = 1;
23 const SHED_SESSION_CLOCK: u8 = 2;
24 const SHED_PERMISSION_KEY: u8 = 3;
25 const SHED_HINT: u8 = 4;
26 const SHED_COUNTS: u8 = 5;
27 const SHED_CAP_WARNING: u8 = 6;
28 const SHED_MODE_KEY: u8 = 7;
29 const SHED_MODE: u8 = 8;
30 const MAX_SHED: u8 = SHED_MODE;
31 const COMPACT_SHED: u8 = SHED_COUNTS;
32
33 /// One caller-authored posture word and its semantic ink.
34 #[derive(Clone, Debug)]
35 pub struct PostureFact<'a> {
36 pub text: Cow<'a, str>,
37 pub role: Role,
38 pub ink: Option<TuiInk>,
39 }
40
41 impl<'a> PostureFact<'a> {
42 #[must_use]
43 pub fn new(value: impl Into<Cow<'a, str>>, role: Role) -> Self {
44 Self {
45 text: value.into(),
46 role,
47 ink: None,
48 }
49 }
50 /// Choose an exact native permission, mode or status ink.
51 #[must_use]
52 pub fn ink(mut self, ink: TuiInk) -> Self {
53 self.ink = Some(ink);
54 self
55 }
56 }
57
58 /// Codewhale's one-row posture bar, immediately below the composer.
59 ///
60 /// Permission is the floor. Optional clocks, keys, hints and counts shed
61 /// before mode; the right notice never covers permission. At 80% context,
62 /// the native `surface soon — /compact` warning replaces the ordinary hint.
63 #[derive(Clone, Debug)]
64 pub struct PostureBar<'a> {
65 pub permission: PostureFact<'a>,
66 pub permission_key: Option<Cow<'a, str>>,
67 pub mode: Option<PostureFact<'a>>,
68 pub mode_key: Option<Cow<'a, str>>,
69 pub turn_clock: Option<PostureFact<'a>>,
70 pub counts: Vec<PostureFact<'a>>,
71 pub session_clock: Option<PostureFact<'a>>,
72 pub hint: Option<PostureFact<'a>>,
73 pub context_percent: u8,
74 pub cap_warning: Cow<'a, str>,
75 pub right: Option<PostureFact<'a>>,
76 pub compact: bool,
77 }
78
79 impl<'a> PostureBar<'a> {
80 #[must_use]
81 pub fn new(permission: impl Into<Cow<'a, str>>) -> Self {
82 Self {
83 permission: PostureFact::new(permission, Role::Attention).ink(TuiInk::PermissionAsk),
84 permission_key: None,
85 mode: None,
86 mode_key: None,
87 turn_clock: None,
88 counts: Vec::new(),
89 session_clock: None,
90 hint: None,
91 context_percent: 0,
92 cap_warning: Cow::Borrowed("surface soon — /compact"),
93 right: None,
94 compact: false,
95 }
96 }
97
98 #[must_use]
99 pub fn permission_role(mut self, role: Role) -> Self {
100 self.permission.role = role;
101 self.permission.ink = None;
102 self
103 }
104
105 #[must_use]
106 pub fn permission_ink(mut self, ink: TuiInk) -> Self {
107 self.permission.ink = Some(ink);
108 self
109 }
110
111 #[must_use]
112 pub fn mode_ink(mut self, value: impl Into<Cow<'a, str>>, ink: TuiInk) -> Self {
113 self.mode = Some(PostureFact::new(value, ink.fallback_role()).ink(ink));
114 self
115 }
116
117 #[must_use]
118 pub fn permission_key(mut self, value: impl Into<Cow<'a, str>>) -> Self {
119 self.permission_key = Some(value.into());
120 self
121 }
122
123 #[must_use]
124 pub fn mode(mut self, value: impl Into<Cow<'a, str>>, role: Role) -> Self {
125 self.mode = Some(PostureFact::new(value, role));
126 self
127 }
128
129 #[must_use]
130 pub fn mode_key(mut self, value: impl Into<Cow<'a, str>>) -> Self {
131 self.mode_key = Some(value.into());
132 self
133 }
134
135 #[must_use]
136 pub fn turn_clock(mut self, value: impl Into<Cow<'a, str>>, role: Role) -> Self {
137 self.turn_clock = Some(PostureFact::new(value, role));
138 self
139 }
140
141 #[must_use]
142 pub fn session_clock(mut self, value: impl Into<Cow<'a, str>>, role: Role) -> Self {
143 self.session_clock = Some(PostureFact::new(value, role));
144 self
145 }
146
147 #[must_use]
148 pub fn counts(mut self, values: Vec<PostureFact<'a>>) -> Self {
149 self.counts = values;
150 self
151 }
152
153 #[must_use]
154 pub fn hint(mut self, value: impl Into<Cow<'a, str>>, role: Role) -> Self {
155 self.hint = Some(PostureFact::new(value, role));
156 self
157 }
158
159 #[must_use]
160 pub fn right(mut self, value: impl Into<Cow<'a, str>>, role: Role) -> Self {
161 self.right = Some(PostureFact::new(value, role));
162 self
163 }
164
165 #[must_use]
166 pub fn context_percent(mut self, percent: u8) -> Self {
167 self.context_percent = percent.min(100);
168 self
169 }
170
171 /// Localize the native cap warning without changing its priority.
172 #[must_use]
173 pub fn cap_warning(mut self, value: impl Into<Cow<'a, str>>) -> Self {
174 self.cap_warning = value.into();
175 self
176 }
177
178 #[must_use]
179 pub fn compact(mut self, compact: bool) -> Self {
180 self.compact = compact;
181 self
182 }
183
184 fn at_context_cap(&self) -> bool {
185 self.context_percent >= 80
186 }
187
188 fn items(&self, shed: u8, ascii: bool) -> Vec<PostureItem> {
189 let mut items = vec![PostureItem {
190 text: format!(
191 "{} {}",
192 project(glyphs::CURRENT, ascii),
193 project(&self.permission.text, ascii)
194 ),
195 key: self
196 .permission_key
197 .as_deref()
198 .filter(|_| shed < SHED_PERMISSION_KEY)
199 .map(|key| format!(" {}", project(key, ascii))),
200 role: self.permission.role,
201 ink: self.permission.ink,
202 bold: true,
203 joined: false,
204 count_index: None,
205 }];
206 if let Some(mode) = self.mode.as_ref().filter(|_| shed < SHED_MODE) {
207 items.push(PostureItem {
208 text: project(&mode.text, ascii),
209 key: self
210 .mode_key
211 .as_deref()
212 .filter(|_| shed < SHED_MODE_KEY)
213 .map(|key| format!(" ({})", project(key, ascii))),
214 role: mode.role,
215 ink: mode.ink,
216 bold: false,
217 joined: false,
218 count_index: None,
219 });
220 }
221 // Preserve the native turn-only clock rung: removing the session
222 // half must not make the remaining clock shed a rung too early.
223 let turn_shed = if self.session_clock.is_none() {
224 SHED_SESSION_CLOCK
225 } else {
226 SHED_TURN_CLOCK
227 };
228 if let Some(clock) = self.turn_clock.as_ref().filter(|_| shed < turn_shed) {
229 items.push(PostureItem::fact(clock, ascii));
230 }
231 if shed < SHED_COUNTS {
232 for (index, count) in self.counts.iter().enumerate() {
233 let count_text = project(&count.text, ascii);
234 let (value, key) = match count_text.strip_suffix(" (Ctrl+])") {
235 Some(label) => (label.to_owned(), Some(" (Ctrl+])".to_owned())),
236 None => (count_text, None),
237 };
238 items.push(PostureItem {
239 text: value,
240 key,
241 role: count.role,
242 ink: count.ink,
243 bold: false,
244 joined: index > 0,
245 count_index: Some(index),
246 });
247 }
248 }
249 if let Some(clock) = self
250 .session_clock
251 .as_ref()
252 .filter(|_| shed < SHED_SESSION_CLOCK)
253 {
254 items.push(PostureItem::fact(clock, ascii));
255 }
256 let hint_rung = if self.at_context_cap() {
257 SHED_CAP_WARNING
258 } else {
259 SHED_HINT
260 };
261 if shed < hint_rung {
262 if self.at_context_cap() {
263 items.push(PostureItem::fact(
264 &PostureFact::new(format!("▲ {}", self.cap_warning), Role::Attention)
265 .ink(TuiInk::Warning),
266 ascii,
267 ));
268 } else if let Some(hint) = &self.hint {
269 items.push(PostureItem::fact(hint, ascii));
270 }
271 }
272 items
273 }
274
275 fn layout(&self, area: Rect, theme: &Theme) -> Option<PostureLayout> {
276 if area.width < 8 || area.height == 0 {
277 return None;
278 }
279 let area = area.inner(Margin::new(1, 0));
280 let ascii = theme.ascii();
281 let width = usize::from(area.width);
282 let floor = left_width(&self.items(MAX_SHED, ascii));
283 let right = self.right.as_ref().map(|fact| {
284 (
285 clip_native(&project(&fact.text, ascii), width.saturating_sub(floor + 1)),
286 fact.role,
287 )
288 });
289 let right_width = right
290 .as_ref()
291 .map_or(0, |(value, _)| text::width(value) + 1);
292 let budget = width.saturating_sub(right_width);
293 let first = if self.compact { COMPACT_SHED } else { 0 };
294 let items = (first..=MAX_SHED)
295 .map(|shed| self.items(shed, ascii))
296 .find(|items| left_width(items) <= budget)
297 .unwrap_or_else(|| self.items(MAX_SHED, ascii));
298 Some(PostureLayout {
299 area,
300 budget,
301 items,
302 right,
303 })
304 }
305
306 /// Geometry of the live-count affordances in the painted row.
307 /// Pass the same area used for painting, intersected with the buffer.
308 #[must_use]
309 pub fn count_hitboxes(&self, area: Rect, theme: &Theme) -> Vec<(usize, Rect)> {
310 let Some(layout) = self.layout(area, theme) else {
311 return Vec::new();
312 };
313 layout.count_hitboxes(theme.ascii())
314 }
315 }
316
317 struct PostureItem {
318 text: String,
319 key: Option<String>,
320 role: Role,
321 ink: Option<TuiInk>,
322 bold: bool,
323 joined: bool,
324 count_index: Option<usize>,
325 }
326
327 impl PostureItem {
328 fn fact(fact: &PostureFact<'_>, ascii: bool) -> Self {
329 Self {
330 text: project(&fact.text, ascii),
331 key: None,
332 role: fact.role,
333 ink: fact.ink,
334 bold: false,
335 joined: false,
336 count_index: None,
337 }
338 }
339
340 fn width(&self) -> usize {
341 text::width(&self.text) + self.key.as_deref().map_or(0, text::width)
342 }
343
344 fn separator(&self) -> &'static str {
345 if self.joined { COUNT_JOIN } else { ITEM_JOIN }
346 }
347 }
348
349 fn left_width(items: &[PostureItem]) -> usize {
350 items.iter().map(PostureItem::width).sum::<usize>()
351 + items
352 .iter()
353 .skip(1)
354 .map(|item| text::width(item.separator()))
355 .sum::<usize>()
356 }
357
358 struct PostureLayout {
359 area: Rect,
360 budget: usize,
361 items: Vec<PostureItem>,
362 right: Option<(String, Role)>,
363 }
364
365 impl PostureLayout {
366 fn clip(&self, x: usize, value: &str) -> String {
367 clip_native(
368 value,
369 (usize::from(self.area.x) + self.budget).saturating_sub(x),
370 )
371 }
372
373 fn count_hitboxes(&self, ascii: bool) -> Vec<(usize, Rect)> {
374 let mut out = Vec::new();
375 let mut x = usize::from(self.area.x);
376 for (index, item) in self.items.iter().enumerate() {
377 if index > 0 {
378 x += text::width(&project(item.separator(), ascii));
379 }
380 let value = self.clip(x, &item.text);
381 let key = item
382 .key
383 .as_deref()
384 .map(|key| self.clip(x + text::width(&item.text), key))
385 .unwrap_or_default();
386 if let Some(index) = item.count_index
387 && !value.is_empty()
388 {
389 out.push((
390 index,
391 Rect::new(
392 x as u16,
393 self.area.y,
394 (text::width(&value) + text::width(&key)) as u16,
395 1,
396 ),
397 ));
398 }
399 x += item.width();
400 }
401 out
402 }
403 }
404
405 impl Paint for PostureBar<'_> {
406 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
407 let Some(layout) = self.layout(area.intersection(buf.area), theme) else {
408 return;
409 };
410 let mut x = usize::from(layout.area.x);
411 for (index, item) in layout.items.iter().enumerate() {
412 if index > 0 {
413 let separator = project(item.separator(), theme.ascii());
414 put(
415 layout.area,
416 buf,
417 x,
418 &layout.clip(x, &separator),
419 theme.fg(Role::Dim),
420 );
421 x += text::width(&separator);
422 }
423 let base = item
424 .ink
425 .map_or_else(|| theme.fg(item.role), |ink| theme.tui_ink(ink));
426 let style = if item.bold {
427 base.add_modifier(Modifier::BOLD)
428 } else {
429 base
430 };
431 put(layout.area, buf, x, &layout.clip(x, &item.text), style);
432 if let Some(key) = &item.key {
433 let key_x = x + text::width(&item.text);
434 put(
435 layout.area,
436 buf,
437 key_x,
438 &layout.clip(key_x, key),
439 theme.fg(Role::Hint),
440 );
441 }
442 x += item.width();
443 }
444 if let Some((value, role)) = &layout.right
445 && !value.is_empty()
446 {
447 let x = usize::from(layout.area.right()).saturating_sub(text::width(value));
448 put(layout.area, buf, x, value, theme.fg(*role));
449 }
450 }
451
452 fn height(&self, width: u16, _theme: &Theme) -> u16 {
453 u16::from(width > 0)
454 }
455 }
456
457 /// Identity and native shedding priority of a metrics-line segment.
458 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
459 pub enum MetricKind {
460 Model,
461 Context,
462 Cost,
463 BillingTier,
464 OutputTokens,
465 Ttft,
466 Rate,
467 Cache,
468 Balance,
469 Goal,
470 Workspace,
471 GitBranch,
472 }
473
474 impl MetricKind {
475 const SHED_BEFORE_HELP: u8 = 7;
476
477 /// Higher priorities shed first; model and context never shed.
478 #[must_use]
479 pub const fn shed_priority(self) -> u8 {
480 match self {
481 Self::Cache => 6,
482 Self::Rate | Self::Ttft | Self::Cost => 5,
483 Self::OutputTokens | Self::BillingTier => 7,
484 Self::Balance | Self::Workspace | Self::GitBranch => 4,
485 Self::Goal => 3,
486 Self::Model | Self::Context => 0,
487 }
488 }
489 }
490
491 /// A reported number or route name. Empty labels paint just the value.
492 #[derive(Clone, Debug)]
493 pub struct MetricSegment<'a> {
494 pub kind: MetricKind,
495 pub label: Cow<'a, str>,
496 pub value: Cow<'a, str>,
497 pub role: Role,
498 }
499
500 impl<'a> MetricSegment<'a> {
501 #[must_use]
502 pub fn new(
503 kind: MetricKind,
504 label: impl Into<Cow<'a, str>>,
505 value: impl Into<Cow<'a, str>>,
506 ) -> Self {
507 Self {
508 kind,
509 label: label.into(),
510 value: value.into(),
511 role: Role::Muted,
512 }
513 }
514
515 #[must_use]
516 pub fn role(mut self, role: Role) -> Self {
517 self.role = role;
518 self
519 }
520 }
521
522 /// A segment's exact visible cells, including the space between label and value.
523 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
524 pub struct MetricHitbox {
525 pub kind: MetricKind,
526 pub area: Rect,
527 }
528
529 /// Codewhale's one-row model, context and session metrics line.
530 ///
531 /// Missing measurements are omitted by leaving out their segments. The
532 /// native shed ladder preserves model and context, removes secondary counts
533 /// before help, and clips at the right edge only below the identity floor.
534 #[derive(Clone, Debug)]
535 pub struct MetricsLine<'a> {
536 pub segments: Vec<MetricSegment<'a>>,
537 pub help_hint: Cow<'a, str>,
538 pub hovered: Option<MetricKind>,
539 pub compact: bool,
540 }
541
542 impl<'a> MetricsLine<'a> {
543 #[must_use]
544 pub fn new(segments: Vec<MetricSegment<'a>>) -> Self {
545 Self {
546 segments,
547 help_hint: Cow::Borrowed(""),
548 hovered: None,
549 compact: false,
550 }
551 }
552
553 #[must_use]
554 pub fn help_hint(mut self, value: impl Into<Cow<'a, str>>) -> Self {
555 self.help_hint = value.into();
556 self
557 }
558
559 #[must_use]
560 pub fn hovered(mut self, kind: MetricKind) -> Self {
561 self.hovered = Some(kind);
562 self
563 }
564
565 #[must_use]
566 pub fn compact(mut self, compact: bool) -> Self {
567 self.compact = compact;
568 self
569 }
570
571 fn layout(&self, area: Rect, theme: &Theme) -> MetricLayout {
572 let ascii = theme.ascii();
573 let help = project(&self.help_hint, ascii);
574 let mut kept: Vec<PreparedMetric> = self
575 .segments
576 .iter()
577 .filter(|segment| {
578 !self.compact || segment.kind.shed_priority() < MetricKind::SHED_BEFORE_HELP
579 })
580 .map(|segment| PreparedMetric {
581 kind: segment.kind,
582 label: project(&segment.label, ascii),
583 value: project(&segment.value, ascii),
584 role: segment.role,
585 })
586 .collect();
587 let left_width = |segments: &[PreparedMetric]| -> usize {
588 segments.iter().map(PreparedMetric::width).sum::<usize>()
589 + text::width(ITEM_JOIN) * segments.len().saturating_sub(1)
590 };
591 let needed = |left: usize, show_help: bool| -> usize {
592 left + if show_help && !help.is_empty() {
593 HELP_GAP + text::width(&help)
594 } else {
595 0
596 }
597 };
598 let sheddable = |segments: &[PreparedMetric], min_priority: u8| -> Option<usize> {
599 segments
600 .iter()
601 .enumerate()
602 .filter(|(_, segment)| segment.kind.shed_priority() >= min_priority.max(1))
603 .max_by_key(|(_, segment)| segment.kind.shed_priority())
604 .map(|(index, _)| index)
605 };
606 let mut show_help = !help.is_empty() && !self.compact;
607 while needed(left_width(&kept), show_help) > usize::from(area.width) {
608 if let Some(index) = sheddable(&kept, MetricKind::SHED_BEFORE_HELP) {
609 kept.remove(index);
610 } else if show_help {
611 show_help = false;
612 } else if let Some(index) = sheddable(&kept, 1) {
613 kept.remove(index);
614 } else {
615 break;
616 }
617 }
618 MetricLayout {
619 kept,
620 help: show_help.then_some(help),
621 }
622 }
623
624 /// Geometry for all surviving readings. Only model and context acquire
625 /// hover styling; hosts choose which readings dispatch an action.
626 /// Use the same clipped area as painting when the buffer is smaller.
627 #[must_use]
628 pub fn hitboxes(&self, area: Rect, theme: &Theme) -> Vec<MetricHitbox> {
629 if area.is_empty() {
630 return Vec::new();
631 }
632 let layout = self.layout(area, theme);
633 let mut x = usize::from(area.x);
634 let mut out = Vec::new();
635 let right = usize::from(area.right());
636 for (index, segment) in layout.kept.iter().enumerate() {
637 if index > 0 {
638 x += text::width(ITEM_JOIN);
639 }
640 let end = (x + segment.width()).min(right);
641 if x < end {
642 out.push(MetricHitbox {
643 kind: segment.kind,
644 area: Rect::new(x as u16, area.y, (end - x) as u16, 1),
645 });
646 }
647 x += segment.width();
648 }
649 out
650 }
651
652 #[must_use]
653 pub fn context_hitbox(&self, area: Rect, theme: &Theme) -> Option<Rect> {
654 self.hitboxes(area, theme)
655 .into_iter()
656 .find(|hit| hit.kind == MetricKind::Context)
657 .map(|hit| hit.area)
658 }
659 }
660
661 struct PreparedMetric {
662 kind: MetricKind,
663 label: String,
664 value: String,
665 role: Role,
666 }
667
668 impl PreparedMetric {
669 fn width(&self) -> usize {
670 text::width(&self.value)
671 + if self.label.is_empty() {
672 0
673 } else {
674 text::width(&self.label) + 1
675 }
676 }
677 }
678
679 struct MetricLayout {
680 kept: Vec<PreparedMetric>,
681 help: Option<String>,
682 }
683
684 impl Paint for MetricsLine<'_> {
685 fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme) {
686 let area = area.intersection(buf.area);
687 if area.is_empty() {
688 return;
689 }
690 let layout = self.layout(area, theme);
691 let mut x = usize::from(area.x);
692 for (index, segment) in layout.kept.iter().enumerate() {
693 if index > 0 {
694 put(area, buf, x, ITEM_JOIN, theme.fg(Role::Dim));
695 x += text::width(ITEM_JOIN);
696 }
697 let hovered = matches!(segment.kind, MetricKind::Model | MetricKind::Context)
698 && self.hovered == Some(segment.kind);
699 let style = if hovered {
700 theme
701 .fg(segment.role)
702 .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
703 } else {
704 theme.fg(segment.role)
705 };
706 if !segment.label.is_empty() {
707 let label_role = if matches!(segment.role, Role::Danger | Role::Attention) {
708 segment.role
709 } else {
710 Role::Muted
711 };
712 put(area, buf, x, &segment.label, theme.fg(label_role));
713 x += text::width(&segment.label) + 1;
714 }
715 put(area, buf, x, &segment.value, style);
716 x += text::width(&segment.value);
717 }
718 if let Some(help) = layout.help {
719 let x = usize::from(area.right()).saturating_sub(text::width(&help));
720 put(area, buf, x, &help, theme.fg(Role::Hint));
721 }
722 }
723
724 fn height(&self, width: u16, _theme: &Theme) -> u16 {
725 u16::from(width > 0)
726 }
727 }
728
729 // Native punctuation projection, with the kit's existing text sanitation.
730 // Authored glyphs change in ASCII mode; language text stays intact.
731 fn project(value: &str, ascii: bool) -> String {
732 let safe = text::display_safe(value);
733 if !ascii {
734 return safe.into_owned();
735 }
736 if let Some(fallback) = glyphs::ascii_fallback(&safe) {
737 return fallback.to_owned();
738 }
739 safe.chars()
740 .map(|ch| {
741 let mut bytes = [0; 4];
742 glyphs::ascii_fallback(ch.encode_utf8(&mut bytes))
743 .map(str::to_owned)
744 .unwrap_or_else(|| ch.to_string())
745 })
746 .collect()
747 }
748
749 // phase_strip uses ui_text::truncate_line_to_width, whose ellipsis is three
750 // ASCII cells even on Unicode terminals and whose tiny budgets have no dot.
751 fn clip_native(value: &str, max: usize) -> String {
752 if text::width(value) <= max {
753 return value.to_owned();
754 }
755 let limit = max.saturating_sub(if max > 3 { 3 } else { 0 });
756 let mut out = String::new();
757 let mut used = 0;
758 for grapheme in value.graphemes(true) {
759 let width = text::width(grapheme);
760 if used + width > limit {
761 break;
762 }
763 out.push_str(grapheme);
764 used += width;
765 }
766 if max > 3 {
767 out.push_str("...");
768 }
769 out
770 }
771
772 fn put(area: Rect, buf: &mut Buffer, x: usize, value: &str, style: Style) {
773 let right = usize::from(area.right());
774 if x >= usize::from(area.x) && x < right && !value.is_empty() {
775 buf.set_stringn(x as u16, area.y, value, right - x, style);
776 }
777 }
778
778 lines RUST