6 Commits
Author SHA1 Message Date
alex 42e93346ab Merge pull request 'fixed a bug with displaying the application version in the window title' (#13) from fix-app-version into master
Release / release (push) Successful in 3m32s
Reviewed-on: #13
2026-08-31 06:52:05 +00:00
alex 9ab26afc16 fixed a bug with displaying the application version in the window title
Pull request / build (pull_request) Successful in 29s
2026-08-31 11:51:13 +05:00
alex 743974a0c1 added comments to readme (#12)
Release / release (push) Successful in 3m47s
Reviewed-on: #12
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-15 02:56:58 +00:00
alex 9c6ec489e1 modified caret mode (#11)
Reviewed-on: #11
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-14 23:07:20 +00:00
alex b2c7b31eb5 take the artifact action from a fork of our own (#10)
Release / release (push) Successful in 4m31s
Upstream v4 refuses to run anywhere but github.com: it reads
GITHUB_SERVER_URL, calls everything else a GitHub Enterprise Server and
throws before making a request. A probe run settled the rest — the name
cannot be overridden from a step, because the runner sets the GITHUB_
ones last. So the action now comes from this Gitea, forked with that one
check removed, on a fixed tag rather than a floating one.

Reviewed-on: #10
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-14 00:26:27 +00:00
alex 5670ad0e64 fixed isGhes func in release pipeline (#9)
Release / release (push) Failing after 4m12s
Reviewed-on: #9
Co-authored-by: Aleksandr Neychev <alexnejchev73@gmail.com>
2026-08-13 23:34:38 +00:00
8 changed files with 124 additions and 52 deletions
+18 -7
View File
@@ -138,9 +138,22 @@ jobs:
- name: Build the installer
run: ./Packaging/build-installer.ps1 -Version ${{ steps.version.outputs.plain }}
# The artifact is where the package waits to be uploaded to Partner Center
# The artifact is where the package waits to be uploaded to Partner Center.
#
# The action comes from this Gitea rather than from github.com, and it is
# a fork of actions/upload-artifact with one line changed. Upstream reads
# GITHUB_SERVER_URL, takes every host but github.com and its enterprise
# ones for a GitHub Enterprise Server — where the v4 artifact backend does
# not exist — and throws before it makes a request. Here that backend does
# exist and answers, but the check runs on the runner ahead of the network
# and never lets it prove itself; nor can the name be corrected from this
# file, as the runner sets the GITHUB_ ones over the env of a step. The
# fork carries the reasoning in full in its GITEA-PATCH.md.
#
# The tag is a fixed one. A floating tag is how this broke before: the
# action is fetched at run time, and what arrives can change on its own
- name: Keep the package
uses: actions/upload-artifact@v4
uses: https://git.alrakis.kz/actions/upload-artifact@v4-gitea
with:
name: msix-${{ steps.version.outputs.version }}
path: artifacts/packages/
@@ -149,7 +162,7 @@ jobs:
# The .wixpdb next to each installer is left out on purpose: it is of use
# only when something has to be traced back to the WiX source
- name: Keep the installer
uses: actions/upload-artifact@v4
uses: https://git.alrakis.kz/actions/upload-artifact@v4-gitea
with:
name: installer-${{ steps.version.outputs.plain }}
path: artifacts/installers/*.msi
@@ -167,10 +180,8 @@ jobs:
# The GITHUB_ names are what Gitea itself hands to the workflow — its
# actions repeat those of GitHub, and the addresses in them point at
# this Gitea instance. GITHUB_API_URL used not to reach the steps at
# all, so the address is put together from the server one when empty
$root = if ($env:GITHUB_API_URL) { $env:GITHUB_API_URL } else { "$env:GITHUB_SERVER_URL/api/v1" }
$api = "$root/repos/$env:GITHUB_REPOSITORY/releases"
# this Gitea instance
$api = "$env:GITHUB_API_URL/repos/$env:GITHUB_REPOSITORY/releases"
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
# Gitea makes a release of its own for a pushed tag, so the release is
+2 -2
View File
@@ -19,8 +19,8 @@
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<Product>CursorLang</Product>
<Company>Aleksandr Neichev</Company>
<Description>Shows the keyboard layout at the cursor</Description>
+62 -38
View File
@@ -68,42 +68,42 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
return;
}
bool atFixedPoint = _settings.PlacementMode == PopupPlacementMode.FixedPoint;
PopupWindowNative.Rect? anchor = TryGetAnchor();
PopupModeSettings mode = ModeFor(anchor);
PopupWindowNative.Rect work = default;
PopupWindowNative.Rect anchor = default;
double scale;
if (atFixedPoint)
if (anchor is { } at)
{
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = at.Left, Y = at.Top });
}
else
{
anchor = GetAnchor();
scale = PopupWindowNative.GetScaleAt(new PopupWindowNative.Point { X = anchor.Left, Y = anchor.Top });
(work, scale) = PopupWindowNative.GetActiveMonitorWorkArea();
}
EnsureFont(scale);
EnsureFont(mode, scale);
Size measured = MeasureText(text);
int width = measured.Width + (2 * PopupLayout.ToPixels(PaddingX, scale));
int height = measured.Height + (2 * PopupLayout.ToPixels(PaddingY, scale));
PopupWindowNative.Point position = atFixedPoint
? PopupLayout.OnScreen(
work,
_settings.FixedPoint.Position,
PopupLayout.ToPixels(_settings.FixedPoint.Offset, scale),
PopupWindowNative.Point position = anchor is { } near
? PopupLayout.NearAnchor(
near,
SideForMode(),
PopupLayout.ToPixels(mode.Offset, scale),
width,
height)
: PopupLayout.NearAnchor(
anchor,
SideForMode(),
PopupLayout.ToPixels(_settings.Current.Offset, scale),
: PopupLayout.OnScreen(
work,
_settings.FixedPoint.Position,
PopupLayout.ToPixels(mode.Offset, scale),
width,
height);
if (!Draw(text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
if (!Draw(mode, text, position, width, height, PopupLayout.ToPixels(CornerRadius, scale)))
{
return;
}
@@ -139,7 +139,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
/// kilobytes for the length of one call, the popup is shown rarely, and a cached one
/// would have to be rebuilt on every change of size, colour or scale anyway.
/// </remarks>
private bool Draw(string text, PopupWindowNative.Point at, int width, int height, int radius)
private bool Draw(
PopupModeSettings mode, string text, PopupWindowNative.Point at, int width, int height, int radius)
{
IntPtr screen = GdiNative.GetDC(IntPtr.Zero);
if (screen == IntPtr.Zero)
@@ -166,8 +167,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
GdiNative.SelectObject(memory, surface);
Fill(bits, width, height);
DrawText(memory, text, width, height);
Fill(mode, bits, width, height);
DrawText(mode, memory, text, width, height);
// GDI writes nothing into the alpha channel, so the letters it just drew are
// sitting at zero alpha and would come out invisible. The inside of the
@@ -178,7 +179,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
RoundTheCorners(bits, width, height, radius);
var size = new WindowNative.Size { Width = width, Height = height };
var alpha = (byte)Math.Clamp(Math.Round(_settings.Current.Opacity * 255), 0, 255);
var alpha = (byte)Math.Clamp(Math.Round(mode.Opacity * 255), 0, 255);
return WindowNative.SetContent(Handle, at, size, memory, alpha);
}
@@ -200,9 +201,9 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
}
}
private void Fill(IntPtr bits, int width, int height)
private static void Fill(PopupModeSettings mode, IntPtr bits, int width, int height)
{
Color background = _settings.Current.BackgroundColor;
Color background = mode.BackgroundColor;
// Straight into the bitmap rather than through a brush: the pixels have to be
// written anyway to carry an alpha channel GDI would not touch
@@ -216,7 +217,8 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
}
}
private void DrawText(IntPtr deviceContext, string text, int width, int height)
private void DrawText(
PopupModeSettings mode, IntPtr deviceContext, string text, int width, int height)
{
if (_font == IntPtr.Zero || text.Length == 0)
{
@@ -227,7 +229,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
IntPtr previousFont = GdiNative.SelectObject(deviceContext, _font);
GdiNative.SetBkMode(deviceContext, GdiNative.TRANSPARENT);
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(_settings.Current.ForegroundColor));
GdiNative.SetTextColor(deviceContext, GdiNative.ToColorRef(mode.ForegroundColor));
GdiNative.DrawText(deviceContext, text, text.Length, ref bounds,
GdiNative.DT_SINGLELINE | GdiNative.DT_CENTER | GdiNative.DT_VCENTER |
@@ -329,18 +331,40 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
return (int)((alpha << 24) | (red << 16) | (green << 8) | blue);
}
// The anchor point: the caret in the input field or the mouse cursor. The cursor
// is a rectangle of zero size, so the corner computation is shared by both
private PopupWindowNative.Rect GetAnchor()
/// <summary>
/// What the popup is placed next to, or <c>null</c> when there is nothing: the fixed
/// point mode, and the caret mode where the application reports no caret.
/// </summary>
/// <remarks>
/// The cursor is a rectangle of zero size, so the corner arithmetic is shared by it
/// and the caret.
/// </remarks>
private PopupWindowNative.Rect? TryGetAnchor() => _settings.PlacementMode switch
{
if (_settings.PlacementMode == PopupPlacementMode.AtCaret &&
CaretNative.TryGetCaretRect() is { } caret)
{
return caret;
}
PopupPlacementMode.FixedPoint => null,
PopupPlacementMode.AtCaret => CaretNative.TryGetCaretRect(),
_ => PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition()),
};
return PopupLayout.AsAnchor(PopupWindowNative.GetCursorPosition());
}
/// <summary>
/// The settings the popup is shown with: those of the mode chosen, or those of the
/// fixed point when there is no anchor to stand next to.
/// </summary>
/// <remarks>
/// The caret mode falls back to the fixed point rather than to the mouse cursor:
/// the cursor is wherever it was last left — off to a side, on another monitor, or
/// over the very text being typed — and a popup that lands there while the eyes are
/// on the caret is one that is looked for and not found. The fixed point is always
/// in the same place, so it is known where to look.
///
/// The look comes from the fixed point mode too, not just the place. The two are set
/// up together for a reason: the popup by the caret is small and quiet because it
/// sits inside a text being read, while the one in the corner of the monitor is
/// looked for on purpose and is set larger. Keeping the caret look at the corner
/// would put a popup meant to go unnoticed where nothing else draws the eye.
/// </remarks>
private PopupModeSettings ModeFor(PopupWindowNative.Rect? anchor) =>
anchor is null ? _settings.FixedPoint : _settings.Current;
// The caret has two sides to choose from and the cursor has six, so each mode names
// its own side in its own terms
@@ -373,10 +397,10 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
// The font is rebuilt only when the size in the settings or the monitor scale
// changes: it is the one expensive thing a show does. A switch of the placement
// mode counts as a change of the size, since the size belongs to the mode
private void EnsureFont(double scale)
private void EnsureFont(PopupModeSettings mode, double scale)
{
if (_font != IntPtr.Zero &&
Math.Abs(_fontSize - _settings.Current.FontSize) < 0.01 &&
Math.Abs(_fontSize - mode.FontSize) < 0.01 &&
Math.Abs(_fontScale - scale) < 0.01)
{
return;
@@ -384,7 +408,7 @@ internal sealed class NativePopupWindow : NativeWindow, ILayoutPopupWindow
ReleaseFont();
_fontSize = _settings.Current.FontSize;
_fontSize = mode.FontSize;
_fontScale = scale;
_font = GdiNative.CreateFont(_fontSize, scale);
}
+2 -2
View File
@@ -113,7 +113,7 @@ internal static class CaretNative
private static PopupWindowNative.Rect? TryGetAutomationCaret()
{
// A hung application must not hang the popup along with it: we wait for the
// answer for a limited time, otherwise we show the popup at the cursor
// answer for a limited time, otherwise the popup goes to the fixed point
Task<PopupWindowNative.Rect?> query = Task.Run(QueryAutomationCaret);
return query.Wait(AutomationTimeout) ? query.Result : null;
}
@@ -165,7 +165,7 @@ internal static class CaretNative
}
#else
// Built without UI Automation: Chromium and Electron keep the system caret and MSAA
// steps above, and where those stay silent the popup falls back to the cursor
// steps above, and where those stay silent the popup falls back to the fixed point
private static PopupWindowNative.Rect? TryGetAutomationCaret() => null;
#endif
+2 -1
View File
@@ -10,7 +10,8 @@ public enum PopupPlacementMode
/// <summary>
/// Next to the caret in the active input field. When the application does not
/// report its position, the popup is shown at the mouse cursor.
/// report its position, the popup is shown as in <see cref="FixedPoint"/> — at the
/// place and with the look that mode is set up with.
/// </summary>
AtCaret,
@@ -21,8 +21,8 @@
<PublishTrimmed>false</PublishTrimmed>
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<Product>CursorLang</Product>
<Company>Aleksandr Neichev</Company>
<Description>Settings window of CursorLang</Description>
+18
View File
@@ -169,6 +169,24 @@ MSIX всегда выполняются в контексте вошедшег
на месте, а не исчезает из раздела — появляющаяся и исчезающая строка сдвигала бы
всё, что ниже, при каждой смене места.
Положение каретки сообщает не всякое приложение, и тогда рядом с курсором ввода —
нигде. Подсказка уходит на фиксированную точку: место, отступ и оформление берутся
оттуда, из режима каретки не берётся ничего. Курсор мыши был бы запасным вариантом
поближе — и неверным: он там, где его оставили, сбоку или на другом мониторе, тогда
как смотрят на каретку. Фиксированная точка всегда на одном и том же месте, и потому
известно, куда смотреть, — а вместе с местом должно приходить и оформление: подсказка,
настроенная мелкой и незаметной для середины текста, в углу монитора останется
незамеченной.
Какие-то приложения, решает не то, на чём они написаны, а то, что окно о себе
сообщает. Обычный элемент Win32 заводит системную каретку, и её читают сразу;
Chrome и Electron рисуют свою и сообщают о ней через интерфейсы доступности.
Окну, которое рисует текст на голом холсте, сообщать нечего — и оба случая
уживаются в одном приложении: редакторы JetBrains отвечают, потому что каретку
для них держит их среда выполнения, а панель инструментов, нарисованная Compose,
не содержит текстового элемента, который нашли бы UI Automation или Java Access
Bridge. Ради такого окна фиксированная точка и нужна.
К какому режиму относится оформление, объясняет всплывающая подсказка рядом с самим
режимом, а не строка текста в разделе: ползунки ниже показывают другие числа после
переключения режима, и это вопрос, который задают один раз.
+18
View File
@@ -163,6 +163,24 @@ zero and the settings window shows it greyed out. It stays in place rather than
leaving the section — a row that comes and goes would move everything below it on
every switch of the place.
Not every application reports where its caret is, and next to the caret is then
nowhere. The popup goes to the fixed point instead — the place, the offset and the
look all from that mode, none of them from the caret one. The mouse cursor would be
the nearer fallback and is the wrong one: it is wherever it was last left, off to a
side or on another monitor, while the eyes are on the caret. The fixed point is
always in the same place, so it is known where to look — and the look must come with
it, since a popup set small and quiet for the middle of a text goes unnoticed in the
corner of a monitor.
Some applications those are is decided by what a window exposes rather than by
what it is written in. A Win32 control creates a system caret and is read at once;
Chromium and Electron draw their own and report it over the accessibility
interfaces. A window that paints its text into a bare canvas has nothing to report
at all — and both kinds live in the same application: the editors of a JetBrains
IDE answer, since the runtime keeps a caret for them, while a tool window whose
interface is painted by Compose holds no text component for UI Automation or the
Java Access Bridge to find. That window is where the fixed point earns its keep.
Which mode the look belongs to is explained in a tooltip next to the mode itself
rather than by a line of text in the section: the sliders below show other numbers
after a switch of the mode, and that is a question asked once.