mirror of
https://github.com/socatlolmeow/softty.git
synced 2026-11-03 11:20:21 +00:00
feat: cursor blink, dead keys, hyperlinks, rich underlines, mouse tweaks, live config reload
cursor: - add blink support (blink, blink_interval config options; defaults: enabled, 500ms) - reset blink phase on keypress so the cursor is always visible right after typing - integrate blink deadline into the poll timeout alongside repeat and sync keyboard: - add XKB compose (dead key) support using the system locale - wire up zwp_text_input_v3 for IME commit-string events mouse: - add copy_on_select option (default: true) — auto-copies selection to clipboard - add right_click_paste option (default: true) — paste on right click - Ctrl+click now also opens OSC 8 hyperlinks in addition to URL-regex matches rendering: - implement distinct underline styles: single (1), double (2), curly (3), dotted (4), dashed (5) - support underline color (SGR 58/59, colon sub-parameter on SGR 4) - auto-underline cells with an active hyperlink when no explicit underline is set term: - OSC 8 hyperlink parsing and per-cell link index tracking - desktop notifications via OSC 9 and OSC 777 (notify-send) - track SGR parameter separators (;/:) to correctly handle colon sub-parameters config: - derive Clone on all config structs - wrap global CONFIG in Mutex to allow live reload - expose config::path() publicly - live config reload on mtime change (cursor blink settings, font size)
This commit is contained in:
parent
a73c5cef50
commit
4873e6e964
3 changed files with 351 additions and 30 deletions
|
|
@ -38,7 +38,7 @@ pub enum Action {
|
||||||
NewWindow,
|
NewWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub term: String,
|
pub term: String,
|
||||||
|
|
@ -51,7 +51,7 @@ pub struct Config {
|
||||||
pub shortcuts: Vec<Shortcut>,
|
pub shortcuts: Vec<Shortcut>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
@ -62,7 +62,7 @@ pub struct Window {
|
||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
pub family: String,
|
pub family: String,
|
||||||
|
|
@ -70,22 +70,24 @@ pub struct Font {
|
||||||
pub zoom_step: f32,
|
pub zoom_step: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Mouse {
|
pub struct Mouse {
|
||||||
pub word_delims: String,
|
pub word_delims: String,
|
||||||
pub multi_click_ms: u64,
|
pub multi_click_ms: u64,
|
||||||
pub scroll_lines: usize,
|
pub scroll_lines: usize,
|
||||||
pub url_launcher: String,
|
pub url_launcher: String,
|
||||||
|
pub copy_on_select: bool,
|
||||||
|
pub right_click_paste: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Scrollback {
|
pub struct Scrollback {
|
||||||
pub lines: usize,
|
pub lines: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Colors {
|
pub struct Colors {
|
||||||
#[serde(deserialize_with = "color")]
|
#[serde(deserialize_with = "color")]
|
||||||
|
|
@ -96,7 +98,7 @@ pub struct Colors {
|
||||||
pub palette: [u32; 16],
|
pub palette: [u32; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
pub style: CursorStyle,
|
pub style: CursorStyle,
|
||||||
|
|
@ -105,9 +107,11 @@ pub struct Cursor {
|
||||||
#[serde(deserialize_with = "color")]
|
#[serde(deserialize_with = "color")]
|
||||||
pub text: u32,
|
pub text: u32,
|
||||||
pub thickness: usize,
|
pub thickness: usize,
|
||||||
|
pub blink: bool,
|
||||||
|
pub blink_interval: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(try_from = "RawShortcut")]
|
#[serde(try_from = "RawShortcut")]
|
||||||
pub struct Shortcut {
|
pub struct Shortcut {
|
||||||
pub mods: u8,
|
pub mods: u8,
|
||||||
|
|
@ -115,7 +119,7 @@ pub struct Shortcut {
|
||||||
pub action: Action,
|
pub action: Action,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Clone)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct RawShortcut {
|
struct RawShortcut {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -220,7 +224,7 @@ impl Default for Font {
|
||||||
|
|
||||||
impl Default for Mouse {
|
impl Default for Mouse {
|
||||||
fn default() -> Mouse {
|
fn default() -> Mouse {
|
||||||
Mouse { word_delims: " `'\"()[]{}<>|&;,".into(), multi_click_ms: 300, scroll_lines: 3, url_launcher: "xdg-open".into() }
|
Mouse { word_delims: " `'\"()[]{}<>|&;,".into(), multi_click_ms: 300, scroll_lines: 3, url_launcher: "xdg-open".into(), copy_on_select: true, right_click_paste: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +249,7 @@ impl Default for Colors {
|
||||||
|
|
||||||
impl Default for Cursor {
|
impl Default for Cursor {
|
||||||
fn default() -> Cursor {
|
fn default() -> Cursor {
|
||||||
Cursor { style: CursorStyle::Block, color: 0x7aa2f7, text: 0x1a1b26, thickness: 2 }
|
Cursor { style: CursorStyle::Block, color: 0x7aa2f7, text: 0x1a1b26, thickness: 2, blink: true, blink_interval: 500 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,6 +296,8 @@ word_delims = " `'\"()[]{}<>|&;," # characters that end a word on double-click
|
||||||
multi_click_ms = 300 # max gap between clicks for double/triple click
|
multi_click_ms = 300 # max gap between clicks for double/triple click
|
||||||
scroll_lines = 3 # lines per wheel notch (history, or arrow keys on the alternate screen)
|
scroll_lines = 3 # lines per wheel notch (history, or arrow keys on the alternate screen)
|
||||||
url_launcher = "xdg-open"
|
url_launcher = "xdg-open"
|
||||||
|
copy_on_select = true # copy the selection to the clipboard automatically
|
||||||
|
right_click_paste = true # paste on right mouse button
|
||||||
|
|
||||||
[scrollback]
|
[scrollback]
|
||||||
lines = 10000 # history kept per terminal, 0 turns scrollback off
|
lines = 10000 # history kept per terminal, 0 turns scrollback off
|
||||||
|
|
@ -310,6 +316,8 @@ style = "block" # "block", "beam" or "underline"
|
||||||
color = "#7aa2f7"
|
color = "#7aa2f7"
|
||||||
text = "#1a1b26" # glyph color under a block cursor
|
text = "#1a1b26" # glyph color under a block cursor
|
||||||
thickness = 2 # width of beam / height of underline, in pixels
|
thickness = 2 # width of beam / height of underline, in pixels
|
||||||
|
blink = true # blink the cursor while focused
|
||||||
|
blink_interval = 500 # cursor blink interval in milliseconds
|
||||||
|
|
||||||
# Shortcuts. Setting any [[shortcuts]] replaces the whole default list.
|
# Shortcuts. Setting any [[shortcuts]] replaces the whole default list.
|
||||||
# mods: "shift", "alt", "ctrl". key: an xkb keysym name (C, Insert, Page_Up, F5, ...).
|
# mods: "shift", "alt", "ctrl". key: an xkb keysym name (C, Insert, Page_Up, F5, ...).
|
||||||
|
|
@ -392,15 +400,15 @@ key = "N"
|
||||||
action = "new_window"
|
action = "new_window"
|
||||||
"##;
|
"##;
|
||||||
|
|
||||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
static CONFIG: OnceLock<std::sync::Mutex<Config>> = OnceLock::new();
|
||||||
|
|
||||||
/// The active configuration. Falls back to the defaults if `load` has not run.
|
/// The active configuration. Falls back to the defaults if `load` has not run.
|
||||||
pub fn get() -> &'static Config {
|
pub fn get() -> Config {
|
||||||
CONFIG.get_or_init(Config::default)
|
CONFIG.get_or_init(|| std::sync::Mutex::new(Config::default())).lock().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `$XDG_CONFIG_HOME/softty/config.toml`, which is `~/.config/softty/config.toml` by default.
|
/// `$XDG_CONFIG_HOME/softty/config.toml`, which is `~/.config/softty/config.toml` by default.
|
||||||
fn path() -> Option<PathBuf> {
|
pub fn path() -> Option<PathBuf> {
|
||||||
let base = match std::env::var_os("XDG_CONFIG_HOME") {
|
let base = match std::env::var_os("XDG_CONFIG_HOME") {
|
||||||
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
|
Some(dir) if !dir.is_empty() => PathBuf::from(dir),
|
||||||
_ => PathBuf::from(std::env::var_os("HOME")?).join(".config"),
|
_ => PathBuf::from(std::env::var_os("HOME")?).join(".config"),
|
||||||
|
|
@ -417,7 +425,14 @@ pub fn load() -> Option<String> {
|
||||||
Some(Ok(config)) => (config, None),
|
Some(Ok(config)) => (config, None),
|
||||||
None => (Config::default(), None),
|
None => (Config::default(), None),
|
||||||
};
|
};
|
||||||
let _ = CONFIG.set(config);
|
match CONFIG.get() {
|
||||||
|
Some(mutex) => {
|
||||||
|
*mutex.lock().unwrap() = config.clone();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let _ = CONFIG.set(std::sync::Mutex::new(config));
|
||||||
|
}
|
||||||
|
}
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
206
src/main.rs
206
src/main.rs
|
|
@ -34,6 +34,10 @@ use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHand
|
||||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{Shape, WpCursorShapeDeviceV1};
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{Shape, WpCursorShapeDeviceV1};
|
||||||
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1;
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_manager_v1::WpCursorShapeManagerV1;
|
||||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1;
|
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1;
|
||||||
|
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_manager_v3::ZwpTextInputManagerV3;
|
||||||
|
use wayland_protocols::wp::text_input::zv3::client::zwp_text_input_v3::ZwpTextInputV3;
|
||||||
|
use wayland_protocols::wp::text_input::zv3::client as zwp_text_input_v3_mod;
|
||||||
|
use zwp_text_input_v3_mod::zwp_text_input_v3 as zwp_text_input_v3;
|
||||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::{self, ZwpPrimarySelectionDeviceV1};
|
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_device_v1::{self, ZwpPrimarySelectionDeviceV1};
|
||||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{self, ZwpPrimarySelectionOfferV1};
|
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::{self, ZwpPrimarySelectionOfferV1};
|
||||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::{self, ZwpPrimarySelectionSourceV1};
|
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_source_v1::{self, ZwpPrimarySelectionSourceV1};
|
||||||
|
|
@ -90,12 +94,14 @@ struct App {
|
||||||
cursor_shape: Option<WpCursorShapeDeviceV1>,
|
cursor_shape: Option<WpCursorShapeDeviceV1>,
|
||||||
primary_manager: Option<ZwpPrimarySelectionDeviceManagerV1>,
|
primary_manager: Option<ZwpPrimarySelectionDeviceManagerV1>,
|
||||||
primary_device: Option<ZwpPrimarySelectionDeviceV1>,
|
primary_device: Option<ZwpPrimarySelectionDeviceV1>,
|
||||||
|
text_input: Option<ZwpTextInputV3>,
|
||||||
keyboard: Option<WlKeyboard>,
|
keyboard: Option<WlKeyboard>,
|
||||||
pointer: Option<WlPointer>,
|
pointer: Option<WlPointer>,
|
||||||
|
|
||||||
xkb: xkb::Context,
|
xkb: xkb::Context,
|
||||||
keymap: Option<xkb::Keymap>,
|
keymap: Option<xkb::Keymap>,
|
||||||
xkb_state: Option<xkb::State>,
|
xkb_state: Option<xkb::State>,
|
||||||
|
compose: Option<xkb::compose::State>,
|
||||||
repeat: Option<Repeat>,
|
repeat: Option<Repeat>,
|
||||||
repeat_rate: u64,
|
repeat_rate: u64,
|
||||||
repeat_delay: u64,
|
repeat_delay: u64,
|
||||||
|
|
@ -122,6 +128,10 @@ struct App {
|
||||||
held: Option<u8>,
|
held: Option<u8>,
|
||||||
last_cell: Pos,
|
last_cell: Pos,
|
||||||
wheel: f64,
|
wheel: f64,
|
||||||
|
cursor_blink: bool,
|
||||||
|
cursor_blink_on: bool,
|
||||||
|
next_blink: Option<Instant>,
|
||||||
|
cursor_blink_interval: Duration,
|
||||||
|
|
||||||
clip_text: String,
|
clip_text: String,
|
||||||
clip_source: Option<WlDataSource>,
|
clip_source: Option<WlDataSource>,
|
||||||
|
|
@ -136,6 +146,7 @@ struct App {
|
||||||
|
|
||||||
search: Option<Search>,
|
search: Option<Search>,
|
||||||
sync_deadline: Option<Instant>,
|
sync_deadline: Option<Instant>,
|
||||||
|
config_mtime: Option<std::time::SystemTime>,
|
||||||
exit: bool,
|
exit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,7 +288,8 @@ impl App {
|
||||||
let (selection, focused) = (self.selection, self.focused);
|
let (selection, focused) = (self.selection, self.focused);
|
||||||
let search = self.search.as_ref().map(|s| (s.query.clone(), s.found));
|
let search = self.search.as_ref().map(|s| (s.query.clone(), s.found));
|
||||||
let (cx, cy) = (term.cur.x, term.cur.y);
|
let (cx, cy) = (term.cur.x, term.cur.y);
|
||||||
let cursor_visible = term.cursor_visible && term.view == 0;
|
let blink_ok = !self.cursor_blink || !self.focused || self.cursor_blink_on;
|
||||||
|
let cursor_visible = term.cursor_visible && term.view == 0 && blink_ok;
|
||||||
let style = term.cursor_style.unwrap_or(config::get().cursor.style);
|
let style = term.cursor_style.unwrap_or(config::get().cursor.style);
|
||||||
let block = matches!(style, CursorStyle::Block);
|
let block = matches!(style, CursorStyle::Block);
|
||||||
let colors = |x: usize, y: usize| {
|
let colors = |x: usize, y: usize| {
|
||||||
|
|
@ -319,7 +331,46 @@ impl App {
|
||||||
fill(px, w, ox, oy + ch / 2, cw, 1, 0xff000000 | fg);
|
fill(px, w, ox, oy + ch / 2, cw, 1, 0xff000000 | fg);
|
||||||
}
|
}
|
||||||
if cell.flags & UNDERLINE != 0 {
|
if cell.flags & UNDERLINE != 0 {
|
||||||
fill(px, w, ox, (oy + ascent + 1).min(oy + ch - 1), cw, 1, 0xff000000 | fg);
|
let ul_color = if cell.ul_color != u32::MAX { cell.ul_color } else { fg };
|
||||||
|
let color = 0xff000000 | ul_color;
|
||||||
|
let base_y = (oy + ascent + 1).min(oy + ch - 1);
|
||||||
|
match cell.underline {
|
||||||
|
2 => {
|
||||||
|
fill(px, w, ox, base_y, cw, 1, color);
|
||||||
|
fill(px, w, ox, (base_y + 2).min(oy + ch - 1), cw, 1, color);
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
for xx in 0..cw {
|
||||||
|
let yy = base_y + if xx % 3 == 1 { 0usize } else { 1usize };
|
||||||
|
if oy + yy < oy + ch {
|
||||||
|
let idx = (oy + yy) * w + (ox + xx);
|
||||||
|
if idx < px.len() {
|
||||||
|
px[idx] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
let mut xx = 0;
|
||||||
|
while xx < cw {
|
||||||
|
fill(px, w, ox + xx, base_y, 1, 1, color);
|
||||||
|
xx += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
5 => {
|
||||||
|
let mut xx = 0;
|
||||||
|
while xx < cw {
|
||||||
|
let seg = 3.min(cw - xx);
|
||||||
|
fill(px, w, ox + xx, base_y, seg, 1, color);
|
||||||
|
xx += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => fill(px, w, ox, base_y, cw, 1, color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cell.link != 0 && cell.flags & UNDERLINE == 0 {
|
||||||
|
let base_y = (oy + ascent + 1).min(oy + ch - 1);
|
||||||
|
fill(px, w, ox, base_y, cw, 1, 0xff000000 | fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -388,6 +439,11 @@ impl App {
|
||||||
if let Some(text) = self.term.clipboard.take() {
|
if let Some(text) = self.term.clipboard.take() {
|
||||||
self.set_clipboard(text, qh);
|
self.set_clipboard(text, qh);
|
||||||
}
|
}
|
||||||
|
if let Some((title, body)) = self.term.notification.take() {
|
||||||
|
let mut cmd = Command::new("notify-send");
|
||||||
|
cmd.arg(title).arg(body);
|
||||||
|
launch(&mut cmd);
|
||||||
|
}
|
||||||
if !self.term.reply.is_empty() {
|
if !self.term.reply.is_empty() {
|
||||||
let reply = std::mem::take(&mut self.term.reply);
|
let reply = std::mem::take(&mut self.term.reply);
|
||||||
self.pty.send(&reply);
|
self.pty.send(&reply);
|
||||||
|
|
@ -640,6 +696,51 @@ impl App {
|
||||||
self.search_key(sym, &text, mods, qh);
|
self.search_key(sym, &text, mods, qh);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compose (dead keys)
|
||||||
|
let compose_result: Option<Option<String>> = if let Some(compose) = &mut self.compose {
|
||||||
|
let code = xkb::Keycode::new(key + 8);
|
||||||
|
let Some(state) = &self.xkb_state else { return; };
|
||||||
|
let keysym = state.key_get_one_sym(code);
|
||||||
|
if compose.feed(keysym) == xkb::compose::FeedResult::Accepted {
|
||||||
|
match compose.status() {
|
||||||
|
xkb::compose::Status::Composed => {
|
||||||
|
let out = compose.utf8();
|
||||||
|
compose.reset();
|
||||||
|
Some(Some(out.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
xkb::compose::Status::Composing => {
|
||||||
|
Some(None)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
compose.reset();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
match compose_result {
|
||||||
|
Some(Some(text)) => {
|
||||||
|
if !text.is_empty() {
|
||||||
|
self.snap_to_bottom();
|
||||||
|
self.pty.send(text.as_bytes());
|
||||||
|
}
|
||||||
|
self.cursor_blink_on = true;
|
||||||
|
if self.cursor_blink && self.focused {
|
||||||
|
self.next_blink = Some(Instant::now() + self.cursor_blink_interval);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Some(None) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(shortcut) = config::get().shortcuts.iter().find(|s| s.mods == mods && s.sym == sym) {
|
if let Some(shortcut) = config::get().shortcuts.iter().find(|s| s.mods == mods && s.sym == sym) {
|
||||||
let page = self.term.rows.saturating_sub(1).max(1) as isize;
|
let page = self.term.rows.saturating_sub(1).max(1) as isize;
|
||||||
let consumed = match shortcut.action {
|
let consumed = match shortcut.action {
|
||||||
|
|
@ -685,6 +786,10 @@ impl App {
|
||||||
if !bytes.is_empty() {
|
if !bytes.is_empty() {
|
||||||
self.snap_to_bottom();
|
self.snap_to_bottom();
|
||||||
self.pty.send(&bytes);
|
self.pty.send(&bytes);
|
||||||
|
self.cursor_blink_on = true;
|
||||||
|
if self.cursor_blink && self.focused {
|
||||||
|
self.next_blink = Some(Instant::now() + self.cursor_blink_interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -891,6 +996,10 @@ impl Dispatch<WlKeyboard, ()> for App {
|
||||||
app.serial = serial;
|
app.serial = serial;
|
||||||
app.focused = true;
|
app.focused = true;
|
||||||
app.dirty = true;
|
app.dirty = true;
|
||||||
|
if let Some(ti) = &app.text_input {
|
||||||
|
ti.enable();
|
||||||
|
ti.commit();
|
||||||
|
}
|
||||||
if app.term.focus_events {
|
if app.term.focus_events {
|
||||||
app.pty.send(b"\x1b[I");
|
app.pty.send(b"\x1b[I");
|
||||||
}
|
}
|
||||||
|
|
@ -899,6 +1008,10 @@ impl Dispatch<WlKeyboard, ()> for App {
|
||||||
app.focused = false;
|
app.focused = false;
|
||||||
app.repeat = None;
|
app.repeat = None;
|
||||||
app.dirty = true;
|
app.dirty = true;
|
||||||
|
if let Some(ti) = &app.text_input {
|
||||||
|
ti.disable();
|
||||||
|
ti.commit();
|
||||||
|
}
|
||||||
if app.term.focus_events {
|
if app.term.focus_events {
|
||||||
app.pty.send(b"\x1b[O");
|
app.pty.send(b"\x1b[O");
|
||||||
}
|
}
|
||||||
|
|
@ -971,7 +1084,12 @@ impl Dispatch<WlPointer, ()> for App {
|
||||||
match (button, state) {
|
match (button, state) {
|
||||||
(BTN_LEFT, wl_pointer::ButtonState::Pressed) => {
|
(BTN_LEFT, wl_pointer::ButtonState::Pressed) => {
|
||||||
let (x, y) = app.cell_at(app.mouse.0, app.mouse.1);
|
let (x, y) = app.cell_at(app.mouse.0, app.mouse.1);
|
||||||
let url = (app.mods() & config::CTRL != 0).then(|| app.term.url_at(x, y)).flatten();
|
let ctrl = app.mods() & config::CTRL != 0;
|
||||||
|
let url = if ctrl {
|
||||||
|
app.term.link_at(x, y).map(str::to_string).or_else(|| app.term.url_at(x, y))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
match url {
|
match url {
|
||||||
Some(url) => app.open(&url),
|
Some(url) => app.open(&url),
|
||||||
None => app.click(),
|
None => app.click(),
|
||||||
|
|
@ -980,10 +1098,14 @@ impl Dispatch<WlPointer, ()> for App {
|
||||||
(BTN_LEFT, wl_pointer::ButtonState::Released) => {
|
(BTN_LEFT, wl_pointer::ButtonState::Released) => {
|
||||||
app.anchor = None;
|
app.anchor = None;
|
||||||
if app.selection.is_some() {
|
if app.selection.is_some() {
|
||||||
|
if config::get().mouse.copy_on_select {
|
||||||
|
app.copy(qh);
|
||||||
|
}
|
||||||
app.publish_primary(qh);
|
app.publish_primary(qh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(BTN_MIDDLE, wl_pointer::ButtonState::Pressed) => app.paste(true),
|
(BTN_MIDDLE, wl_pointer::ButtonState::Pressed) => app.paste(true),
|
||||||
|
(BTN_RIGHT, wl_pointer::ButtonState::Pressed) if config::get().mouse.right_click_paste => app.paste(false),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1080,11 +1202,26 @@ impl Dispatch<ZwpPrimarySelectionSourceV1, ()> for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn compose_state(ctx: &xkb::Context) -> Option<xkb::compose::State> {
|
||||||
|
let locale = std::env::var("LC_ALL")
|
||||||
|
.or_else(|_| std::env::var("LC_CTYPE"))
|
||||||
|
.or_else(|_| std::env::var("LANG"))
|
||||||
|
.unwrap_or_else(|_| "C.UTF-8".into());
|
||||||
|
if locale.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let table = xkb::compose::Table::new_from_locale(ctx, std::ffi::OsStr::new(&locale), xkb::compose::COMPILE_NO_FLAGS).ok()?;
|
||||||
|
Some(xkb::compose::State::new(&table, xkb::compose::STATE_NO_FLAGS))
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let config_error = config::load();
|
let config_error = config::load();
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let command = args.iter().position(|a| a == "-e").map_or(Vec::new(), |i| args[i + 1..].to_vec());
|
let command = args.iter().position(|a| a == "-e").map_or(Vec::new(), |i| args[i + 1..].to_vec());
|
||||||
|
|
||||||
|
let xkb = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||||
|
let compose = compose_state(&xkb);
|
||||||
let conn = Connection::connect_to_env()?;
|
let conn = Connection::connect_to_env()?;
|
||||||
let (globals, mut queue) = registry_queue_init::<App>(&conn)?;
|
let (globals, mut queue) = registry_queue_init::<App>(&conn)?;
|
||||||
let qh = queue.handle();
|
let qh = queue.handle();
|
||||||
|
|
@ -1098,6 +1235,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let data_device = data_manager.get_data_device(&seat, &qh, ());
|
let data_device = data_manager.get_data_device(&seat, &qh, ());
|
||||||
let primary_manager = globals.bind::<ZwpPrimarySelectionDeviceManagerV1, _, _>(&qh, 1..=1, ()).ok();
|
let primary_manager = globals.bind::<ZwpPrimarySelectionDeviceManagerV1, _, _>(&qh, 1..=1, ()).ok();
|
||||||
let primary_device = primary_manager.as_ref().map(|m| m.get_device(&seat, &qh, ()));
|
let primary_device = primary_manager.as_ref().map(|m| m.get_device(&seat, &qh, ()));
|
||||||
|
let text_input: Option<ZwpTextInputV3> = globals.bind::<ZwpTextInputManagerV3, _, _>(&qh, 1..=1, ()).ok()
|
||||||
|
.as_ref().map(|m| m.get_text_input(&seat, &qh, ()));
|
||||||
|
|
||||||
let surface = compositor.create_surface(&qh, ());
|
let surface = compositor.create_surface(&qh, ());
|
||||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||||
|
|
@ -1125,9 +1264,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
cursor_shape: None,
|
cursor_shape: None,
|
||||||
primary_manager,
|
primary_manager,
|
||||||
primary_device,
|
primary_device,
|
||||||
|
text_input,
|
||||||
keyboard: None,
|
keyboard: None,
|
||||||
pointer: None,
|
pointer: None,
|
||||||
xkb: xkb::Context::new(xkb::CONTEXT_NO_FLAGS),
|
xkb,
|
||||||
|
compose,
|
||||||
keymap: None,
|
keymap: None,
|
||||||
xkb_state: None,
|
xkb_state: None,
|
||||||
repeat: None,
|
repeat: None,
|
||||||
|
|
@ -1153,6 +1294,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
held: None,
|
held: None,
|
||||||
last_cell: (0, 0),
|
last_cell: (0, 0),
|
||||||
wheel: 0.0,
|
wheel: 0.0,
|
||||||
|
cursor_blink: config::get().cursor.blink,
|
||||||
|
cursor_blink_on: true,
|
||||||
|
next_blink: None,
|
||||||
|
cursor_blink_interval: Duration::from_millis(config::get().cursor.blink_interval.max(1)),
|
||||||
clip_text: String::new(),
|
clip_text: String::new(),
|
||||||
clip_source: None,
|
clip_source: None,
|
||||||
offer: None,
|
offer: None,
|
||||||
|
|
@ -1165,10 +1310,32 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
primary_mimes: Vec::new(),
|
primary_mimes: Vec::new(),
|
||||||
search: None,
|
search: None,
|
||||||
sync_deadline: None,
|
sync_deadline: None,
|
||||||
|
config_mtime: None,
|
||||||
exit: false,
|
exit: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// live config reload
|
||||||
|
let current_mtime = config::path().and_then(|p| std::fs::metadata(p).ok()).and_then(|m| m.modified().ok());
|
||||||
|
if app.config_mtime.is_some() && app.config_mtime != current_mtime {
|
||||||
|
let _ = config::load();
|
||||||
|
app.cursor_blink = config::get().cursor.blink;
|
||||||
|
app.cursor_blink_interval = Duration::from_millis(config::get().cursor.blink_interval.max(1));
|
||||||
|
app.fonts.set_size(config::get().font.size);
|
||||||
|
app.dirty = true;
|
||||||
|
}
|
||||||
|
app.config_mtime = current_mtime;
|
||||||
|
|
||||||
|
// blink timer scheduling
|
||||||
|
if app.focused && app.cursor_blink {
|
||||||
|
if app.next_blink.is_none() {
|
||||||
|
app.next_blink = Some(Instant::now() + app.cursor_blink_interval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.cursor_blink_on = true;
|
||||||
|
app.next_blink = None;
|
||||||
|
}
|
||||||
|
|
||||||
queue.dispatch_pending(&mut app)?;
|
queue.dispatch_pending(&mut app)?;
|
||||||
app.draw(&qh);
|
app.draw(&qh);
|
||||||
if app.exit {
|
if app.exit {
|
||||||
|
|
@ -1189,7 +1356,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
fds.push(libc::pollfd { fd: file.as_raw_fd(), events: libc::POLLIN, revents: 0 });
|
fds.push(libc::pollfd { fd: file.as_raw_fd(), events: libc::POLLIN, revents: 0 });
|
||||||
}
|
}
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let timeout = [app.repeat.map(|r| r.next), app.sync_deadline]
|
let timeout = [app.repeat.map(|r| r.next), app.sync_deadline, app.next_blink]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|deadline| deadline.saturating_duration_since(now).as_millis() as i32 + 1)
|
.map(|deadline| deadline.saturating_duration_since(now).as_millis() as i32 + 1)
|
||||||
|
|
@ -1226,5 +1393,34 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
app.on_key(repeat.key, &qh);
|
app.on_key(repeat.key, &qh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if app.focused && app.cursor_blink {
|
||||||
|
if let Some(deadline) = app.next_blink {
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
app.cursor_blink_on = !app.cursor_blink_on;
|
||||||
|
app.next_blink = Some(Instant::now() + app.cursor_blink_interval);
|
||||||
|
app.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZwpTextInputManagerV3, ()> for App {
|
||||||
|
fn event(_: &mut Self, _: &ZwpTextInputManagerV3, _: <ZwpTextInputManagerV3 as Proxy>::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZwpTextInputV3, ()> for App {
|
||||||
|
fn event(app: &mut Self, _: &ZwpTextInputV3, event: <ZwpTextInputV3 as Proxy>::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
|
||||||
|
match event {
|
||||||
|
zwp_text_input_v3::Event::CommitString { text, .. } => {
|
||||||
|
if let Some(t) = text {
|
||||||
|
app.pty.send(t.as_bytes());
|
||||||
|
app.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zwp_text_input_v3::Event::Enter { surface } => { let _ = surface; }
|
||||||
|
zwp_text_input_v3::Event::Leave { surface } => { let _ = surface; }
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
128
src/term.rs
128
src/term.rs
|
|
@ -30,11 +30,14 @@ pub struct Cell {
|
||||||
pub fg: u32,
|
pub fg: u32,
|
||||||
pub bg: u32,
|
pub bg: u32,
|
||||||
pub flags: u8,
|
pub flags: u8,
|
||||||
|
pub underline: u8,
|
||||||
|
pub ul_color: u32,
|
||||||
|
pub link: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Cell {
|
impl Cell {
|
||||||
fn blank(bg: u32) -> Cell {
|
fn blank(bg: u32) -> Cell {
|
||||||
Cell { ch: ' ', fg: default_fg(), bg, flags: 0 }
|
Cell { ch: ' ', fg: default_fg(), bg, flags: 0, underline: 0, ul_color: u32::MAX, link: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,6 +48,8 @@ pub struct Cursor {
|
||||||
fg: u32,
|
fg: u32,
|
||||||
bg: u32,
|
bg: u32,
|
||||||
flags: u8,
|
flags: u8,
|
||||||
|
underline: u8,
|
||||||
|
underline_color: u32,
|
||||||
wrap_next: bool,
|
wrap_next: bool,
|
||||||
gfx: bool,
|
gfx: bool,
|
||||||
origin: bool,
|
origin: bool,
|
||||||
|
|
@ -52,7 +57,7 @@ pub struct Cursor {
|
||||||
|
|
||||||
impl Cursor {
|
impl Cursor {
|
||||||
fn new() -> Cursor {
|
fn new() -> Cursor {
|
||||||
Cursor { x: 0, y: 0, fg: default_fg(), bg: default_bg(), flags: 0, wrap_next: false, gfx: false, origin: false }
|
Cursor { x: 0, y: 0, fg: default_fg(), bg: default_bg(), flags: 0, underline: 0, underline_color: u32::MAX, wrap_next: false, gfx: false, origin: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +85,9 @@ pub struct Term {
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub reply: Vec<u8>,
|
pub reply: Vec<u8>,
|
||||||
pub sync: bool,
|
pub sync: bool,
|
||||||
|
pub hyperlinks: Vec<Option<String>>,
|
||||||
|
pub current_link: u32,
|
||||||
|
pub notification: Option<(String, String)>,
|
||||||
pub focus_events: bool,
|
pub focus_events: bool,
|
||||||
pub cwd: Option<String>,
|
pub cwd: Option<String>,
|
||||||
pub clipboard: Option<String>,
|
pub clipboard: Option<String>,
|
||||||
|
|
@ -98,6 +106,7 @@ pub struct Term {
|
||||||
last: char,
|
last: char,
|
||||||
state: State,
|
state: State,
|
||||||
params: Vec<usize>,
|
params: Vec<usize>,
|
||||||
|
param_seps: Vec<u8>,
|
||||||
private: u8,
|
private: u8,
|
||||||
inter: u8,
|
inter: u8,
|
||||||
osc: Vec<u8>,
|
osc: Vec<u8>,
|
||||||
|
|
@ -199,6 +208,9 @@ impl Term {
|
||||||
title: None,
|
title: None,
|
||||||
reply: Vec::new(),
|
reply: Vec::new(),
|
||||||
sync: false,
|
sync: false,
|
||||||
|
hyperlinks: vec![None],
|
||||||
|
current_link: 0,
|
||||||
|
notification: None,
|
||||||
focus_events: false,
|
focus_events: false,
|
||||||
cwd: None,
|
cwd: None,
|
||||||
clipboard: None,
|
clipboard: None,
|
||||||
|
|
@ -217,6 +229,7 @@ impl Term {
|
||||||
last: ' ',
|
last: ' ',
|
||||||
state: State::Ground,
|
state: State::Ground,
|
||||||
params: vec![0],
|
params: vec![0],
|
||||||
|
param_seps: vec![b';'],
|
||||||
private: 0,
|
private: 0,
|
||||||
inter: 0,
|
inter: 0,
|
||||||
osc: Vec::new(),
|
osc: Vec::new(),
|
||||||
|
|
@ -368,6 +381,18 @@ impl Term {
|
||||||
self.history.len() + self.rows
|
self.history.len() + self.rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn link_at(&self, x: usize, y: usize) -> Option<&str> {
|
||||||
|
if x >= self.cols || y >= self.rows {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cell = self.cell(x, y);
|
||||||
|
if cell.link == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.hyperlinks.get(cell.link as usize).and_then(|u| u.as_deref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn line_cells(&self, line: usize) -> &[Cell] {
|
fn line_cells(&self, line: usize) -> &[Cell] {
|
||||||
let past = self.history.len();
|
let past = self.history.len();
|
||||||
if line < past {
|
if line < past {
|
||||||
|
|
@ -595,6 +620,35 @@ impl Term {
|
||||||
match cmd {
|
match cmd {
|
||||||
"0" | "2" => self.title = Some(rest.to_string()),
|
"0" | "2" => self.title = Some(rest.to_string()),
|
||||||
"7" => self.cwd = parse_cwd(rest),
|
"7" => self.cwd = parse_cwd(rest),
|
||||||
|
"8" => {
|
||||||
|
let (params, uri) = rest.split_once(';').unwrap_or(("", rest));
|
||||||
|
let _ = params;
|
||||||
|
if uri.is_empty() {
|
||||||
|
self.current_link = 0;
|
||||||
|
} else {
|
||||||
|
let idx = match self.hyperlinks.iter().position(|u| u.as_deref() == Some(uri)) {
|
||||||
|
Some(i) => i,
|
||||||
|
None => {
|
||||||
|
self.hyperlinks.push(Some(uri.to_string()));
|
||||||
|
self.hyperlinks.len() - 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.current_link = idx as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"9" => {
|
||||||
|
if !rest.starts_with('4') || !rest.contains(';') {
|
||||||
|
self.notification = Some(("softty".to_string(), rest.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"777" => {
|
||||||
|
let mut it = rest.splitn(3, ';');
|
||||||
|
if it.next() == Some("notify") {
|
||||||
|
let title = it.next().unwrap_or("softty").to_string();
|
||||||
|
let body = it.next().unwrap_or("").to_string();
|
||||||
|
self.notification = Some((title, body));
|
||||||
|
}
|
||||||
|
}
|
||||||
"52" => {
|
"52" => {
|
||||||
let data = rest.split_once(';').map_or("", |(_, data)| data);
|
let data = rest.split_once(';').map_or("", |(_, data)| data);
|
||||||
if let Some(text) = decode_base64(data).and_then(|bytes| String::from_utf8(bytes).ok()) {
|
if let Some(text) = decode_base64(data).and_then(|bytes| String::from_utf8(bytes).ok()) {
|
||||||
|
|
@ -618,6 +672,8 @@ impl Term {
|
||||||
b'[' => {
|
b'[' => {
|
||||||
self.params.clear();
|
self.params.clear();
|
||||||
self.params.push(0);
|
self.params.push(0);
|
||||||
|
self.param_seps.clear();
|
||||||
|
self.param_seps.push(b';');
|
||||||
self.private = 0;
|
self.private = 0;
|
||||||
self.inter = 0;
|
self.inter = 0;
|
||||||
self.state = State::Csi;
|
self.state = State::Csi;
|
||||||
|
|
@ -665,6 +721,7 @@ impl Term {
|
||||||
b';' | b':' => {
|
b';' | b':' => {
|
||||||
if self.params.len() < 32 {
|
if self.params.len() < 32 {
|
||||||
self.params.push(0);
|
self.params.push(0);
|
||||||
|
self.param_seps.push(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'<'..=b'?' => self.private = b,
|
b'<'..=b'?' => self.private = b,
|
||||||
|
|
@ -852,6 +909,7 @@ impl Term {
|
||||||
|
|
||||||
fn sgr(&mut self) {
|
fn sgr(&mut self) {
|
||||||
let p = std::mem::take(&mut self.params);
|
let p = std::mem::take(&mut self.params);
|
||||||
|
let ps = std::mem::take(&mut self.param_seps);
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while i < p.len() {
|
while i < p.len() {
|
||||||
match p[i] {
|
match p[i] {
|
||||||
|
|
@ -859,22 +917,74 @@ impl Term {
|
||||||
self.cur.fg = default_fg();
|
self.cur.fg = default_fg();
|
||||||
self.cur.bg = default_bg();
|
self.cur.bg = default_bg();
|
||||||
self.cur.flags = 0;
|
self.cur.flags = 0;
|
||||||
|
self.cur.underline = 0;
|
||||||
|
self.cur.underline_color = u32::MAX;
|
||||||
}
|
}
|
||||||
1 => self.cur.flags |= BOLD,
|
1 => self.cur.flags |= BOLD,
|
||||||
2 => self.cur.flags |= DIM,
|
2 => self.cur.flags |= DIM,
|
||||||
9 => self.cur.flags |= STRIKE,
|
3 => self.cur.flags |= 0,
|
||||||
4 => self.cur.flags |= UNDERLINE,
|
4 => {
|
||||||
|
// colon sub-parameter selects underline style
|
||||||
|
if i + 1 < p.len() && ps.get(i + 1) == Some(&b':') {
|
||||||
|
let style = p[i + 1].min(5) as u8;
|
||||||
|
if style == 0 {
|
||||||
|
self.cur.flags &= !UNDERLINE;
|
||||||
|
self.cur.underline = 0;
|
||||||
|
} else {
|
||||||
|
self.cur.flags |= UNDERLINE;
|
||||||
|
self.cur.underline = style.max(1);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
} else {
|
||||||
|
self.cur.flags |= UNDERLINE;
|
||||||
|
if self.cur.underline == 0 {
|
||||||
|
self.cur.underline = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7 => self.cur.flags |= REVERSE,
|
7 => self.cur.flags |= REVERSE,
|
||||||
|
9 => self.cur.flags |= STRIKE,
|
||||||
|
21 => {
|
||||||
|
self.cur.flags |= UNDERLINE;
|
||||||
|
self.cur.underline = 2;
|
||||||
|
}
|
||||||
22 => self.cur.flags &= !(BOLD | DIM),
|
22 => self.cur.flags &= !(BOLD | DIM),
|
||||||
29 => self.cur.flags &= !STRIKE,
|
24 => {
|
||||||
24 => self.cur.flags &= !UNDERLINE,
|
self.cur.flags &= !UNDERLINE;
|
||||||
|
self.cur.underline = 0;
|
||||||
|
}
|
||||||
27 => self.cur.flags &= !REVERSE,
|
27 => self.cur.flags &= !REVERSE,
|
||||||
|
29 => self.cur.flags &= !STRIKE,
|
||||||
30..=37 => self.cur.fg = palette(p[i] - 30),
|
30..=37 => self.cur.fg = palette(p[i] - 30),
|
||||||
40..=47 => self.cur.bg = palette(p[i] - 40),
|
40..=47 => self.cur.bg = palette(p[i] - 40),
|
||||||
90..=97 => self.cur.fg = palette(p[i] - 90 + 8),
|
90..=97 => self.cur.fg = palette(p[i] - 90 + 8),
|
||||||
100..=107 => self.cur.bg = palette(p[i] - 100 + 8),
|
100..=107 => self.cur.bg = palette(p[i] - 100 + 8),
|
||||||
39 => self.cur.fg = default_fg(),
|
39 => self.cur.fg = default_fg(),
|
||||||
49 => self.cur.bg = default_bg(),
|
49 => self.cur.bg = default_bg(),
|
||||||
|
58 => {
|
||||||
|
// set underline color, mirroring 38
|
||||||
|
let (color, used) = match (p.get(i + 1), p.get(i + 2), p.get(i + 3), p.get(i + 4), p.get(i + 5)) {
|
||||||
|
(Some(5), Some(&n), _, _, _) => (Some(color256(n)), 2),
|
||||||
|
(Some(2), Some(&0), Some(&r), Some(&g), Some(&b)) if r <= 255 && g <= 255 && b <= 255 => {
|
||||||
|
(Some((r as u32) << 16 | (g as u32) << 8 | (b as u32)), 5)
|
||||||
|
}
|
||||||
|
(Some(2), Some(&r), Some(&g), Some(&b), _) if r <= 255 && g <= 255 && b <= 255 => {
|
||||||
|
(Some((r as u32) << 16 | (g as u32) << 8 | (b as u32)), 4)
|
||||||
|
}
|
||||||
|
_ => (None, p.len()),
|
||||||
|
};
|
||||||
|
if let Some(c) = color {
|
||||||
|
self.cur.underline_color = c;
|
||||||
|
self.cur.flags |= UNDERLINE;
|
||||||
|
if self.cur.underline == 0 {
|
||||||
|
self.cur.underline = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += used;
|
||||||
|
}
|
||||||
|
59 => {
|
||||||
|
self.cur.underline_color = u32::MAX;
|
||||||
|
}
|
||||||
38 | 48 => {
|
38 | 48 => {
|
||||||
let (color, used) = match (p.get(i + 1), p.get(i + 2), p.get(i + 3), p.get(i + 4)) {
|
let (color, used) = match (p.get(i + 1), p.get(i + 2), p.get(i + 3), p.get(i + 4)) {
|
||||||
(Some(5), Some(&n), _, _) => (Some(color256(n)), 2),
|
(Some(5), Some(&n), _, _) => (Some(color256(n)), 2),
|
||||||
|
|
@ -961,15 +1071,15 @@ impl Term {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let i = self.cur.y * self.cols + self.cur.x;
|
let i = self.cur.y * self.cols + self.cur.x;
|
||||||
self.grid[i] = Cell { ch: ' ', flags: WRAP, ..Cell::blank(self.cur.bg) };
|
self.grid[i] = Cell { ch: ' ', flags: WRAP, underline: 0, ul_color: u32::MAX, link: self.current_link, ..Cell::blank(self.cur.bg) };
|
||||||
self.cur.x = 0;
|
self.cur.x = 0;
|
||||||
self.linefeed();
|
self.linefeed();
|
||||||
}
|
}
|
||||||
let i = self.cur.y * self.cols + self.cur.x;
|
let i = self.cur.y * self.cols + self.cur.x;
|
||||||
let Cursor { fg, bg, flags, .. } = self.cur;
|
let Cursor { fg, bg, flags, .. } = self.cur;
|
||||||
self.grid[i] = Cell { ch, fg, bg, flags };
|
self.grid[i] = Cell { ch, fg, bg, flags, underline: self.cur.underline, ul_color: self.cur.underline_color, link: self.current_link };
|
||||||
if width == 2 {
|
if width == 2 {
|
||||||
self.grid[i + 1] = Cell { ch: ' ', fg, bg, flags: flags | TAIL };
|
self.grid[i + 1] = Cell { ch: ' ', fg, bg, flags: flags | TAIL, underline: self.cur.underline, ul_color: self.cur.underline_color, link: self.current_link };
|
||||||
}
|
}
|
||||||
self.last = ch;
|
self.last = ch;
|
||||||
if self.cur.x + width < self.cols {
|
if self.cur.x + width < self.cols {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue