Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,67 @@ on:
env:
CARGO_TERM_COLOR: always

permissions:
contents: read

jobs:
build-and-test:
name: Build & Test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
build-and-test-macos:
name: Build & Test (macos-latest)
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache cargo
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: macos-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Check formatting
run: cargo fmt --check

- name: Clippy
run: cargo clippy --all-targets -- -D warnings

- name: Build
run: cargo build --verbose

- name: Test
run: cargo test --verbose

build-and-test-windows:
name: Build & Test (windows-latest)
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
key: windows-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Check formatting
run: cargo fmt --check
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
All notable changes to vterm are documented here.

## [Unreleased]
- Add split panes: divide any tab side-by-side (`⌘ D`) or stacked (`⌘ ⇧ D`), click or `⌘ [` / `⌘ ]` to focus, `⌘ W` closes the pane (or the tab when only one remains). Layouts persist across restarts and old single-pane state files keep loading.
- Enable all keyboard shortcuts on Windows via Ctrl (`Ctrl+T/D/W`, `Ctrl+C` copies when text is selected and sends SIGINT otherwise) and show `Ctrl+…` hints in Settings; AltGr combos no longer fire shortcuts or send stray control codes.


## [0.6.0] - 2026-09-12


- Add Windows support to the curl install script (install.sh) and add native install.ps1 for PowerShell.
- Embed application icon and version metadata into Windows vterm.exe binary.
- Build and package Windows release zip in the CD workflow.
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions crates/ui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
pub mod button;
pub mod modal;
pub mod text_input;

use gpui::Modifiers;

/// Menu-shortcut modifier: ⌘ on macOS, Ctrl elsewhere. Alt is excluded so
/// AltGr combos on international Windows layouts keep typing characters
/// instead of firing shortcuts.
pub fn shortcut_modifier(m: &Modifiers) -> bool {
m.platform || (!cfg!(target_os = "macos") && m.control && !m.alt)
}

