T60: fallback fonts, which a user asked for after 0.1.6: font_fallbacks lists installed families the terminal tries in order for a character Font family lacks, through gpui's Font::fallbacks; each must load as the terminal draws it; Settings › Appearance lists them under Family, each a FontPicker with a remove button, then Add fallback
This commit is contained in:
8 files changed
+194
-13
No files matched your search
@@ -117,3 +117,4 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
||||
- 2026-10-19: the terminal shows a scrollbar, which a user asked for in issue #2 after 0.1.5: a program that tracks the mouse takes the wheel, so the wheel alone could not reach the history. A thumb rides the view's right edge, over the padding, in Ely's scrollbar sizes and colors, its thickness, its least length and the foreground at 22 and 42 percent; its length is the screen's share of the history, and its place where the view stands. A drag moves the view through the history, whatever mouse mode the program asked for; the bar covers the grid's hitbox, so the terminal sees none of its presses, and a release with no press of its own reports nothing. Ely's `Scrollbar` reads a gpui `ScrollHandle`, and the terminal's place is alacritty's display offset, so the bar is the terminal's own, drawn as Ely draws hers and dragged through gpui's drag as hers is; gpui sets a drag's anchor at its first move past the threshold, so a pointer that jumps far at once loses that jump. The alternate screen holds no history, so it shows none. Under Xorg's dummy screen, 500 lines in, a drag to the top showed the first prompt and lines 1 to 39, and with mouse tracking on, which sent the wheel to the program, the bar still scrolled.
|
||||
- 2026-10-19: a plain click on the line the shell reads moves its cursor there, which a user asked for after 0.1.6, as Ghostty's `cursor-click-to-move`: a press with no modifier, one click, let go in its own cell with no selection, while OSC 133 says the shell reads a line, B since the last A or C, and the program tracks no mouse at the release. A shell whose prompt start says `click_events=1` or `2`, as fish from 4.1 says on its own, hears a click past the prompt's last cell before the cursor, or, where no prompt cell of its own line comes before the input, as under fish's empty prompt, from the first cell of the newest line typed, known by its number, not its place, since a prompt drawn at the screen's top leaves older lines below it, down to that line's last row or the cursor's, and never on a prompt cell, itself, a left press in SGR, its row the screen's or counted from the prompt's first, as kitty and Ghostty send it, and moves its own cursor, combining marks and its own redraws included: fish moves by code point, and its redraws drop the wrap flags a count would follow. Any other shell gets Left or Right arrows, one per cell, each pressed and let go as the keyboard would, through the key encoder, so DECCKM, kitty's flags and win32-input-mode shape them; the count runs only on the cursor's line, soft wraps joined, past the prompt's cells, which the prompt's link marks, so bash 3.2, which sends no C, moves nothing once its command left the prompt's line. Cells after the last character typed count only up to the cursor, so a click past the end goes to the end; a wide character takes one arrow. Never on the alternate screen. One line only, as Ghostty's `cl=line`. A right prompt or an autosuggestion past the typed text counts as text, as in Ghostty, so a click on it may take the suggestion. The first click of a double click moves the cursor before the word selects, as in Ghostty.
|
||||
- 2026-10-19: files dropped on the terminal type their paths, which a user asked for after 0.1.6, as macOS Terminal does: each path followed by a space, the paths of one drop in its order, sent as a paste, bracketed when the program asked, and kept literal for the shell the session started, which its name says, as the integration reads it: for sh, bash, zsh, fish and any other, each character Ghostty escapes gets a backslash, and `^` and `~` too, which zsh's `extendedglob` reads; for pwsh and powershell, the default on Windows, single quotes, each single quote inside doubled, PowerShell's curly ones too, since PowerShell reads a backslash as a plain character and expands `$` inside double quotes; for cmd, double quotes, which keep `&`, `|`, `<`, `>` and `^` inert, the backslashes ending a path doubled, since a program's parser reads `\"` as a quote, as `C:\` ends, and a path holding `%`, or `!`, which cmd expands inside them under `/v:on`, stops the drop. Windows Terminal's quotes around a path with a space alone leave `$(...)` and `;` live in PowerShell. A program run inside the session, `cmd` under PowerShell among them, gets the session's own quoting. A path not in UTF-8, or holding a control character, which would act as it is typed, stops the drop, logged. gpui hands a drop to the hitbox under the pointer only while the last input was not the keyboard, and a file drag counts as no input of its own: after typing, a drop with no pointer move over the window since does nothing, and nothing in ELYTerminal sees it, which the owner chose to leave so. `shot`'s `drop@x,y` plays a drop through `Window::dispatch_event`, so AppKit's drag session itself goes untested.
|
||||
- 2026-10-19: fallback fonts, which a user asked for after 0.1.6, since the Chinese and Latin fonts people like seldom come as one: `font_fallbacks` in the settings lists installed families, tried in order for a character Font family lacks, through gpui's `Font::fallbacks`, which CoreText's cascade list, DirectWrite's fallback builder and cosmic-text's chain each honor; the system's own fallback follows the last. Each family must load as the terminal draws it, as Font family must, so a family gpui would pass over without a word stops the start or the save, the error naming `font_fallbacks`; an empty name is refused. The field is new beside `font_family`, so a settings file from before loads as it was. Settings › Appearance lists them under Family: each an Ely `FontPicker` whose pick replaces it and a button that removes it, then Add fallback, which opens an empty picker whose pick adds one, its button closing it; a refused pick shows the saved family again, as Family's does. Ligatures and the bundled Nerd Font for private-use icons stand as before; the cursor preview keeps the family alone.
|
||||
@@ -52,7 +52,7 @@ Ghostty is the bar for what the terminal does. ELYTerminal owns its engine on `a
|
||||
- Inside herdr, which passes no OSC 7501 on, herdr's agent states (idle, working, blocked, done) are the session's status records.
|
||||
- Grid: colors (named, 256, truecolor, OSC 4/10/11/12), bold, dim, italic, underline (single, double, curly, dotted, dashed), strikethrough, inverse, hidden, wide characters, combining marks.
|
||||
- Box drawing (U+2500–U+257F), block elements (U+2580–U+259F) and powerline shapes (U+E0B0–U+E0B7) painted as geometry, so lines and segments join across cells.
|
||||
- Fonts: JetBrains Mono by default, any installed font by choice; Nerd Font icons the font lacks come from a bundled Nerd Font.
|
||||
- Fonts: JetBrains Mono by default, any installed font by choice, and fallback families tried in order for a character it lacks, a Chinese font after a Latin one among them; Nerd Font icons the font lacks come from a bundled Nerd Font.
|
||||
- Cursor: block, bar, underline, hollow block; blinking per setting and per DECSCUSR; hollow when the window is not focused.
|
||||
- Keys: xterm encoding with modifiers, application cursor mode, Option as Alt on macOS, the kitty keyboard protocol as the program sets its flags; a key Shift makes a symbol of goes as its own key with Shift, the symbol its alternate, as Ghostty sends it, on macOS and Windows. On Windows keys go as win32-input-mode records while ConPTY asks, as Windows Terminal sends them. Keypad keys type their characters, as Ghostty's do with mode 1035 on by default.
|
||||
- IME: marked text drawn at the cursor; committed text sent to the program.
|
||||
@@ -110,7 +110,7 @@ Cmd-, opens it; on Windows and Linux, which have no menu bar, so does a gear in
|
||||
- Appearance
|
||||
- Theme: a grid of previews; on Omarchy, the Omarchy theme first.
|
||||
- Separate dark theme: a switch; when on, a second grid for dark mode.
|
||||
- Font family: a list of installed fonts. Font size. Line height. Ligatures.
|
||||
- Font family: a list of installed fonts. Fallbacks: installed families tried in order for a character the family lacks, each added, changed or removed in a list of pickers. Font size. Line height. Ligatures.
|
||||
- Cursor: style (block, bar, underline, hollow), blink, with a live preview.
|
||||
- Background opacity: a slider. Background blur: a switch.
|
||||
- Padding: horizontal and vertical.
|
||||
|
||||
@@ -60,6 +60,8 @@ pub struct Settings {
|
||||
/// The theme while the system is dark; none keeps `theme`.
|
||||
pub dark_theme: Option<String>,
|
||||
pub font_family: String,
|
||||
/// Families tried in order for a character Font family lacks.
|
||||
pub font_fallbacks: Vec<String>,
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub ligatures: bool,
|
||||
@@ -97,6 +99,7 @@ impl Default for Settings {
|
||||
theme: themes::default_id().into(),
|
||||
dark_theme: None,
|
||||
font_family: "JetBrains Mono".into(),
|
||||
font_fallbacks: Vec::new(),
|
||||
font_size: 13.0,
|
||||
line_height: 1.2,
|
||||
ligatures: true,
|
||||
@@ -155,6 +158,10 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
anyhow::ensure!(!self.font_family.is_empty(), "font_family is empty");
|
||||
anyhow::ensure!(
|
||||
!self.font_fallbacks.iter().any(String::is_empty),
|
||||
"font_fallbacks holds an empty family"
|
||||
);
|
||||
if !self.shell.is_empty() {
|
||||
program_path(&self.shell)?;
|
||||
}
|
||||
@@ -269,6 +276,23 @@ mod tests {
|
||||
assert_eq!(error.to_string(), "panel_width is 1000, outside 160 to 960");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_fonts_are_none_unless_named_and_none_is_empty() {
|
||||
let older: Settings = serde_json::from_str(r#"{"font_family":"Menlo"}"#).expect("parses");
|
||||
assert!(older.font_fallbacks.is_empty(), "a file from before");
|
||||
let named: Settings =
|
||||
serde_json::from_str(r#"{"font_fallbacks":["Kaiti SC","Noto Sans CJK SC"]}"#)
|
||||
.expect("parses");
|
||||
assert_eq!(named.font_fallbacks, ["Kaiti SC", "Noto Sans CJK SC"]);
|
||||
named.validate().expect("named families pass");
|
||||
let empty = Settings {
|
||||
font_fallbacks: vec!["Kaiti SC".into(), String::new()],
|
||||
..Settings::default()
|
||||
};
|
||||
let error = empty.validate().expect_err("an empty family");
|
||||
assert_eq!(error.to_string(), "font_fallbacks holds an empty family");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shell_names_a_program_or_none() {
|
||||
let shell = |shell: &str| Settings {
|
||||
|
||||
+17
-6
@@ -24,7 +24,7 @@ impl SettingsStore {
|
||||
let folder = dirs::config_dir().context("settings: the platform names no config folder")?;
|
||||
let path = folder.join("ELYTerminal").join("settings.json");
|
||||
let settings = load(&path)?;
|
||||
loads(&settings.font_family, cx)
|
||||
fonts(&settings, cx)
|
||||
.with_context(|| format!("settings: {} holds a bad value", path.display()))?;
|
||||
cx.set_global(Self { settings, path });
|
||||
Ok(())
|
||||
@@ -43,8 +43,10 @@ impl SettingsStore {
|
||||
return Ok(());
|
||||
}
|
||||
next.validate()?;
|
||||
if next.font_family != store.settings.font_family {
|
||||
loads(&next.font_family, cx)?;
|
||||
if (&next.font_family, &next.font_fallbacks)
|
||||
!= (&store.settings.font_family, &store.settings.font_fallbacks)
|
||||
{
|
||||
fonts(&next, cx)?;
|
||||
}
|
||||
save(&store.path, &next)?;
|
||||
cx.global_mut::<Self>().settings = next;
|
||||
@@ -52,9 +54,18 @@ impl SettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the terminal can load `family`.
|
||||
fn loads(family: &str, cx: &App) -> anyhow::Result<()> {
|
||||
crate::term::load_font(&gpui::font(family.to_string()), cx.text_system()).map(|_| ())
|
||||
/// Checks the terminal can load its family and each fallback; the error names the field.
|
||||
fn fonts(settings: &Settings, cx: &App) -> anyhow::Result<()> {
|
||||
let fallbacks = settings
|
||||
.font_fallbacks
|
||||
.iter()
|
||||
.map(|family| ("font_fallbacks", family));
|
||||
for (field, family) in std::iter::once(("font_family", &settings.font_family)).chain(fallbacks)
|
||||
{
|
||||
crate::term::load_font(&gpui::font(family.clone()), cx.text_system())
|
||||
.with_context(|| field.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The file's settings; defaults when there is no file. A file that fails to read or check is an error.
|
||||
|
||||
+10
-1
@@ -96,6 +96,10 @@ pub struct SettingsWindow {
|
||||
/// Refusals so far; a field keyed by it drops its draft and shows the settings again.
|
||||
drafts: usize,
|
||||
font: Entity<TextInput>,
|
||||
/// A picker's input for each fallback family, and one for the family being added.
|
||||
fallbacks: Vec<Entity<TextInput>>,
|
||||
/// The empty picker that adds a fallback shows.
|
||||
adding: bool,
|
||||
/// Whether the cursor preview shows, as it blinks.
|
||||
lit: bool,
|
||||
_typed: Subscription,
|
||||
@@ -131,6 +135,8 @@ impl SettingsWindow {
|
||||
failed: None,
|
||||
drafts: 0,
|
||||
font,
|
||||
fallbacks: Vec::new(),
|
||||
adding: false,
|
||||
lit: true,
|
||||
_typed: typed,
|
||||
_settings: cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
|
||||
@@ -285,7 +291,10 @@ fn set(
|
||||
impl Render for SettingsWindow {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let page = match self.section.as_ref() {
|
||||
"appearance" => self.appearance(window, cx).into_any_element(),
|
||||
"appearance" => {
|
||||
self.grow_fallbacks(window, cx);
|
||||
self.appearance(window, cx).into_any_element()
|
||||
}
|
||||
"terminal" => self.terminal(cx).into_any_element(),
|
||||
other => unreachable!("settings: no section {other}"),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use ely_gpui_component::{
|
||||
forms::{FontPicker, NumberInput, RadioCard, Slider, Switch},
|
||||
buttons::{Button, ButtonVariant, IconButton},
|
||||
forms::{FontPicker, NumberInput, RadioCard, Slider, Switch, TextInput},
|
||||
layout::SimpleGrid,
|
||||
primitives::IconName,
|
||||
settings::{SettingsRow, SettingsSection},
|
||||
theme::{ActiveTheme, ControlSize, Mix, Radius, TextSize},
|
||||
};
|
||||
@@ -72,8 +74,12 @@ impl SettingsWindow {
|
||||
}
|
||||
}
|
||||
});
|
||||
let fallbacks = SettingsRow::new("Fallbacks")
|
||||
.description("Tried in order for characters the family lacks")
|
||||
.control(self.fallback_list(cx));
|
||||
let font = SettingsSection::new("Font")
|
||||
.row(SettingsRow::new("Family").control(div().w(theme.prose_width()).child(family)))
|
||||
.row(fallbacks)
|
||||
.row(SettingsRow::new("Size").control(self.number(
|
||||
"font_size",
|
||||
settings.font_size,
|
||||
@@ -173,6 +179,94 @@ impl SettingsWindow {
|
||||
.child(background)
|
||||
}
|
||||
|
||||
/// An input for each fallback picker and the one that adds a family.
|
||||
pub(super) fn grow_fallbacks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let wanted = SettingsStore::get(cx).font_fallbacks.len() + 1;
|
||||
while self.fallbacks.len() < wanted {
|
||||
let input = cx.new(|cx| TextInput::new(window, cx));
|
||||
self.fallbacks.push(input);
|
||||
}
|
||||
}
|
||||
|
||||
/// Each fallback family, a picker that replaces it and a button that removes it, then Add or the empty picker it opens.
|
||||
fn fallback_list(&self, cx: &Context<Self>) -> Div {
|
||||
let this = cx.entity();
|
||||
let families = SettingsStore::get(cx).font_fallbacks.clone();
|
||||
let width = cx.theme().prose_width();
|
||||
let row = |index: usize, picker: FontPicker, remove: IconButton| {
|
||||
let tip = if index < families.len() {
|
||||
"Remove"
|
||||
} else {
|
||||
"Cancel"
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.child(div().w(width).child(picker))
|
||||
.child(
|
||||
remove
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.size(ControlSize::Sm)
|
||||
.tooltip(tip),
|
||||
)
|
||||
};
|
||||
let rows = families.iter().enumerate().map(|(index, family)| {
|
||||
let picker = FontPicker::new(("font_fallback", index), &self.fallbacks[index])
|
||||
.selected(family.clone())
|
||||
.on_change(pick(&this, index));
|
||||
let remove = IconButton::new(("remove_fallback", index), IconName::X).on_click({
|
||||
let (this, family) = (this.clone(), family.clone());
|
||||
move |_, _, cx| {
|
||||
set(
|
||||
&this,
|
||||
format!("font_fallbacks drops {family:?}"),
|
||||
cx,
|
||||
|settings| {
|
||||
settings.font_fallbacks.remove(index);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
row(index, picker, remove)
|
||||
});
|
||||
let next = families.len();
|
||||
let add = if self.adding {
|
||||
let picker = FontPicker::new(("font_fallback", next), &self.fallbacks[next])
|
||||
.on_change(pick(&this, next));
|
||||
let cancel = IconButton::new("cancel_fallback", IconName::X).on_click({
|
||||
let this = this.clone();
|
||||
move |_, _, cx| this.update(cx, |this, cx| this.leave_adding(cx))
|
||||
});
|
||||
row(next, picker, cancel)
|
||||
} else {
|
||||
let add = Button::new("add_fallback", "Add fallback")
|
||||
.icon(IconName::Plus)
|
||||
.size(ControlSize::Sm)
|
||||
.on_click({
|
||||
let this = this.clone();
|
||||
move |_, _, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.adding = true;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
});
|
||||
div().child(add)
|
||||
};
|
||||
div().flex().flex_col().gap_2().children(rows).child(add)
|
||||
}
|
||||
|
||||
/// Closes the empty picker, its text gone.
|
||||
fn leave_adding(&mut self, cx: &mut Context<Self>) {
|
||||
let next = SettingsStore::get(cx).font_fallbacks.len();
|
||||
if let Some(input) = self.fallbacks.get(next) {
|
||||
input.update(cx, |input, cx| input.set_text("", cx));
|
||||
}
|
||||
self.adding = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// A field over `range`, by `step` to `places`, that saves each committed number through `put`; a refusal starts it again from the settings.
|
||||
fn number(
|
||||
&self,
|
||||
@@ -198,6 +292,45 @@ impl SettingsWindow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the family picked at fallback `index`, past the last adds one; a refusal shows the saved family again, the list closed.
|
||||
fn pick(
|
||||
this: &Entity<SettingsWindow>,
|
||||
index: usize,
|
||||
) -> impl Fn(&SharedString, &mut Window, &mut App) + 'static {
|
||||
let this = this.clone();
|
||||
move |family, window, cx| {
|
||||
let family = family.to_string();
|
||||
let adds = index == SettingsStore::get(cx).font_fallbacks.len();
|
||||
let saved = set(
|
||||
&this,
|
||||
format!("font_fallbacks[{index}] is {family:?}"),
|
||||
cx,
|
||||
|settings| {
|
||||
if adds {
|
||||
settings.font_fallbacks.push(family)
|
||||
} else {
|
||||
settings.font_fallbacks[index] = family
|
||||
}
|
||||
},
|
||||
);
|
||||
if adds && saved {
|
||||
this.update(cx, |this, cx| {
|
||||
this.adding = false;
|
||||
cx.notify();
|
||||
});
|
||||
} else if !saved {
|
||||
let shown = SettingsStore::get(cx)
|
||||
.font_fallbacks
|
||||
.get(index)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let input = this.read(cx).fallbacks[index].clone();
|
||||
input.update(cx, |input, cx| input.set_text(shown, cx));
|
||||
window.blur(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `fraction` in whole percents; the slider moves by them, so a saved fraction carries no float noise.
|
||||
fn percent(fraction: f32) -> f64 {
|
||||
(f64::from(fraction) * 100.).round()
|
||||
|
||||
+5
-3
@@ -1,7 +1,8 @@
|
||||
//! The terminal's font, cell and metrics.
|
||||
|
||||
use gpui::{
|
||||
Font, FontFeatures, FontId, FontStyle, FontWeight, Pixels, TextSystem, Window, px, size,
|
||||
Font, FontFallbacks, FontFeatures, FontId, FontStyle, FontWeight, Pixels, TextSystem, Window,
|
||||
px, size,
|
||||
};
|
||||
|
||||
use super::{ink::Ink, paint::Look};
|
||||
@@ -19,7 +20,7 @@ pub fn load_font(font: &Font, system: &TextSystem) -> anyhow::Result<FontId> {
|
||||
let loaded = system.get_font_for_id(id);
|
||||
anyhow::ensure!(
|
||||
loaded.is_some_and(|loaded| loaded.family == font.family),
|
||||
"font_family is {:?}, which the terminal cannot load",
|
||||
"the terminal cannot load {:?}",
|
||||
font.family
|
||||
);
|
||||
Ok(id)
|
||||
@@ -39,7 +40,8 @@ pub(super) fn look(
|
||||
} else {
|
||||
FontFeatures::disable_ligatures()
|
||||
},
|
||||
fallbacks: None,
|
||||
fallbacks: (!settings.font_fallbacks.is_empty())
|
||||
.then(|| FontFallbacks::from_fonts(settings.font_fallbacks.clone())),
|
||||
weight: FontWeight::NORMAL,
|
||||
style: FontStyle::Normal,
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ What no check reached: no real KWin drew the blur; of Wayland desktops, only Hyp
|
||||
| Colors, styles, five underlines, wide characters, combining marks | T06 | `term::frame`, `term::ink`, `term::paint` tests | Pass |
|
||||
| Box drawing, blocks and powerline shapes as geometry | T07 | `term::boxes` tests | Pass |
|
||||
| JetBrains Mono, any installed font, Nerd Font icons | T06, T17 | `tests/fonts.rs` | Pass |
|
||||
| Fallback fonts: installed families tried in order for a character the family lacks, listed under Family in Settings › Appearance | T60 | `settings` test: a file without `font_fallbacks` loads with none, named families keep their order, and an empty one is refused; on macOS with Menlo and `"font_fallbacks": ["Kaiti SC"]` the terminal drew `中文字体 終端` in Kaiti and Latin in Menlo, where without it the system drew them in PingFang; a fallback gpui cannot load, `No Such Font`, stopped the start with `font_fallbacks: the terminal cannot load "No Such Font"`, and an empty one with `font_fallbacks holds an empty family`; in Settings › Appearance, Add fallback opened an empty picker, picking Kaiti SC saved `["Kaiti SC"]` and the terminal took it at once, the row's X saved `[]`, and Apple Color Emoji, which gpui loads no font for, was refused and nothing saved | Pass |
|
||||
| Cursor shapes, blink by setting and DECSCUSR, hollow unfocused | T06 | T06 review | Pass |
|
||||
| xterm keys, cursor mode, Option as Alt, kitty protocol, keypad; a shifted symbol as its own key with Shift on macOS and Windows; win32-input-mode on Windows | T08, T44 | `term::keys` tests, `kitty_reports_the_key_shift_turned_into_a_symbol` red at `CSI 123;13u` before the fix; on macOS herdr 0.9.5, its `[keys]` `previous_workspace = "cmd+shift+ctrl+["` and `next_workspace = "cmd+shift+ctrl+]"`, run by `shot` in a fresh HOME: before, ⌘⇧⌃] sent `CSI 125;13u` and herdr stayed on its workspace; after, `CSI 93:125;14u` moved it to the next and ⌘⇧⌃[, `CSI 91:123;14u`, back; `term::win32` and `term::tap` tests on Windows | Pass |
|
||||
| IME: marked text at the cursor, commits to the program | T08 | T08 review | Pass |
|
||||
|
||||
Reference in new issue
Block a user