feat(tui): replace hard‑coded colors with Theme values and propagate Theme through UI rendering
- Introduce `Theme` import and pass a cloned `theme` instance to UI helpers (e.g., `render_editable_textarea`). - Remove direct `Color` usage; UI now derives colors from `Theme` fields for placeholders, selections, ReAct components (thought, action, input, observation, final answer), status badges, operating mode badges, and model info panel. - Extend `Theme` with new color fields for agent ReAct stages, badge foreground/background, and operating mode colors. - Update rendering logic to apply these theme colors throughout the TUI (input panel, help text, status lines, model selection UI, etc.). - Adjust imports to drop unused `Color` references.
This commit is contained in:
@@ -8,6 +8,8 @@ use std::collections::HashMap;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub type ThemePalette = Theme;
|
||||||
|
|
||||||
/// A complete theme definition for OWLEN TUI
|
/// A complete theme definition for OWLEN TUI
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
@@ -128,6 +130,84 @@ pub struct Theme {
|
|||||||
#[serde(deserialize_with = "deserialize_color")]
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
#[serde(serialize_with = "serialize_color")]
|
#[serde(serialize_with = "serialize_color")]
|
||||||
pub info: Color,
|
pub info: Color,
|
||||||
|
|
||||||
|
/// Agent action coloring (ReAct THOUGHT)
|
||||||
|
#[serde(default = "Theme::default_agent_thought")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_thought: Color,
|
||||||
|
|
||||||
|
/// Agent action coloring (ReAct ACTION)
|
||||||
|
#[serde(default = "Theme::default_agent_action")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_action: Color,
|
||||||
|
|
||||||
|
/// Agent action coloring (ReAct ACTION_INPUT)
|
||||||
|
#[serde(default = "Theme::default_agent_action_input")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_action_input: Color,
|
||||||
|
|
||||||
|
/// Agent action coloring (ReAct OBSERVATION)
|
||||||
|
#[serde(default = "Theme::default_agent_observation")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_observation: Color,
|
||||||
|
|
||||||
|
/// Agent action coloring (ReAct FINAL_ANSWER)
|
||||||
|
#[serde(default = "Theme::default_agent_final_answer")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_final_answer: Color,
|
||||||
|
|
||||||
|
/// Status badge foreground when agent is running
|
||||||
|
#[serde(default = "Theme::default_agent_badge_running_fg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_badge_running_fg: Color,
|
||||||
|
|
||||||
|
/// Status badge background when agent is running
|
||||||
|
#[serde(default = "Theme::default_agent_badge_running_bg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_badge_running_bg: Color,
|
||||||
|
|
||||||
|
/// Status badge foreground when agent mode is idle
|
||||||
|
#[serde(default = "Theme::default_agent_badge_idle_fg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_badge_idle_fg: Color,
|
||||||
|
|
||||||
|
/// Status badge background when agent mode is idle
|
||||||
|
#[serde(default = "Theme::default_agent_badge_idle_bg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub agent_badge_idle_bg: Color,
|
||||||
|
|
||||||
|
/// Operating mode badge foreground (Chat)
|
||||||
|
#[serde(default = "Theme::default_operating_chat_fg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub operating_chat_fg: Color,
|
||||||
|
|
||||||
|
/// Operating mode badge background (Chat)
|
||||||
|
#[serde(default = "Theme::default_operating_chat_bg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub operating_chat_bg: Color,
|
||||||
|
|
||||||
|
/// Operating mode badge foreground (Code)
|
||||||
|
#[serde(default = "Theme::default_operating_code_fg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub operating_code_fg: Color,
|
||||||
|
|
||||||
|
/// Operating mode badge background (Code)
|
||||||
|
#[serde(default = "Theme::default_operating_code_bg")]
|
||||||
|
#[serde(deserialize_with = "deserialize_color")]
|
||||||
|
#[serde(serialize_with = "serialize_color")]
|
||||||
|
pub operating_code_bg: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Theme {
|
impl Default for Theme {
|
||||||
@@ -136,6 +216,60 @@ impl Default for Theme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
|
const fn default_agent_thought() -> Color {
|
||||||
|
Color::LightBlue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_action() -> Color {
|
||||||
|
Color::Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_action_input() -> Color {
|
||||||
|
Color::LightCyan
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_observation() -> Color {
|
||||||
|
Color::LightGreen
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_final_answer() -> Color {
|
||||||
|
Color::Magenta
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_badge_running_fg() -> Color {
|
||||||
|
Color::Black
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_badge_running_bg() -> Color {
|
||||||
|
Color::Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_badge_idle_fg() -> Color {
|
||||||
|
Color::Black
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_agent_badge_idle_bg() -> Color {
|
||||||
|
Color::Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_operating_chat_fg() -> Color {
|
||||||
|
Color::Black
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_operating_chat_bg() -> Color {
|
||||||
|
Color::Blue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_operating_code_fg() -> Color {
|
||||||
|
Color::Black
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_operating_code_bg() -> Color {
|
||||||
|
Color::Magenta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the default themes directory path
|
/// Get the default themes directory path
|
||||||
pub fn default_themes_dir() -> PathBuf {
|
pub fn default_themes_dir() -> PathBuf {
|
||||||
let config_dir = PathBuf::from(shellexpand::tilde(crate::config::DEFAULT_CONFIG_PATH).as_ref())
|
let config_dir = PathBuf::from(shellexpand::tilde(crate::config::DEFAULT_CONFIG_PATH).as_ref())
|
||||||
@@ -294,6 +428,19 @@ fn default_dark() -> Theme {
|
|||||||
placeholder: Color::DarkGray,
|
placeholder: Color::DarkGray,
|
||||||
error: Color::Red,
|
error: Color::Red,
|
||||||
info: Color::LightGreen,
|
info: Color::LightGreen,
|
||||||
|
agent_thought: Color::LightBlue,
|
||||||
|
agent_action: Color::Yellow,
|
||||||
|
agent_action_input: Color::LightCyan,
|
||||||
|
agent_observation: Color::LightGreen,
|
||||||
|
agent_final_answer: Color::Magenta,
|
||||||
|
agent_badge_running_fg: Color::Black,
|
||||||
|
agent_badge_running_bg: Color::Yellow,
|
||||||
|
agent_badge_idle_fg: Color::Black,
|
||||||
|
agent_badge_idle_bg: Color::Cyan,
|
||||||
|
operating_chat_fg: Color::Black,
|
||||||
|
operating_chat_bg: Color::Blue,
|
||||||
|
operating_code_fg: Color::Black,
|
||||||
|
operating_code_bg: Color::Magenta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +471,19 @@ fn default_light() -> Theme {
|
|||||||
placeholder: Color::Gray,
|
placeholder: Color::Gray,
|
||||||
error: Color::Rgb(192, 57, 43),
|
error: Color::Rgb(192, 57, 43),
|
||||||
info: Color::Green,
|
info: Color::Green,
|
||||||
|
agent_thought: Color::Rgb(0, 85, 164),
|
||||||
|
agent_action: Color::Rgb(181, 137, 0),
|
||||||
|
agent_action_input: Color::Rgb(0, 139, 139),
|
||||||
|
agent_observation: Color::Rgb(46, 139, 87),
|
||||||
|
agent_final_answer: Color::Rgb(142, 68, 173),
|
||||||
|
agent_badge_running_fg: Color::White,
|
||||||
|
agent_badge_running_bg: Color::Rgb(241, 196, 15),
|
||||||
|
agent_badge_idle_fg: Color::White,
|
||||||
|
agent_badge_idle_bg: Color::Rgb(0, 150, 136),
|
||||||
|
operating_chat_fg: Color::White,
|
||||||
|
operating_chat_bg: Color::Rgb(0, 85, 164),
|
||||||
|
operating_code_fg: Color::White,
|
||||||
|
operating_code_bg: Color::Rgb(142, 68, 173),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +514,19 @@ fn gruvbox() -> Theme {
|
|||||||
placeholder: Color::Rgb(102, 92, 84),
|
placeholder: Color::Rgb(102, 92, 84),
|
||||||
error: Color::Rgb(251, 73, 52), // #fb4934
|
error: Color::Rgb(251, 73, 52), // #fb4934
|
||||||
info: Color::Rgb(184, 187, 38),
|
info: Color::Rgb(184, 187, 38),
|
||||||
|
agent_thought: Color::Rgb(131, 165, 152),
|
||||||
|
agent_action: Color::Rgb(250, 189, 47),
|
||||||
|
agent_action_input: Color::Rgb(142, 192, 124),
|
||||||
|
agent_observation: Color::Rgb(184, 187, 38),
|
||||||
|
agent_final_answer: Color::Rgb(211, 134, 155),
|
||||||
|
agent_badge_running_fg: Color::Rgb(40, 40, 40),
|
||||||
|
agent_badge_running_bg: Color::Rgb(250, 189, 47),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(40, 40, 40),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(131, 165, 152),
|
||||||
|
operating_chat_fg: Color::Rgb(40, 40, 40),
|
||||||
|
operating_chat_bg: Color::Rgb(131, 165, 152),
|
||||||
|
operating_code_fg: Color::Rgb(40, 40, 40),
|
||||||
|
operating_code_bg: Color::Rgb(211, 134, 155),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +557,19 @@ fn dracula() -> Theme {
|
|||||||
placeholder: Color::Rgb(98, 114, 164),
|
placeholder: Color::Rgb(98, 114, 164),
|
||||||
error: Color::Rgb(255, 85, 85), // #ff5555
|
error: Color::Rgb(255, 85, 85), // #ff5555
|
||||||
info: Color::Rgb(80, 250, 123),
|
info: Color::Rgb(80, 250, 123),
|
||||||
|
agent_thought: Color::Rgb(139, 233, 253),
|
||||||
|
agent_action: Color::Rgb(241, 250, 140),
|
||||||
|
agent_action_input: Color::Rgb(189, 147, 249),
|
||||||
|
agent_observation: Color::Rgb(80, 250, 123),
|
||||||
|
agent_final_answer: Color::Rgb(255, 121, 198),
|
||||||
|
agent_badge_running_fg: Color::Rgb(40, 42, 54),
|
||||||
|
agent_badge_running_bg: Color::Rgb(241, 250, 140),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(40, 42, 54),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(139, 233, 253),
|
||||||
|
operating_chat_fg: Color::Rgb(40, 42, 54),
|
||||||
|
operating_chat_bg: Color::Rgb(139, 233, 253),
|
||||||
|
operating_code_fg: Color::Rgb(40, 42, 54),
|
||||||
|
operating_code_bg: Color::Rgb(189, 147, 249),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,6 +600,19 @@ fn solarized() -> Theme {
|
|||||||
placeholder: Color::Rgb(88, 110, 117),
|
placeholder: Color::Rgb(88, 110, 117),
|
||||||
error: Color::Rgb(220, 50, 47), // #dc322f (red)
|
error: Color::Rgb(220, 50, 47), // #dc322f (red)
|
||||||
info: Color::Rgb(133, 153, 0),
|
info: Color::Rgb(133, 153, 0),
|
||||||
|
agent_thought: Color::Rgb(42, 161, 152),
|
||||||
|
agent_action: Color::Rgb(181, 137, 0),
|
||||||
|
agent_action_input: Color::Rgb(38, 139, 210),
|
||||||
|
agent_observation: Color::Rgb(133, 153, 0),
|
||||||
|
agent_final_answer: Color::Rgb(108, 113, 196),
|
||||||
|
agent_badge_running_fg: Color::Rgb(0, 43, 54),
|
||||||
|
agent_badge_running_bg: Color::Rgb(181, 137, 0),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(0, 43, 54),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(42, 161, 152),
|
||||||
|
operating_chat_fg: Color::Rgb(0, 43, 54),
|
||||||
|
operating_chat_bg: Color::Rgb(42, 161, 152),
|
||||||
|
operating_code_fg: Color::Rgb(0, 43, 54),
|
||||||
|
operating_code_bg: Color::Rgb(108, 113, 196),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +643,19 @@ fn midnight_ocean() -> Theme {
|
|||||||
placeholder: Color::Rgb(110, 118, 129),
|
placeholder: Color::Rgb(110, 118, 129),
|
||||||
error: Color::Rgb(248, 81, 73),
|
error: Color::Rgb(248, 81, 73),
|
||||||
info: Color::Rgb(158, 206, 106),
|
info: Color::Rgb(158, 206, 106),
|
||||||
|
agent_thought: Color::Rgb(121, 192, 255),
|
||||||
|
agent_action: Color::Rgb(255, 212, 59),
|
||||||
|
agent_action_input: Color::Rgb(137, 221, 255),
|
||||||
|
agent_observation: Color::Rgb(158, 206, 106),
|
||||||
|
agent_final_answer: Color::Rgb(246, 140, 245),
|
||||||
|
agent_badge_running_fg: Color::Rgb(13, 17, 23),
|
||||||
|
agent_badge_running_bg: Color::Rgb(255, 212, 59),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(13, 17, 23),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(137, 221, 255),
|
||||||
|
operating_chat_fg: Color::Rgb(13, 17, 23),
|
||||||
|
operating_chat_bg: Color::Rgb(121, 192, 255),
|
||||||
|
operating_code_fg: Color::Rgb(13, 17, 23),
|
||||||
|
operating_code_bg: Color::Rgb(246, 140, 245),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,6 +686,19 @@ fn rose_pine() -> Theme {
|
|||||||
placeholder: Color::Rgb(110, 106, 134),
|
placeholder: Color::Rgb(110, 106, 134),
|
||||||
error: Color::Rgb(235, 111, 146),
|
error: Color::Rgb(235, 111, 146),
|
||||||
info: Color::Rgb(156, 207, 216),
|
info: Color::Rgb(156, 207, 216),
|
||||||
|
agent_thought: Color::Rgb(156, 207, 216),
|
||||||
|
agent_action: Color::Rgb(246, 193, 119),
|
||||||
|
agent_action_input: Color::Rgb(196, 167, 231),
|
||||||
|
agent_observation: Color::Rgb(235, 188, 186),
|
||||||
|
agent_final_answer: Color::Rgb(235, 111, 146),
|
||||||
|
agent_badge_running_fg: Color::Rgb(25, 23, 36),
|
||||||
|
agent_badge_running_bg: Color::Rgb(246, 193, 119),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(25, 23, 36),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(156, 207, 216),
|
||||||
|
operating_chat_fg: Color::Rgb(25, 23, 36),
|
||||||
|
operating_chat_bg: Color::Rgb(156, 207, 216),
|
||||||
|
operating_code_fg: Color::Rgb(25, 23, 36),
|
||||||
|
operating_code_bg: Color::Rgb(196, 167, 231),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +729,19 @@ fn monokai() -> Theme {
|
|||||||
placeholder: Color::Rgb(117, 113, 94),
|
placeholder: Color::Rgb(117, 113, 94),
|
||||||
error: Color::Rgb(249, 38, 114),
|
error: Color::Rgb(249, 38, 114),
|
||||||
info: Color::Rgb(166, 226, 46),
|
info: Color::Rgb(166, 226, 46),
|
||||||
|
agent_thought: Color::Rgb(102, 217, 239),
|
||||||
|
agent_action: Color::Rgb(230, 219, 116),
|
||||||
|
agent_action_input: Color::Rgb(174, 129, 255),
|
||||||
|
agent_observation: Color::Rgb(166, 226, 46),
|
||||||
|
agent_final_answer: Color::Rgb(249, 38, 114),
|
||||||
|
agent_badge_running_fg: Color::Rgb(39, 40, 34),
|
||||||
|
agent_badge_running_bg: Color::Rgb(230, 219, 116),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(39, 40, 34),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(102, 217, 239),
|
||||||
|
operating_chat_fg: Color::Rgb(39, 40, 34),
|
||||||
|
operating_chat_bg: Color::Rgb(102, 217, 239),
|
||||||
|
operating_code_fg: Color::Rgb(39, 40, 34),
|
||||||
|
operating_code_bg: Color::Rgb(174, 129, 255),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,6 +772,19 @@ fn material_dark() -> Theme {
|
|||||||
placeholder: Color::Rgb(84, 110, 122),
|
placeholder: Color::Rgb(84, 110, 122),
|
||||||
error: Color::Rgb(240, 113, 120),
|
error: Color::Rgb(240, 113, 120),
|
||||||
info: Color::Rgb(195, 232, 141),
|
info: Color::Rgb(195, 232, 141),
|
||||||
|
agent_thought: Color::Rgb(128, 203, 196),
|
||||||
|
agent_action: Color::Rgb(255, 203, 107),
|
||||||
|
agent_action_input: Color::Rgb(199, 146, 234),
|
||||||
|
agent_observation: Color::Rgb(195, 232, 141),
|
||||||
|
agent_final_answer: Color::Rgb(240, 113, 120),
|
||||||
|
agent_badge_running_fg: Color::Rgb(38, 50, 56),
|
||||||
|
agent_badge_running_bg: Color::Rgb(255, 203, 107),
|
||||||
|
agent_badge_idle_fg: Color::Rgb(38, 50, 56),
|
||||||
|
agent_badge_idle_bg: Color::Rgb(128, 203, 196),
|
||||||
|
operating_chat_fg: Color::Rgb(38, 50, 56),
|
||||||
|
operating_chat_bg: Color::Rgb(130, 170, 255),
|
||||||
|
operating_code_fg: Color::Rgb(38, 50, 56),
|
||||||
|
operating_code_bg: Color::Rgb(199, 146, 234),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,6 +815,19 @@ fn material_light() -> Theme {
|
|||||||
placeholder: Color::Rgb(144, 164, 174),
|
placeholder: Color::Rgb(144, 164, 174),
|
||||||
error: Color::Rgb(211, 47, 47),
|
error: Color::Rgb(211, 47, 47),
|
||||||
info: Color::Rgb(56, 142, 60),
|
info: Color::Rgb(56, 142, 60),
|
||||||
|
agent_thought: Color::Rgb(68, 138, 255),
|
||||||
|
agent_action: Color::Rgb(245, 124, 0),
|
||||||
|
agent_action_input: Color::Rgb(124, 77, 255),
|
||||||
|
agent_observation: Color::Rgb(56, 142, 60),
|
||||||
|
agent_final_answer: Color::Rgb(211, 47, 47),
|
||||||
|
agent_badge_running_fg: Color::White,
|
||||||
|
agent_badge_running_bg: Color::Rgb(245, 124, 0),
|
||||||
|
agent_badge_idle_fg: Color::White,
|
||||||
|
agent_badge_idle_bg: Color::Rgb(0, 150, 136),
|
||||||
|
operating_chat_fg: Color::White,
|
||||||
|
operating_chat_bg: Color::Rgb(68, 138, 255),
|
||||||
|
operating_code_fg: Color::White,
|
||||||
|
operating_code_bg: Color::Rgb(124, 77, 255),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use owlen_core::{
|
|||||||
types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role},
|
types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role},
|
||||||
ui::{AppState, AutoScroll, FocusedPanel, InputMode},
|
ui::{AppState, AutoScroll, FocusedPanel, InputMode},
|
||||||
};
|
};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use textwrap::wrap;
|
use textwrap::wrap;
|
||||||
use tokio::{sync::mpsc, task::JoinHandle};
|
use tokio::{sync::mpsc, task::JoinHandle};
|
||||||
@@ -1372,9 +1372,10 @@ impl ChatApp {
|
|||||||
// Sync buffer to textarea before entering visual mode
|
// Sync buffer to textarea before entering visual mode
|
||||||
self.sync_buffer_to_textarea();
|
self.sync_buffer_to_textarea();
|
||||||
// Set a visible selection style
|
// Set a visible selection style
|
||||||
self.textarea.set_selection_style(
|
let selection_style = Style::default()
|
||||||
Style::default().bg(Color::LightBlue).fg(Color::Black),
|
.bg(self.theme.selection_bg)
|
||||||
);
|
.fg(self.theme.selection_fg);
|
||||||
|
self.textarea.set_selection_style(selection_style);
|
||||||
// Start visual selection at current cursor position
|
// Start visual selection at current cursor position
|
||||||
self.textarea.start_selection();
|
self.textarea.start_selection();
|
||||||
self.visual_start = Some(self.textarea.cursor());
|
self.visual_start = Some(self.textarea.cursor());
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use owlen_core::theme::Theme;
|
|||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier, Style},
|
style::{Modifier, Style},
|
||||||
widgets::{Block, Borders, Paragraph, Wrap},
|
widgets::{Block, Borders, Paragraph, Wrap},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ impl ModelInfoPanel {
|
|||||||
.block(block)
|
.block(block)
|
||||||
.style(
|
.style(
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::DarkGray)
|
.fg(theme.placeholder)
|
||||||
.add_modifier(Modifier::ITALIC),
|
.add_modifier(Modifier::ITALIC),
|
||||||
)
|
)
|
||||||
.wrap(Wrap { trim: true });
|
.wrap(Wrap { trim: true });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap};
|
||||||
use serde_json;
|
use serde_json;
|
||||||
@@ -11,6 +11,7 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
|
|
||||||
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
|
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
|
||||||
use owlen_core::model::DetailedModelInfo;
|
use owlen_core::model::DetailedModelInfo;
|
||||||
|
use owlen_core::theme::Theme;
|
||||||
use owlen_core::types::{ModelInfo, Role};
|
use owlen_core::types::{ModelInfo, Role};
|
||||||
use owlen_core::ui::{FocusedPanel, InputMode};
|
use owlen_core::ui::{FocusedPanel, InputMode};
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ fn render_editable_textarea(
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
textarea: &mut TextArea<'static>,
|
textarea: &mut TextArea<'static>,
|
||||||
mut wrap_lines: bool,
|
mut wrap_lines: bool,
|
||||||
|
theme: &Theme,
|
||||||
) {
|
) {
|
||||||
let block = textarea.block().cloned();
|
let block = textarea.block().cloned();
|
||||||
let inner = block.as_ref().map(|b| b.inner(area)).unwrap_or(area);
|
let inner = block.as_ref().map(|b| b.inner(area)).unwrap_or(area);
|
||||||
@@ -183,7 +185,7 @@ fn render_editable_textarea(
|
|||||||
|
|
||||||
if is_empty {
|
if is_empty {
|
||||||
if !placeholder_text.is_empty() {
|
if !placeholder_text.is_empty() {
|
||||||
let style = placeholder_style.unwrap_or_else(|| Style::default().fg(Color::DarkGray));
|
let style = placeholder_style.unwrap_or_else(|| Style::default().fg(theme.placeholder));
|
||||||
render_lines.push(Line::from(vec![Span::styled(placeholder_text, style)]));
|
render_lines.push(Line::from(vec![Span::styled(placeholder_text, style)]));
|
||||||
} else {
|
} else {
|
||||||
render_lines.push(Line::default());
|
render_lines.push(Line::default());
|
||||||
@@ -954,7 +956,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
|
|
||||||
// Detect ReAct components and apply color coding
|
// Detect ReAct components and apply color coding
|
||||||
if line_trimmed.starts_with("THOUGHT:") {
|
if line_trimmed.starts_with("THOUGHT:") {
|
||||||
// Blue for THOUGHT
|
let thought_color = theme.agent_thought;
|
||||||
let thought_content = line_trimmed.strip_prefix("THOUGHT:").unwrap_or("").trim();
|
let thought_content = line_trimmed.strip_prefix("THOUGHT:").unwrap_or("").trim();
|
||||||
let wrapped = wrap(thought_content, content_width as usize);
|
let wrapped = wrap(thought_content, content_width as usize);
|
||||||
|
|
||||||
@@ -964,10 +966,10 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"THOUGHT: ",
|
"THOUGHT: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Blue)
|
.fg(thought_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(first.to_string(), Style::default().fg(Color::Blue)),
|
Span::styled(first.to_string(), Style::default().fg(thought_color)),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,28 +977,28 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
for chunk in wrapped.iter().skip(1) {
|
for chunk in wrapped.iter().skip(1) {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!(" {}", chunk),
|
format!(" {}", chunk),
|
||||||
Style::default().fg(Color::Blue),
|
Style::default().fg(thought_color),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else if line_trimmed.starts_with("ACTION:") {
|
} else if line_trimmed.starts_with("ACTION:") {
|
||||||
// Yellow for ACTION
|
let action_color = theme.agent_action;
|
||||||
let action_content = line_trimmed.strip_prefix("ACTION:").unwrap_or("").trim();
|
let action_content = line_trimmed.strip_prefix("ACTION:").unwrap_or("").trim();
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"ACTION: ",
|
"ACTION: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Yellow)
|
.fg(action_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
action_content,
|
action_content,
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Yellow)
|
.fg(action_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
} else if line_trimmed.starts_with("ACTION_INPUT:") {
|
} else if line_trimmed.starts_with("ACTION_INPUT:") {
|
||||||
// Cyan for ACTION_INPUT
|
let input_color = theme.agent_action_input;
|
||||||
let input_content = line_trimmed
|
let input_content = line_trimmed
|
||||||
.strip_prefix("ACTION_INPUT:")
|
.strip_prefix("ACTION_INPUT:")
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
@@ -1008,21 +1010,21 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"ACTION_INPUT: ",
|
"ACTION_INPUT: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Cyan)
|
.fg(input_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(first.to_string(), Style::default().fg(Color::Cyan)),
|
Span::styled(first.to_string(), Style::default().fg(input_color)),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
for chunk in wrapped.iter().skip(1) {
|
for chunk in wrapped.iter().skip(1) {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!(" {}", chunk),
|
format!(" {}", chunk),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(input_color),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else if line_trimmed.starts_with("OBSERVATION:") {
|
} else if line_trimmed.starts_with("OBSERVATION:") {
|
||||||
// Green for OBSERVATION
|
let observation_color = theme.agent_observation;
|
||||||
let obs_content = line_trimmed
|
let obs_content = line_trimmed
|
||||||
.strip_prefix("OBSERVATION:")
|
.strip_prefix("OBSERVATION:")
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
@@ -1034,21 +1036,21 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"OBSERVATION: ",
|
"OBSERVATION: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Green)
|
.fg(observation_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(first.to_string(), Style::default().fg(Color::Green)),
|
Span::styled(first.to_string(), Style::default().fg(observation_color)),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
for chunk in wrapped.iter().skip(1) {
|
for chunk in wrapped.iter().skip(1) {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!(" {}", chunk),
|
format!(" {}", chunk),
|
||||||
Style::default().fg(Color::Green),
|
Style::default().fg(observation_color),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else if line_trimmed.starts_with("FINAL_ANSWER:") {
|
} else if line_trimmed.starts_with("FINAL_ANSWER:") {
|
||||||
// Magenta for FINAL_ANSWER
|
let answer_color = theme.agent_final_answer;
|
||||||
let answer_content = line_trimmed
|
let answer_content = line_trimmed
|
||||||
.strip_prefix("FINAL_ANSWER:")
|
.strip_prefix("FINAL_ANSWER:")
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
@@ -1060,13 +1062,13 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"FINAL_ANSWER: ",
|
"FINAL_ANSWER: ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(answer_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
first.to_string(),
|
first.to_string(),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(answer_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
@@ -1075,7 +1077,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
for chunk in wrapped.iter().skip(1) {
|
for chunk in wrapped.iter().skip(1) {
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
format!(" {}", chunk),
|
format!(" {}", chunk),
|
||||||
Style::default().fg(Color::Magenta),
|
Style::default().fg(answer_color),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
} else if !line_trimmed.is_empty() {
|
} else if !line_trimmed.is_empty() {
|
||||||
@@ -1123,7 +1125,7 @@ fn render_agent_actions(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
||||||
let theme = app.theme();
|
let theme = app.theme().clone();
|
||||||
let title = match app.mode() {
|
let title = match app.mode() {
|
||||||
InputMode::Editing => " Input (Enter=send · Ctrl+J=newline · Esc=exit input mode) ",
|
InputMode::Editing => " Input (Enter=send · Ctrl+J=newline · Esc=exit input mode) ",
|
||||||
InputMode::Visual => " Visual Mode (y=yank · d=cut · Esc=cancel) ",
|
InputMode::Visual => " Visual Mode (y=yank · d=cut · Esc=cancel) ",
|
||||||
@@ -1154,13 +1156,13 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
|
|||||||
let textarea = app.textarea_mut();
|
let textarea = app.textarea_mut();
|
||||||
textarea.set_block(input_block.clone());
|
textarea.set_block(input_block.clone());
|
||||||
textarea.set_hard_tab_indent(false);
|
textarea.set_hard_tab_indent(false);
|
||||||
render_editable_textarea(frame, area, textarea, true);
|
render_editable_textarea(frame, area, textarea, true, &theme);
|
||||||
} else if matches!(app.mode(), InputMode::Visual) {
|
} else if matches!(app.mode(), InputMode::Visual) {
|
||||||
// In visual mode, render textarea in read-only mode with selection
|
// In visual mode, render textarea in read-only mode with selection
|
||||||
let textarea = app.textarea_mut();
|
let textarea = app.textarea_mut();
|
||||||
textarea.set_block(input_block.clone());
|
textarea.set_block(input_block.clone());
|
||||||
textarea.set_hard_tab_indent(false);
|
textarea.set_hard_tab_indent(false);
|
||||||
render_editable_textarea(frame, area, textarea, true);
|
render_editable_textarea(frame, area, textarea, true, &theme);
|
||||||
} else if matches!(app.mode(), InputMode::Command) {
|
} else if matches!(app.mode(), InputMode::Command) {
|
||||||
// In command mode, show the command buffer with : prefix
|
// In command mode, show the command buffer with : prefix
|
||||||
let command_text = format!(":{}", app.command_buffer());
|
let command_text = format!(":{}", app.command_buffer());
|
||||||
@@ -1291,31 +1293,35 @@ fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
" 🤖 AGENT RUNNING ",
|
" 🤖 AGENT RUNNING ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(theme.agent_badge_running_fg)
|
||||||
.bg(Color::Yellow)
|
.bg(theme.agent_badge_running_bg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
} else if app.is_agent_mode() {
|
} else if app.is_agent_mode() {
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
" 🤖 AGENT MODE ",
|
" 🤖 AGENT MODE ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(theme.agent_badge_idle_fg)
|
||||||
.bg(Color::Cyan)
|
.bg(theme.agent_badge_idle_bg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add operating mode indicator
|
// Add operating mode indicator
|
||||||
let operating_mode = app.get_mode();
|
let operating_mode = app.get_mode();
|
||||||
let (op_mode_text, op_mode_color) = match operating_mode {
|
let (op_mode_text, op_mode_fg, op_mode_bg) = match operating_mode {
|
||||||
owlen_core::mode::Mode::Chat => (" 💬 CHAT", Color::Blue),
|
owlen_core::mode::Mode::Chat => {
|
||||||
owlen_core::mode::Mode::Code => (" 💻 CODE", Color::Magenta),
|
(" 💬 CHAT", theme.operating_chat_fg, theme.operating_chat_bg)
|
||||||
|
}
|
||||||
|
owlen_core::mode::Mode::Code => {
|
||||||
|
(" 💻 CODE", theme.operating_code_fg, theme.operating_code_bg)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
op_mode_text,
|
op_mode_text,
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Black)
|
.fg(op_mode_fg)
|
||||||
.bg(op_mode_color)
|
.bg(op_mode_bg)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -1707,7 +1713,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"[1] ",
|
"[1] ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Cyan)
|
.fg(theme.mode_provider_selection)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw("Allow once "),
|
Span::raw("Allow once "),
|
||||||
@@ -1720,7 +1726,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"[2] ",
|
"[2] ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Green)
|
.fg(theme.mode_editing)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw("Allow session "),
|
Span::raw("Allow session "),
|
||||||
@@ -1733,7 +1739,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"[3] ",
|
"[3] ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Yellow)
|
.fg(theme.mode_model_selection)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw("Allow always "),
|
Span::raw("Allow always "),
|
||||||
@@ -1745,7 +1751,9 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
"[4] ",
|
"[4] ",
|
||||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
Style::default()
|
||||||
|
.fg(theme.error)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw("Deny "),
|
Span::raw("Deny "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
@@ -1758,7 +1766,7 @@ fn render_consent_dialog(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
"[Esc] ",
|
"[Esc] ",
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::DarkGray)
|
.fg(theme.placeholder)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw("Cancel"),
|
Span::raw("Cancel"),
|
||||||
@@ -1950,7 +1958,7 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let help_text = match tab_index {
|
let mut help_text = match tab_index {
|
||||||
0 => vec![
|
0 => vec![
|
||||||
// Navigation
|
// Navigation
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
@@ -2219,6 +2227,25 @@ fn render_help(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
help_text.insert(
|
||||||
|
0,
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(
|
||||||
|
"Current Theme: ",
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.placeholder)
|
||||||
|
.add_modifier(Modifier::ITALIC),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
theme.name.clone(),
|
||||||
|
Style::default()
|
||||||
|
.fg(theme.mode_model_selection)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
help_text.insert(1, Line::from(""));
|
||||||
|
|
||||||
// Create layout for tabs and content
|
// Create layout for tabs and content
|
||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
|
|||||||
Reference in New Issue
Block a user