#[cfg(test)]
mod tests {
use super::shortcut_modifier;
use gpui::Modifiers;

fn mods(platform: bool, control: bool, alt: bool) -> Modifiers {
Modifiers {
platform,
control,
alt,
shift: false,
function: false,
}
}

#[test]
fn shortcut_modifier_matches_platform_conventions() {
// ⌘-style always works.
assert!(shortcut_modifier(&mods(true, false, false)));
// AltGr (Ctrl+Alt) never fires shortcuts — it types characters.
assert!(!shortcut_modifier(&mods(false, true, true)));
if cfg!(target_os = "macos") {
assert!(!shortcut_modifier(&mods(false, true, false)));
} else {
assert!(shortcut_modifier(&mods(false, true, false)));
}
}
}
23 changes: 20 additions & 3 deletions crates/ui/src/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl TextField {
return consumed;
}
match key {
"c" | "x" if modifiers.platform => {
"c" | "x" if crate::shortcut_modifier(modifiers) => {
let (start, end) = match self.selection() {
Some((s, e)) => (s, e),
None => return true,
Expand All @@ -118,7 +118,7 @@ impl TextField {
}
true
}
"v" if modifiers.platform => {
"v" if crate::shortcut_modifier(modifiers) => {
if let Some(item) = cx.read_from_clipboard()
&& let Some(text) = item.text()
{
Expand Down Expand Up @@ -173,7 +173,7 @@ impl TextField {
self.caret = self.value.len();
Some(true)
}
"a" if modifiers.platform => {
"a" if crate::shortcut_modifier(modifiers) => {
self.select_all();
Some(true)
}
Expand Down Expand Up @@ -311,4 +311,21 @@ mod tests {
assert_eq!(field.apply_simple_key("backspace", &no_mods()), Some(true));
assert_eq!(field.value(), "สวัสด");
}

#[test]
fn ctrl_a_selects_all_off_macos() {
// Ctrl is the menu modifier everywhere except macOS.
let mut field = TextField::new("hi");
let ctrl = Modifiers {
control: true,
..no_mods()
};
field.apply_simple_key("a", &ctrl);
field.apply_simple_key("backspace", &no_mods());
if cfg!(target_os = "macos") {
assert_eq!(field.value(), "h", "Ctrl+A must not select on macOS");
} else {
assert_eq!(field.value(), "", "Ctrl+A must select all off macOS");
}
}
}
4 changes: 4 additions & 0 deletions crates/vterm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ fn open_window(cx: &mut App) {
let app_name = std::env::var("VTERM_APP_NAME").unwrap_or_else(|_| "vterm".to_string());
let app_version = std::env::var("VTERM_DEV_VERSION")
.unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_string());
// On macOS every TitlebarOptions field above is specified, so the struct
// update is needless there — but dropping it breaks other platforms
// where `traffic_light_position` is compiled out.
#[allow(clippy::needless_update)]
let options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(format!("{app_name} v{app_version}").into()),
Expand Down
49 changes: 43 additions & 6 deletions crates/workspace/src/components/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ fn render_terminal(workspace: &Workspace, cx: &mut Context<Workspace>) -> Div {
div()
.text_size(px(11.0))
.text_color(theme.text_muted)
.child("⌘ + / ⌘ -"),
.child(format!("{} / {}", shortcut_key("+"), shortcut_key("-"))),
),
)
.child(
Expand Down Expand Up @@ -271,14 +271,51 @@ fn render_terminal(workspace: &Workspace, cx: &mut Context<Workspace>) -> Div {
.text_color(theme.text_muted)
.child("KEYBOARD SHORTCUTS"),
)
.child(shortcut_row("New terminal", "⌘ T", theme))
.child(shortcut_row("New workspace", "⌘ N", theme))
.child(shortcut_row("Close terminal", "⌘ W", theme))
.child(shortcut_row("Reset font size", "⌘ 0", theme)),
.child(shortcut_row("New terminal", shortcut_key("T"), theme))
.child(shortcut_row("New workspace", shortcut_key("N"), theme))
.child(shortcut_row(
"Split pane side-by-side",
shortcut_key("D"),
theme,
))
.child(shortcut_row(
"Split pane stacked",
shortcut_key("Shift+D"),
theme,
))
.child(shortcut_row(
"Focus next/previous pane",
format!("{} / {}", shortcut_key("]"), shortcut_key("[")),
theme,
))
.child(shortcut_row(
"Close pane (or tab)",
shortcut_key("W"),
theme,
))
.child(shortcut_row("Reset font size", shortcut_key("0"), theme)),
)
}

fn shortcut_row(label: &'static str, shortcut: &'static str, theme: &Theme) -> Div {
/// Menu-shortcut modifier glyph per platform (mirrors `ui::shortcut_modifier`).
fn mod_glyph() -> &'static str {
if cfg!(target_os = "macos") {
"⌘"
} else {
"Ctrl"
}
}

/// Renders a shortcut hint like "⌘ T" on macOS or "Ctrl+T" on Windows.
fn shortcut_key(key: &str) -> String {
if cfg!(target_os = "macos") {
format!("{} {key}", mod_glyph())
} else {
format!("{}+{key}", mod_glyph())
}
}

fn shortcut_row(label: &'static str, shortcut: String, theme: &Theme) -> Div {
div()
.flex()
.justify_between()
Expand Down
4 changes: 2 additions & 2 deletions crates/workspace/src/components/tab_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ pub fn render_tab_bar(workspace: &Workspace, cx: &mut Context<Workspace>) -> imp
theme.text_muted
};

let drag_term = term.name.clone();
let drag_term = term.display_name().to_string();
let drag_theme = theme.clone();
let show_indicator = drop_target == Some(i);
let accent = theme.accent;
Expand Down Expand Up @@ -147,7 +147,7 @@ pub fn render_tab_bar(workspace: &Workspace, cx: &mut Context<Workspace>) -> imp
.flex()
.gap_2()
.items_center()
.child(term.name.clone())
.child(term.display_name().to_string())
.child(
div()
.id(("del-tab", i))
Expand Down
Loading
Loading