mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
Layout manager (#4601)
* ad plugin override_layout command * watch available layouts and report to plugin thread * Event::AvailableLayoutInfo * save layout command * refactor save_layout * fix api and permissions * delete_layout API command * edit layout in place API * edit layout response through api * propagate layout parsing errors * generate random name plugin api * dump stringified layout API * synchronicity dump_session_layout and include LayoutMetadata in sync response * parse_layout plugin api * fix: default to default layout dir in plugin api * rename_layout plugin command API * initial implementation of multi-tab override * apply to multiple tabs * allow applying to only the active tab * add tab/pane names to layout parsing metadata * indicate is_builtin_plugin in PaneMetadata * get_layout_dir plugin API command * consolidate (and fix) is_builtin_plugin logic * fix bad ret * allow dumping the layout for just a specific tab in the session * get focused pane and tab sync api * color last substring variants * add plugin * update assets * fix tests * add e2e tests for new plugin apis * update ui with new shortcut * rustfmt * docs(changelog): add PR
This commit is contained in:
+374
-6
@@ -10,7 +10,12 @@ use zellij_utils::input::actions::Action;
|
||||
pub use zellij_utils::plugin_api;
|
||||
use zellij_utils::plugin_api::event::ProtobufPaneScrollbackResponse;
|
||||
use zellij_utils::plugin_api::plugin_command::{
|
||||
CreateTokenResponse, ListTokensResponse, ProtobufGetPanePidResponse, ProtobufPluginCommand,
|
||||
dump_layout_response, dump_session_layout_response, get_focused_pane_info_response,
|
||||
parse_layout_response, CreateTokenResponse, ListTokensResponse, ProtobufDeleteLayoutResponse,
|
||||
ProtobufDumpLayoutResponse, ProtobufDumpSessionLayoutResponse, ProtobufEditLayoutResponse,
|
||||
ProtobufGenerateRandomNameResponse, ProtobufGetFocusedPaneInfoResponse,
|
||||
ProtobufGetLayoutDirResponse, ProtobufGetPanePidResponse, ProtobufParseLayoutResponse,
|
||||
ProtobufPluginCommand, ProtobufRenameLayoutResponse, ProtobufSaveLayoutResponse,
|
||||
RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse,
|
||||
};
|
||||
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
|
||||
@@ -89,6 +94,98 @@ pub fn get_zellij_version() -> String {
|
||||
protobuf_zellij_version.version
|
||||
}
|
||||
|
||||
/// Generates a random human-readable name using Zellij's curated word lists.
|
||||
/// Returns a name in the format AdjectiveNoun (e.g., "BraveRustacean", "ZippyWeasel").
|
||||
///
|
||||
/// This uses the same word lists as session name generation, providing
|
||||
/// approximately 4,096 unique combinations.
|
||||
pub fn generate_random_name() -> String {
|
||||
let plugin_command = PluginCommand::GenerateRandomName;
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
let response =
|
||||
ProtobufGenerateRandomNameResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
response.name
|
||||
}
|
||||
|
||||
/// Dumps a layout by name and returns its KDL content as a String
|
||||
///
|
||||
/// Supports both built-in layouts (eg. "default", "compact", "welcome")
|
||||
/// and custom layouts from the plugin's layout directory.
|
||||
pub fn dump_layout(layout_name: &str) -> Result<String, String> {
|
||||
// Create the plugin command with the layout name
|
||||
let plugin_command = PluginCommand::DumpLayout(layout_name.to_string());
|
||||
|
||||
// Convert to protobuf and encode
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
|
||||
// Call the host function (blocks until response)
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
// Read and decode the response
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
|
||||
let protobuf_response = ProtobufDumpLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Extract result from the oneof field
|
||||
match protobuf_response.result {
|
||||
Some(dump_layout_response::Result::LayoutContent(content)) => Ok(content),
|
||||
Some(dump_layout_response::Result::Error(error)) => Err(error),
|
||||
None => Err("Server returned empty response".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the layout directory.
|
||||
///
|
||||
/// This is the directory where Zellij looks for layout files. It can be:
|
||||
/// - The directory specified via CLI `--layout-dir` flag
|
||||
/// - The directory specified in the config file
|
||||
/// - The directory specified via ZELLIJ_LAYOUT_DIR env var
|
||||
/// - The default: `~/.config/zellij/layouts`
|
||||
///
|
||||
/// # Returns
|
||||
/// A String containing the absolute path to the layout directory.
|
||||
/// Returns an empty string if the layout directory cannot be determined (rare edge case).
|
||||
pub fn get_layout_dir() -> String {
|
||||
let plugin_command = PluginCommand::GetLayoutDir;
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
let response =
|
||||
ProtobufGetLayoutDirResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
response.layout_dir
|
||||
}
|
||||
|
||||
/// Returns the focused pane ID and tab index for the client associated with this plugin.
|
||||
pub fn get_focused_pane_info() -> Result<(usize, PaneId), String> {
|
||||
let plugin_command = PluginCommand::GetFocusedPaneInfo;
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let protobuf_response =
|
||||
ProtobufGetFocusedPaneInfoResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
|
||||
match protobuf_response.result {
|
||||
Some(get_focused_pane_info_response::Result::FocusedPaneInfo(info)) => {
|
||||
let tab_index = info.focused_tab_index as usize;
|
||||
match info.focused_pane_id {
|
||||
Some(pb_pane_id) => match pb_pane_id.try_into() {
|
||||
Ok(pane_id) => Ok((tab_index, pane_id)),
|
||||
Err(_) => Err("Invalid pane_id in response".to_string()),
|
||||
},
|
||||
None => Err("Missing pane_id in response".to_string()),
|
||||
}
|
||||
},
|
||||
Some(get_focused_pane_info_response::Result::Error(err)) => Err(err),
|
||||
None => Err("Empty response from host".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Host Functions
|
||||
|
||||
/// Open a file in the user's default `$EDITOR` in a new pane
|
||||
@@ -439,8 +536,8 @@ pub fn new_tabs_with_layout(layout: &str) {
|
||||
}
|
||||
|
||||
/// Provide a LayoutInfo to be applied to the current session in a new tab. If the layout has multiple tabs, they will all be opened.
|
||||
pub fn new_tabs_with_layout_info(layout_info: LayoutInfo) {
|
||||
let plugin_command = PluginCommand::NewTabsWithLayoutInfo(layout_info);
|
||||
pub fn new_tabs_with_layout_info<L: AsRef<LayoutInfo>>(layout_info: L) {
|
||||
let plugin_command = PluginCommand::NewTabsWithLayoutInfo(layout_info.as_ref().clone());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
@@ -1009,12 +1106,70 @@ pub fn watch_filesystem() {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Get the serialized session layout in KDL format as a CustomMessage Event
|
||||
pub fn dump_session_layout() {
|
||||
let plugin_command = PluginCommand::DumpSessionLayout;
|
||||
/// Get the serialized session layout in KDL format synchronously
|
||||
/// note: this removes the requesting plugin from the dumped layout
|
||||
pub fn dump_session_layout() -> Result<(String, Option<LayoutMetadata>), String> {
|
||||
dump_session_layout_impl(None)
|
||||
}
|
||||
|
||||
/// Get the serialized layout for a specific tab in KDL format synchronously
|
||||
/// note: this removes the requesting plugin from the dumped layout
|
||||
pub fn dump_session_layout_for_tab(
|
||||
tab_index: usize,
|
||||
) -> Result<(String, Option<LayoutMetadata>), String> {
|
||||
dump_session_layout_impl(Some(tab_index))
|
||||
}
|
||||
|
||||
fn dump_session_layout_impl(
|
||||
tab_index: Option<usize>,
|
||||
) -> Result<(String, Option<LayoutMetadata>), String> {
|
||||
let plugin_command = PluginCommand::DumpSessionLayout { tab_index };
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
let protobuf_response = ProtobufDumpSessionLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Extract metadata if present
|
||||
let metadata = protobuf_response
|
||||
.metadata
|
||||
.and_then(|pb_metadata| pb_metadata.try_into().ok());
|
||||
|
||||
match protobuf_response.result {
|
||||
Some(dump_session_layout_response::Result::LayoutContent(content)) => {
|
||||
Ok((content, metadata))
|
||||
},
|
||||
Some(dump_session_layout_response::Result::Error(error)) => Err(error),
|
||||
None => Err("Server returned empty response".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a KDL layout string and returns LayoutMetadata
|
||||
pub fn parse_layout(layout_string: &str) -> Result<LayoutMetadata, LayoutParsingError> {
|
||||
let plugin_command = PluginCommand::ParseLayout(layout_string.to_string());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
let response_bytes = bytes_from_stdin().map_err(|_| LayoutParsingError::SyntaxError)?;
|
||||
|
||||
let protobuf_response = ProtobufParseLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|_| LayoutParsingError::SyntaxError)?;
|
||||
|
||||
match protobuf_response.result {
|
||||
Some(parse_layout_response::Result::Metadata(metadata)) => metadata
|
||||
.try_into()
|
||||
.map_err(|_| LayoutParsingError::SyntaxError),
|
||||
Some(parse_layout_response::Result::Error(error)) => Err(error
|
||||
.try_into()
|
||||
.map_err(|_| LayoutParsingError::SyntaxError)?),
|
||||
None => Err(LayoutParsingError::SyntaxError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of clients, their focused pane and running command or focused plugin back as an
|
||||
@@ -1191,6 +1346,200 @@ pub fn get_pane_pid(pane_id: PaneId) -> Result<i32, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Save a layout to the user's layout directory
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `layout_name` - Name of the layout file (without .kdl extension)
|
||||
/// * `layout_kdl` - KDL string representing the layout
|
||||
/// * `overwrite` - Whether to overwrite if the file already exists
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Layout was successfully validated and saved
|
||||
/// * `Err(String)` - Error message (parse error, I/O error, file exists, etc.)
|
||||
pub fn save_layout<S: AsRef<str>>(
|
||||
layout_name: S,
|
||||
layout_kdl: S,
|
||||
overwrite: bool,
|
||||
) -> Result<(), String> {
|
||||
let plugin_command = PluginCommand::SaveLayout {
|
||||
layout_name: layout_name.as_ref().to_owned(),
|
||||
layout_kdl: layout_kdl.as_ref().to_owned(),
|
||||
overwrite,
|
||||
};
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
// Read response from stdin
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
|
||||
// Decode protobuf response
|
||||
let protobuf_response = ProtobufSaveLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Convert to Rust type
|
||||
let response = SaveLayoutResponse::try_from(protobuf_response)
|
||||
.map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
|
||||
|
||||
// Convert Result enum to actual Result type
|
||||
match response {
|
||||
SaveLayoutResponse::Ok(_) => Ok(()),
|
||||
SaveLayoutResponse::Err(error_msg) => Err(error_msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a layout from the user's layout directory.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `layout_name` - The name of the layout file to delete (without the `.kdl` extension).
|
||||
/// Layout names are sanitized server-side to prevent directory traversal.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` - The layout was successfully deleted
|
||||
/// * `Err(String)` - An error occurred with a descriptive message (e.g., file not found, permission denied)
|
||||
///
|
||||
/// # Permissions
|
||||
///
|
||||
/// Requires the `ChangeApplicationState` permission.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use zellij_tile::prelude::*;
|
||||
///
|
||||
/// // Delete a layout named "my-layout"
|
||||
/// match delete_layout("my-layout") {
|
||||
/// Ok(_) => eprintln!("Layout deleted successfully"),
|
||||
/// Err(e) => eprintln!("Failed to delete layout: {}", e),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn delete_layout<S: AsRef<str>>(layout_name: S) -> Result<(), String> {
|
||||
let plugin_command = PluginCommand::DeleteLayout {
|
||||
layout_name: layout_name.as_ref().to_owned(),
|
||||
};
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
// Read response from stdin
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
|
||||
// Decode protobuf response
|
||||
let protobuf_response = ProtobufDeleteLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Convert to Rust type
|
||||
let response = DeleteLayoutResponse::try_from(protobuf_response)
|
||||
.map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
|
||||
|
||||
// Convert Result enum to actual Result type
|
||||
match response {
|
||||
DeleteLayoutResponse::Ok(_) => Ok(()),
|
||||
DeleteLayoutResponse::Err(error_msg) => Err(error_msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rename a layout file in the user's layout directory
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `old_layout_name` - Current name of the layout (without .kdl extension)
|
||||
/// * `new_layout_name` - New name for the layout (without .kdl extension)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Layout was successfully renamed
|
||||
/// * `Err(String)` - Error message describing what went wrong
|
||||
///
|
||||
/// # Error Cases
|
||||
/// * Old layout name is invalid (empty, contains path separators, etc.)
|
||||
/// * New layout name is invalid
|
||||
/// * Source layout file doesn't exist
|
||||
/// * Target layout file already exists (no overwrite)
|
||||
/// * Layout directory not found
|
||||
/// * File system error during rename operation
|
||||
///
|
||||
/// # Permissions
|
||||
///
|
||||
/// Requires the `ChangeApplicationState` permission.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use zellij_tile::prelude::*;
|
||||
///
|
||||
/// // Rename a layout from "old-name" to "new-name"
|
||||
/// match rename_layout("old-name", "new-name") {
|
||||
/// Ok(_) => eprintln!("Layout renamed successfully"),
|
||||
/// Err(e) => eprintln!("Failed to rename layout: {}", e),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn rename_layout(
|
||||
old_layout_name: impl Into<String>,
|
||||
new_layout_name: impl Into<String>,
|
||||
) -> Result<(), String> {
|
||||
let plugin_command = PluginCommand::RenameLayout {
|
||||
old_layout_name: old_layout_name.into(),
|
||||
new_layout_name: new_layout_name.into(),
|
||||
};
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
// Read response from stdin
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
|
||||
// Decode protobuf response
|
||||
let protobuf_response = ProtobufRenameLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Convert to native response type
|
||||
let response: RenameLayoutResponse = protobuf_response
|
||||
.try_into()
|
||||
.map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
|
||||
|
||||
match response {
|
||||
RenameLayoutResponse::Ok(_) => Ok(()),
|
||||
RenameLayoutResponse::Err(error_msg) => Err(error_msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a layout file in the user's default `$EDITOR`
|
||||
pub fn edit_layout<S: AsRef<str>>(
|
||||
layout_name: S,
|
||||
context: BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
let layout_name = layout_name.as_ref().to_owned();
|
||||
let plugin_command = PluginCommand::EditLayout {
|
||||
layout_name,
|
||||
context,
|
||||
};
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
|
||||
// Read response from stdin
|
||||
let response_bytes =
|
||||
bytes_from_stdin().map_err(|e| format!("Failed to read response from stdin: {:?}", e))?;
|
||||
|
||||
// Decode protobuf response
|
||||
let protobuf_response = ProtobufEditLayoutResponse::decode(response_bytes.as_slice())
|
||||
.map_err(|e| format!("Failed to decode protobuf response: {}", e))?;
|
||||
|
||||
// Convert to Rust type
|
||||
let response = EditLayoutResponse::try_from(protobuf_response)
|
||||
.map_err(|e| format!("Failed to convert protobuf response: {}", e))?;
|
||||
|
||||
// Convert Result enum to actual Result type
|
||||
match response {
|
||||
EditLayoutResponse::Ok(_) => Ok(()),
|
||||
EditLayoutResponse::Err(error_msg) => Err(error_msg),
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch the position of the pane with this id with a different pane
|
||||
pub fn move_pane_with_pane_id(pane_id: PaneId) {
|
||||
let plugin_command = PluginCommand::MovePaneWithPaneId(pane_id);
|
||||
@@ -1641,6 +1990,25 @@ pub fn get_focused_pane(tab_position: usize, pane_manifest: &PaneManifest) -> Op
|
||||
None
|
||||
}
|
||||
|
||||
pub fn override_layout<L: AsRef<LayoutInfo>>(
|
||||
layout_info: L,
|
||||
retain_existing_terminal_panes: bool,
|
||||
retain_existing_plugin_panes: bool,
|
||||
apply_only_to_active_tab: bool,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let plugin_command = PluginCommand::OverrideLayout(
|
||||
layout_info.as_ref().clone(),
|
||||
retain_existing_terminal_panes,
|
||||
retain_existing_plugin_panes,
|
||||
apply_only_to_active_tab,
|
||||
context,
|
||||
);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
// Internal Functions
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -175,6 +175,24 @@ impl Text {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
|
||||
const ERROR_COLOR_LEVEL: usize = 6;
|
||||
let substr = substr.as_ref();
|
||||
let mut start = 0;
|
||||
let mut last_pos = None;
|
||||
|
||||
while let Some(pos) = self.text[start..].find(substr) {
|
||||
last_pos = Some(start + pos);
|
||||
start = start + pos + substr.len();
|
||||
}
|
||||
|
||||
if let Some(abs_pos) = last_pos {
|
||||
return self.color_range(ERROR_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn error_color_all(self) -> Self {
|
||||
const ERROR_COLOR_LEVEL: usize = 6;
|
||||
self.color_range(ERROR_COLOR_LEVEL, ..)
|
||||
@@ -239,6 +257,24 @@ impl Text {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn success_color_last_substring<S: AsRef<str>>(self, substr: S) -> Self {
|
||||
const SUCCESS_COLOR_LEVEL: usize = 7;
|
||||
let substr = substr.as_ref();
|
||||
let mut start = 0;
|
||||
let mut last_pos = None;
|
||||
|
||||
while let Some(pos) = self.text[start..].find(substr) {
|
||||
last_pos = Some(start + pos);
|
||||
start = start + pos + substr.len();
|
||||
}
|
||||
|
||||
if let Some(abs_pos) = last_pos {
|
||||
return self.color_range(SUCCESS_COLOR_LEVEL, abs_pos..abs_pos + substr.len());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn success_color_all(self) -> Self {
|
||||
const SUCCESS_COLOR_LEVEL: usize = 7;
|
||||
self.color_range(SUCCESS_COLOR_LEVEL, ..)
|
||||
@@ -307,6 +343,23 @@ impl Text {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn color_last_substring<S: AsRef<str>>(self, index_level: usize, substr: S) -> Self {
|
||||
let substr = substr.as_ref();
|
||||
let mut start = 0;
|
||||
let mut last_pos = None;
|
||||
|
||||
while let Some(pos) = self.text[start..].find(substr) {
|
||||
last_pos = Some(start + pos);
|
||||
start = start + pos + substr.len();
|
||||
}
|
||||
|
||||
if let Some(abs_pos) = last_pos {
|
||||
return self.color_range(index_level, abs_pos..abs_pos + substr.len());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user