logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
62
crates/terminal_view/Cargo.toml
Normal file
62
crates/terminal_view/Cargo.toml
Normal file
@@ -0,0 +1,62 @@
|
||||
[package]
|
||||
name = "terminal_view"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = ["editor/test-support", "gpui/test-support"]
|
||||
|
||||
[lib]
|
||||
path = "src/terminal_view.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
dirs.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
task.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
terminal.workspace = true
|
||||
theme.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
remote = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
semver.workspace = true
|
||||
terminal = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["log"]
|
||||
1
crates/terminal_view/LICENSE-GPL
Symbolic link
1
crates/terminal_view/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
23
crates/terminal_view/README.md
Normal file
23
crates/terminal_view/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
Design notes:
|
||||
|
||||
This crate is split into two conceptual halves:
|
||||
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
|
||||
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
|
||||
|
||||
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
|
||||
|
||||
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
|
||||
|
||||
#Input
|
||||
|
||||
There are currently many distinct paths for getting keystrokes to the terminal:
|
||||
|
||||
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
|
||||
|
||||
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
|
||||
|
||||
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
|
||||
|
||||
4. Pasted text has a separate pathway.
|
||||
|
||||
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
|
||||
96
crates/terminal_view/scripts/print256color.sh
Executable file
96
crates/terminal_view/scripts/print256color.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Tom Hale, 2016. MIT Licence.
|
||||
# Print out 256 colours, with each number printed in its corresponding colour
|
||||
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
|
||||
|
||||
set -eu # Fail on errors or undeclared variables
|
||||
|
||||
printable_colours=256
|
||||
|
||||
# Return a colour that contrasts with the given colour
|
||||
# Bash only does integer division, so keep it integral
|
||||
function contrast_colour {
|
||||
local r g b luminance
|
||||
colour="$1"
|
||||
|
||||
if (( colour < 16 )); then # Initial 16 ANSI colours
|
||||
(( colour == 0 )) && printf "15" || printf "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
|
||||
if (( colour > 231 )); then # Greyscale ramp
|
||||
(( colour < 244 )) && printf "15" || printf "0"
|
||||
return
|
||||
fi
|
||||
|
||||
# All other colours:
|
||||
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
|
||||
# See http://stackoverflow.com/a/27165165/5353461
|
||||
|
||||
# r=$(( (colour-16) / 36 ))
|
||||
g=$(( ((colour-16) % 36) / 6 ))
|
||||
# b=$(( (colour-16) % 6 ))
|
||||
|
||||
# If luminance is bright, print number in black, white otherwise.
|
||||
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
|
||||
(( g > 2)) && printf "0" || printf "15"
|
||||
return
|
||||
|
||||
# Uncomment the below for more precise luminance calculations
|
||||
|
||||
# # Calculate perceived brightness
|
||||
# # See https://www.w3.org/TR/AERT#color-contrast
|
||||
# # and http://www.itu.int/rec/R-REC-BT.601
|
||||
# # Luminance is in range 0..5000 as each value is 0..5
|
||||
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
|
||||
# (( $luminance > 2500 )) && printf "0" || printf "15"
|
||||
}
|
||||
|
||||
# Print a coloured block with the number of that colour
|
||||
function print_colour {
|
||||
local colour="$1" contrast
|
||||
contrast=$(contrast_colour "$1")
|
||||
printf "\e[48;5;%sm" "$colour" # Start block of colour
|
||||
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
|
||||
printf "\e[0m " # Reset colour
|
||||
}
|
||||
|
||||
# Starting at $1, print a run of $2 colours
|
||||
function print_run {
|
||||
local i
|
||||
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
|
||||
print_colour "$i"
|
||||
done
|
||||
printf " "
|
||||
}
|
||||
|
||||
# Print blocks of colours
|
||||
function print_blocks {
|
||||
local start="$1" i
|
||||
local end="$2" # inclusive
|
||||
local block_cols="$3"
|
||||
local block_rows="$4"
|
||||
local blocks_per_line="$5"
|
||||
local block_length=$((block_cols * block_rows))
|
||||
|
||||
# Print sets of blocks
|
||||
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
|
||||
printf "\n" # Space before each set of blocks
|
||||
# For each block row
|
||||
for (( row = 0; row < block_rows; row++ )) do
|
||||
# Print block columns for all blocks on the line
|
||||
for (( block = 0; block < blocks_per_line; block++ )) do
|
||||
print_run $(( i + (block * block_length) )) "$block_cols"
|
||||
done
|
||||
(( i += block_cols )) # Prepare to print the next row
|
||||
printf "\n"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
print_run 0 16 # The first 16 colours are spread over the whole spectrum
|
||||
printf "\n"
|
||||
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
|
||||
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey
|
||||
19
crates/terminal_view/scripts/truecolor.sh
Executable file
19
crates/terminal_view/scripts/truecolor.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copied from: https://unix.stackexchange.com/a/696756
|
||||
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
|
||||
|
||||
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
|
||||
s="/\\";
|
||||
total_cols=term_cols*term_lines;
|
||||
for (colnum = 0; colnum<total_cols; colnum++) {
|
||||
r = 255-(colnum*255/total_cols);
|
||||
g = (colnum*510/total_cols);
|
||||
b = (colnum*255/total_cols);
|
||||
if (g>255) g = 510-g;
|
||||
printf "\033[48;2;%d;%d;%dm", r,g,b;
|
||||
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
|
||||
printf "%s\033[0m", substr(s,colnum%2+1,1);
|
||||
if (colnum%term_cols==term_cols) printf "\n";
|
||||
}
|
||||
printf "\n";
|
||||
}'
|
||||
507
crates/terminal_view/src/persistence.rs
Normal file
507
crates/terminal_view/src/persistence.rs
Normal file
@@ -0,0 +1,507 @@
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use collections::HashSet;
|
||||
use futures::future::join_all;
|
||||
use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use ui::{App, Context, Window};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use db::{
|
||||
query,
|
||||
sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
|
||||
sqlez_macros::sql,
|
||||
};
|
||||
use workspace::{
|
||||
ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
|
||||
WorkspaceDb, WorkspaceId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
TerminalView, default_working_directory,
|
||||
terminal_panel::{TerminalPanel, new_terminal_pane},
|
||||
};
|
||||
|
||||
pub(crate) fn serialize_pane_group(
|
||||
pane_group: &PaneGroup,
|
||||
active_pane: &Entity<Pane>,
|
||||
cx: &mut App,
|
||||
) -> SerializedPaneGroup {
|
||||
build_serialized_pane_group(&pane_group.root, active_pane, cx)
|
||||
}
|
||||
|
||||
fn build_serialized_pane_group(
|
||||
pane_group: &Member,
|
||||
active_pane: &Entity<Pane>,
|
||||
cx: &mut App,
|
||||
) -> SerializedPaneGroup {
|
||||
match pane_group {
|
||||
Member::Axis(PaneAxis {
|
||||
axis,
|
||||
members,
|
||||
flexes,
|
||||
bounding_boxes: _,
|
||||
}) => SerializedPaneGroup::Group {
|
||||
axis: SerializedAxis(*axis),
|
||||
children: members
|
||||
.iter()
|
||||
.map(|member| build_serialized_pane_group(member, active_pane, cx))
|
||||
.collect::<Vec<_>>(),
|
||||
flexes: Some(flexes.lock().clone()),
|
||||
},
|
||||
Member::Pane(pane_handle) => {
|
||||
SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_pane(pane: &Entity<Pane>, active: bool, cx: &mut App) -> SerializedPane {
|
||||
let mut items_to_serialize = HashSet::default();
|
||||
let pane = pane.read(cx);
|
||||
let children = pane
|
||||
.items()
|
||||
.filter_map(|item| {
|
||||
let terminal_view = item.act_as::<TerminalView>(cx)?;
|
||||
if terminal_view.read(cx).terminal().read(cx).task().is_some() {
|
||||
None
|
||||
} else {
|
||||
let id = item.item_id().as_u64();
|
||||
items_to_serialize.insert(id);
|
||||
Some(id)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let active_item = pane
|
||||
.active_item()
|
||||
.map(|item| item.item_id().as_u64())
|
||||
.filter(|active_id| items_to_serialize.contains(active_id));
|
||||
|
||||
let pinned_count = pane.pinned_count();
|
||||
SerializedPane {
|
||||
active,
|
||||
children,
|
||||
active_item,
|
||||
pinned_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_terminal_panel(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
database_id: WorkspaceId,
|
||||
serialized_panel: SerializedTerminalPanel,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<Entity<TerminalPanel>>> {
|
||||
window.spawn(cx, async move |cx| {
|
||||
let terminal_panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||
cx.new(|cx| TerminalPanel::new(workspace, window, cx))
|
||||
})?;
|
||||
match &serialized_panel.items {
|
||||
SerializedItems::NoSplits(item_ids) => {
|
||||
let items = deserialize_terminal_views(
|
||||
database_id,
|
||||
project,
|
||||
workspace,
|
||||
item_ids.as_slice(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let active_item = serialized_panel.active_item_id;
|
||||
terminal_panel.update_in(cx, |terminal_panel, window, cx| {
|
||||
terminal_panel.active_pane.update(cx, |pane, cx| {
|
||||
populate_pane_items(pane, items, active_item, window, cx);
|
||||
});
|
||||
})?;
|
||||
}
|
||||
SerializedItems::WithSplits(serialized_pane_group) => {
|
||||
let center_pane = deserialize_pane_group(
|
||||
workspace,
|
||||
project,
|
||||
terminal_panel.clone(),
|
||||
database_id,
|
||||
serialized_pane_group,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
if let Some((center_group, active_pane)) = center_pane {
|
||||
terminal_panel.update(cx, |terminal_panel, _| {
|
||||
terminal_panel.center = PaneGroup::with_root(center_group);
|
||||
terminal_panel.active_pane =
|
||||
active_pane.unwrap_or_else(|| terminal_panel.center.first_pane());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(terminal_panel)
|
||||
})
|
||||
}
|
||||
|
||||
fn populate_pane_items(
|
||||
pane: &mut Pane,
|
||||
items: Vec<Entity<TerminalView>>,
|
||||
active_item: Option<u64>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Pane>,
|
||||
) {
|
||||
let mut active_item_index = None;
|
||||
for (item_index, item) in (pane.items_len()..).zip(items) {
|
||||
if Some(item.item_id().as_u64()) == active_item {
|
||||
active_item_index = Some(item_index);
|
||||
}
|
||||
pane.add_item(Box::new(item), false, false, None, window, cx);
|
||||
}
|
||||
if let Some(index) = active_item_index {
|
||||
pane.activate_item(index, false, false, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
async fn deserialize_pane_group(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
panel: Entity<TerminalPanel>,
|
||||
workspace_id: WorkspaceId,
|
||||
serialized: &SerializedPaneGroup,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Option<(Member, Option<Entity<Pane>>)> {
|
||||
match serialized {
|
||||
SerializedPaneGroup::Group {
|
||||
axis,
|
||||
flexes,
|
||||
children,
|
||||
} => {
|
||||
let mut current_active_pane = None;
|
||||
let mut members = Vec::new();
|
||||
for child in children {
|
||||
if let Some((new_member, active_pane)) = deserialize_pane_group(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
panel.clone(),
|
||||
workspace_id,
|
||||
child,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
members.push(new_member);
|
||||
current_active_pane = current_active_pane.or(active_pane);
|
||||
}
|
||||
}
|
||||
|
||||
if members.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if members.len() == 1 {
|
||||
return Some((members.remove(0), current_active_pane));
|
||||
}
|
||||
|
||||
Some((
|
||||
Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())),
|
||||
current_active_pane,
|
||||
))
|
||||
}
|
||||
SerializedPaneGroup::Pane(serialized_pane) => {
|
||||
let active = serialized_pane.active;
|
||||
|
||||
let pane = panel
|
||||
.update_in(cx, |terminal_panel, window, cx| {
|
||||
new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
terminal_panel.active_pane.read(cx).is_zoomed(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.log_err()?;
|
||||
let active_item = serialized_pane.active_item;
|
||||
let pinned_count = serialized_pane.pinned_count;
|
||||
let new_items = deserialize_terminal_views(
|
||||
workspace_id,
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
serialized_pane.children.as_slice(),
|
||||
cx,
|
||||
);
|
||||
cx.spawn({
|
||||
let pane = pane.downgrade();
|
||||
async move |cx| {
|
||||
let new_items = new_items.await;
|
||||
|
||||
let items = pane.update_in(cx, |pane, window, cx| {
|
||||
populate_pane_items(pane, new_items, active_item, window, cx);
|
||||
pane.set_pinned_count(pinned_count.min(pane.items_len()));
|
||||
pane.items_len()
|
||||
});
|
||||
// Avoid blank panes in splits
|
||||
if items.is_ok_and(|items| items == 0) {
|
||||
let working_directory = workspace
|
||||
.update(cx, |workspace, cx| default_working_directory(workspace, cx))
|
||||
.ok()
|
||||
.flatten();
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_shell(working_directory, cx)
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
let Some(terminal) = terminal else {
|
||||
return;
|
||||
};
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
let terminal_view = Box::new(cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
workspace.clone(),
|
||||
Some(workspace_id),
|
||||
project.downgrade(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
pane.add_item(terminal_view, true, false, None, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
Some((Member::Pane(pane.clone()), active.then_some(pane)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_terminal_views(
|
||||
workspace_id: WorkspaceId,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
item_ids: &[u64],
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
|
||||
let deserialized_items = join_all(item_ids.iter().filter_map(|item_id| {
|
||||
cx.update(|window, cx| {
|
||||
TerminalView::deserialize(
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
workspace_id,
|
||||
*item_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
}));
|
||||
async move {
|
||||
deserialized_items
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|item| item.log_err())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedTerminalPanel {
|
||||
pub items: SerializedItems,
|
||||
// A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced.
|
||||
pub active_item_id: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum SerializedItems {
|
||||
// The data stored before terminal splits were introduced.
|
||||
NoSplits(Vec<u64>),
|
||||
WithSplits(SerializedPaneGroup),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum SerializedPaneGroup {
|
||||
Pane(SerializedPane),
|
||||
Group {
|
||||
axis: SerializedAxis,
|
||||
flexes: Option<Vec<f32>>,
|
||||
children: Vec<SerializedPaneGroup>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedPane {
|
||||
pub active: bool,
|
||||
pub children: Vec<u64>,
|
||||
pub active_item: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub pinned_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SerializedAxis(pub Axis);
|
||||
|
||||
impl Serialize for SerializedAxis {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self.0 {
|
||||
Axis::Horizontal => serializer.serialize_str("horizontal"),
|
||||
Axis::Vertical => serializer.serialize_str("vertical"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SerializedAxis {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
match s.as_str() {
|
||||
"horizontal" => Ok(SerializedAxis(Axis::Horizontal)),
|
||||
"vertical" => Ok(SerializedAxis(Axis::Vertical)),
|
||||
invalid => Err(serde::de::Error::custom(format!(
|
||||
"Invalid axis value: '{invalid}'"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalDb(ThreadSafeConnection);
|
||||
|
||||
impl Domain for TerminalDb {
|
||||
const NAME: &str = stringify!(TerminalDb);
|
||||
|
||||
const MIGRATIONS: &[&str] = &[
|
||||
sql!(
|
||||
CREATE TABLE terminals (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER UNIQUE,
|
||||
working_directory BLOB,
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
),
|
||||
// Remove the unique constraint on the item_id table
|
||||
// SQLite doesn't have a way of doing this automatically, so
|
||||
// we have to do this silly copying.
|
||||
sql!(
|
||||
CREATE TABLE terminals2 (
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER,
|
||||
working_directory BLOB,
|
||||
PRIMARY KEY(workspace_id, item_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||
ON DELETE CASCADE
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO terminals2 (workspace_id, item_id, working_directory)
|
||||
SELECT workspace_id, item_id, working_directory FROM terminals;
|
||||
|
||||
DROP TABLE terminals;
|
||||
|
||||
ALTER TABLE terminals2 RENAME TO terminals;
|
||||
),
|
||||
sql! (
|
||||
ALTER TABLE terminals ADD COLUMN working_directory_path TEXT;
|
||||
UPDATE terminals SET working_directory_path = CAST(working_directory AS TEXT);
|
||||
),
|
||||
sql! (
|
||||
ALTER TABLE terminals ADD COLUMN custom_title TEXT;
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
db::static_connection!(TerminalDb, [WorkspaceDb]);
|
||||
|
||||
impl TerminalDb {
|
||||
query! {
|
||||
pub async fn update_workspace_id(
|
||||
new_id: WorkspaceId,
|
||||
old_id: WorkspaceId,
|
||||
item_id: ItemId
|
||||
) -> Result<()> {
|
||||
UPDATE terminals
|
||||
SET workspace_id = ?
|
||||
WHERE workspace_id = ? AND item_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_working_directory(
|
||||
&self,
|
||||
item_id: ItemId,
|
||||
workspace_id: WorkspaceId,
|
||||
working_directory: PathBuf,
|
||||
) -> Result<()> {
|
||||
log::debug!(
|
||||
"Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}"
|
||||
);
|
||||
let query =
|
||||
"INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT DO UPDATE SET
|
||||
item_id = ?1,
|
||||
workspace_id = ?2,
|
||||
working_directory = ?3,
|
||||
working_directory_path = ?4"
|
||||
;
|
||||
self.write(move |conn| {
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&item_id, 1)?;
|
||||
next_index = statement.bind(&workspace_id, next_index)?;
|
||||
next_index = statement.bind(&working_directory, next_index)?;
|
||||
statement.bind(
|
||||
&working_directory.to_string_lossy().into_owned(),
|
||||
next_index,
|
||||
)?;
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
||||
SELECT working_directory
|
||||
FROM terminals
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_custom_title(
|
||||
&self,
|
||||
item_id: ItemId,
|
||||
workspace_id: WorkspaceId,
|
||||
custom_title: Option<String>,
|
||||
) -> Result<()> {
|
||||
log::debug!(
|
||||
"Saving custom title {:?} for item {} in workspace {:?}",
|
||||
custom_title,
|
||||
item_id,
|
||||
workspace_id
|
||||
);
|
||||
self.write(move |conn| {
|
||||
let query = "INSERT INTO terminals (item_id, workspace_id, custom_title)
|
||||
VALUES (?1, ?2, ?3)
|
||||
ON CONFLICT (workspace_id, item_id) DO UPDATE SET
|
||||
custom_title = excluded.custom_title";
|
||||
let mut statement = Statement::prepare(conn, query)?;
|
||||
let mut next_index = statement.bind(&item_id, 1)?;
|
||||
next_index = statement.bind(&workspace_id, next_index)?;
|
||||
statement.bind(&custom_title, next_index)?;
|
||||
statement.exec()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn get_custom_title(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<String>> {
|
||||
SELECT custom_title
|
||||
FROM terminals
|
||||
WHERE item_id = ? AND workspace_id = ?
|
||||
}
|
||||
}
|
||||
}
|
||||
2484
crates/terminal_view/src/terminal_element.rs
Normal file
2484
crates/terminal_view/src/terminal_element.rs
Normal file
File diff suppressed because it is too large
Load Diff
2370
crates/terminal_view/src/terminal_panel.rs
Normal file
2370
crates/terminal_view/src/terminal_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
1359
crates/terminal_view/src/terminal_path_like_target.rs
Normal file
1359
crates/terminal_view/src/terminal_path_like_target.rs
Normal file
File diff suppressed because it is too large
Load Diff
87
crates/terminal_view/src/terminal_scrollbar.rs
Normal file
87
crates/terminal_view/src/terminal_scrollbar.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use gpui::{Bounds, Point, point, size};
|
||||
use terminal::Terminal;
|
||||
use ui::{Pixels, ScrollableHandle, px};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ScrollHandleState {
|
||||
line_height: Pixels,
|
||||
total_lines: usize,
|
||||
viewport_lines: usize,
|
||||
display_offset: usize,
|
||||
}
|
||||
|
||||
impl ScrollHandleState {
|
||||
fn new(terminal: &Terminal) -> Self {
|
||||
Self {
|
||||
line_height: terminal.last_content().terminal_bounds.line_height,
|
||||
total_lines: terminal.total_lines(),
|
||||
viewport_lines: terminal.viewport_lines(),
|
||||
display_offset: terminal.last_content().display_offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TerminalScrollHandle {
|
||||
state: Rc<RefCell<ScrollHandleState>>,
|
||||
pub future_display_offset: Rc<Cell<Option<usize>>>,
|
||||
}
|
||||
|
||||
impl TerminalScrollHandle {
|
||||
pub fn new(terminal: &Terminal) -> Self {
|
||||
Self {
|
||||
state: Rc::new(RefCell::new(ScrollHandleState::new(terminal))),
|
||||
future_display_offset: Rc::new(Cell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&self, terminal: &Terminal) {
|
||||
*self.state.borrow_mut() = ScrollHandleState::new(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for TerminalScrollHandle {
|
||||
fn max_offset(&self) -> Point<Pixels> {
|
||||
let state = self.state.borrow();
|
||||
point(
|
||||
Pixels::ZERO,
|
||||
state.total_lines.saturating_sub(state.viewport_lines) as f32 * state.line_height,
|
||||
)
|
||||
}
|
||||
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
let state = self.state.borrow();
|
||||
let scroll_offset = state
|
||||
.total_lines
|
||||
.saturating_sub(state.viewport_lines)
|
||||
.saturating_sub(state.display_offset);
|
||||
Point::new(Pixels::ZERO, -(scroll_offset as f32 * state.line_height))
|
||||
}
|
||||
|
||||
fn set_offset(&self, point: Point<Pixels>) {
|
||||
let state = self.state.borrow();
|
||||
let offset_delta = (point.y / state.line_height).round() as i32;
|
||||
|
||||
let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
|
||||
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
|
||||
|
||||
self.future_display_offset
|
||||
.set(Some(display_offset as usize));
|
||||
}
|
||||
|
||||
fn viewport(&self) -> Bounds<Pixels> {
|
||||
let state = self.state.borrow();
|
||||
Bounds::new(
|
||||
Point::new(px(0.), px(0.)),
|
||||
size(
|
||||
Pixels::ZERO,
|
||||
state.viewport_lines as f32 * state.line_height,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
2834
crates/terminal_view/src/terminal_view.rs
Normal file
2834
crates/terminal_view/src/terminal_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user